Developer PACI 921 Posted January 6, 2022 Developer Share Posted January 6, 2022 (edited) Hello everyone. Back in WoM2, at some point in time, there was this task we had to collect the locations of all monsters featured in hunting quests. One could possibly think it'd be an easy task, but I cannot imagine any of us going through all data from all quests and note down every single one of those locations. This was a particular issue for WoM2 since it ran with something we called "The Dynasties". The kingdoms or empires as we know them would have been removed, mainly due to very bad seasons with the player count. As such, the concept of Dynasty fit the game just well enough. For those who don't know, we got rid of the standard first maps and built a whole new one, making it one common first village for all dynasties. Now what kinda problems would this bring? I'd the say the hardest to wrap our heads on is adaptation. As a new map comes in, knowing the best spots, the best spawns and where to find certain enemies, come to play for a player about to explore the new area their favorite server brought for them. However, we're talking about adapting a completely new server start - which is a very big deal and not always a very good idea. As a server that possessed many risky concepts and quests of many different kinds, guiding the player through that painful and dislikeable start was a must. And on this case, to, at least, facilitate the search of the monsters the quest was telling the player to slay. This little python module I named make_monster_location.py, iterates over all the maps in the map folder, reads their regen, collects groups and groups of groups, and, ultimately, notes down every single location for every monster identified in said map. This is later saved the proper Lua multidimensional table, to be used by quest-functions show_mob_location() and clear_mob_location(). Upon finishing, a Lua module named monster_location.lua is generated. This one must be loaded (through dofile()) in one of your bootload libraries (i.e: settings.lua, questlib.lua, ...). In the spoiler below there is a sample of the generated Lua file: Spoiler monster_location = { -- metin2_map_a1 [1] = { [101] = {{628, 781}, {822, 620}, {546, 413}, {475, 501}, {751, 311}}, [102] = {{670, 829}, {586, 722}, {405, 431}, {504, 347}, {780, 264}}, [103] = {{670, 829}, {586, 722}, {405, 431}, {504, 347}, {780, 264}}, [104] = {{564, 841}, {461, 649}, {894, 571}, {305, 396}, {821, 267}, {689, 197}}, [105] = {{214, 485}, {682, 1070}, {609, 993}, {869, 747}, {293, 384}, {408, 238}, {532, 167}}, [109] = {{214, 485}, {682, 1070}, {609, 993}, {869, 747}, {293, 384}, {408, 238}, {532, 167}}, [110] = {{806, 830}, {164, 598}, {238, 414}, {922, 325}, {338, 255}, {483, 179}, {589, 93}}, [106] = {{806, 830}, {164, 598}, {238, 414}, {922, 325}, {338, 255}, {483, 179}, {589, 93}}, [111] = {{806, 830}, {164, 598}, {238, 414}, {922, 325}, {338, 255}, {483, 179}, {589, 93}}, [107] = {{448, 1044}, {373, 961}, {861, 888}, {120, 551}, {206, 374}, {277, 210}, {732, 135}, {614, 65}}, [114] = {{448, 1044}, {373, 961}, {861, 888}, {120, 551}, {206, 374}, {277, 210}, {732, 135}, {614, 65}}, [112] = {{448, 1044}, {373, 961}, {861, 888}, {120, 551}, {206, 374}, {277, 210}, {732, 135}, {614, 65}}, [113] = {{923, 960}, {228, 832}, {136, 749}, {681, 75}}, [115] = {{923, 960}, {228, 832}, {136, 749}, {681, 75}}, [301] = {{131, 419}, {205, 255}, {275, 185}}, [302] = {{131, 419}, {205, 255}, {275, 185}}, [351] = {{131, 419}, {205, 255}, {275, 185}}, [352] = {{131, 419}, {205, 255}, {275, 185}}, [304] = {{136, 353}, {213, 195}, {290, 89}}, [303] = {{136, 353}, {213, 195}, {290, 89}}, [395] = {{136, 353}, {213, 195}, {290, 89}}, [396] = {{136, 353}, {213, 195}, {290, 89}}, [391] = {{247, 143}, {113, 67}}, [392] = {{247, 143}, {113, 67}}, [393] = {{247, 143}, {113, 67}}, [398] = {{247, 143}, {113, 67}}, [397] = {{247, 143}, {113, 67}}, [394] = {{334, 141}, {128, 383}}, }, -- metin2_map_capedragonhead [301] = { [3202] = {{1246, 525}, {1347, 411}, {1099, 338}, {983, 254}, {1174, 184}}, [3203] = {{1099, 338}, {983, 254}, {1174, 184}}, [3204] = {{1077, 395}, {974, 323}}, [3205] = {{1077, 395}, {974, 323}}, [3201] = {{1246, 525}, {1342, 273}, {970, 183}}, [3003] = {{996, 701}, {305, 631}, {862, 556}, {501, 486}, {665, 402}, {184, 305}, {430, 235}, {595, 163}}, [3004] = {{862, 556}, {501, 486}, {665, 402}, {184, 305}, {430, 235}, {595, 163}}, [3002] = {{783, 930}, {1076, 819}, {996, 701}, {305, 631}, {862, 556}, {501, 486}, {665, 402}, {184, 305}, {430, 235}, {595, 163}}, [3005] = {{862, 556}, {501, 486}, {665, 402}, {184, 305}, {430, 235}, {595, 163}}, [3501] = {{1035, 1221}, {965, 1080}, {881, 986}, {1110, 851}, {1356, 777}, {1205, 702}, {1281, 630}}, [3502] = {{1035, 1221}, {965, 1080}, {881, 986}, {1110, 851}, {1356, 777}, {1205, 702}, {1281, 630}}, [3503] = {{1196, 1180}, {1274, 933}, {1398, 792}}, [3504] = {{1294, 1221}, {1438, 1138}}, [3505] = {{1294, 1221}, {1438, 1138}}, [3001] = {{378, 1086}, {295, 927}, {783, 842}, {660, 770}, {1157, 656}, {1035, 571}}, }, } function show_mob_location(mob_vnum, map_index, map_x, map_y) if not monster_location[map_index] or not monster_location[map_index][mob_vnum] or pc.get_map_index() ~= map_index then return end for _, location in ipairs(monster_location[map_index][mob_vnum]) do addmapsignal(location[1]*100, location[2]*100) end setmapcenterposition(map_x or 200, map_y or 0) end function clear_mob_location() clearmapsignal() setskin(NOWINDOW) end I talked about two new quest-functions previously. Those are the ones you should use in your quests, each one of them works as you can possibly figure: show_mob_location(mob_vnum, map_index, map_x, map_y) -- Displays the locations of the given monster, as long as the player is in the specified map. clear_mob_location() -- Clears the signals inserted by the previous function call. And below, there is the module: Spoiler # Number of coordinates the application will be able to store for a given mob MAX_COORDINATES_NUM = 10 # Defines a range between coordinates so that we don't face overlaps too often COORDINATES_RANGE = 70 # List of maps we don't want to process MAPS_BLACKLIST = ( "metin2_map_monkey_dungeon_11", "metin2_map_monkey_dungeon_12", "metin2_map_monkey_dungeon_13", "metin2_map_deviltower1", "metin2_map_trent02", "metin2_map_spiderdungeon_02", "metin2_map_skipia_dungeon_01", "metin2_map_skipia_dungeon_02", "wom_map_citadel", "metin2_map_spiderdungeon", "metin2_map_monkey_dungeon2", "metin2_map_monkey_dungeon3", "metin2_map_sungzi_snow_pass01", "metin2_map_sungzi_desert_hill_02", "metin2_map_empirewar01", "metin2_map_empirewar02", "metin2_map_empirewar03", "metin2_map_dd", "metin2_map_n_flame_dungeon_01", "metin2_map_n_flame_02", "metin2_map_spiderdungeon_03", "metin2_map_holyplace_flame" ) # The application will read all of the regen files present on this tuple REGEN_FILES = ("regen.txt", ) # Defines all the regen types existing in the game REGEN_TYPE_MOB = ("m", "ma", "mc") REGEN_TYPE_GROUP = ("g", "ga", "gc") REGEN_TYPE_GROUP_GROUP = ("r", "ra", "rc") REGEN_TYPE_EXCEPTION = ("e", ) # Ignored REGEN_TYPE_RANDOM = ("s", ) # Ignored # Names of the group-related files GROUP_FILE_NAME = ("group.txt", "group_group.txt") class Map: class Mob: def __init__(self): self.spawn_vnum = -1 self.coordinates = [] def SetVnum(self, vnum): self.spawn_vnum = vnum def SetCoordinates(self, x, y): if len(self.coordinates) >= MAX_COORDINATES_NUM: return for (_x, _y) in self.coordinates: # Too close! if abs(x - _x) < COORDINATES_RANGE or abs(y - _y) < COORDINATES_RANGE: return self.coordinates.append((x, y)) def GetVnum(self): return self.spawn_vnum def PopCoordinates(self): if len(self.coordinates): return self.coordinates.pop() return None def __init__(self): self.index = -1 self.name = "NoName" self.path = None self.regen = [] self.mob_groups = None self.mob_group_groups = None def SetIndex(self, index): self.index = index def SetName(self, name): self.name = name def GetIndex(self): return self.index def GetName(self): return self.name def SetPath(self, locale_path): self.path = locale_path + self.name + "/" def BindGroups(self, groups): self.mob_groups = groups def BindGroupGroups(self, group_groups): self.mob_group_groups = group_groups def GetGroup(self, vnum): for group in self.mob_groups: if group.GetVnum() == vnum: return group return None def GetGroupGroups(self, vnum): for group in self.mob_group_groups: if group.GetVnum() == vnum: return group return None def FindMob(self, vnum): for mob in self.regen: if vnum == mob.GetVnum(): return mob return None def AppendMob(self, vnum, x, y): mob = self.FindMob(vnum) if mob: mob.SetCoordinates(x, y) return mob = self.Mob() mob.SetVnum(vnum) mob.SetCoordinates(x, y) self.regen.append(mob) def ParseRegen(self): if not self.path: print("ERROR: Map path not defined.") return # Here we will have all the regeneration entries of this map (stone + boss + regen) full_regen = [] # Get the contents for regen_file in REGEN_FILES: try: with open(self.path + regen_file) as file: for line in file: # Ignore empty lines and comments if not line or line.startswith("//"): continue # Append full_regen.append(line.split()) except IOError: print(">> WARNING: Could not find %s/%s. Ignoring..." % (self.name, regen_file)) continue # Now parse the result accordingly with the types provided for regen_entry in full_regen: if not regen_entry: # Nothing to do continue # Unpack all the relevant data (regen_type, x, y, _, _, _, _, _, _, _, vnum) = regen_entry # Convert everything x = int(x) y = int(y) vnum = int(vnum) # It's just a mob after all. Simply add it if regen_type in REGEN_TYPE_MOB: self.AppendMob(vnum, x, y) continue # It's a group! Get all the mobs that belong to this group and append them afterwards if regen_type in REGEN_TYPE_GROUP: group = self.GetGroup(vnum) if group: for member in group.GetMembers(): self.AppendMob(member, x, y) continue # Group of groups! Iterate over it and gather all the mobs of the groups attached to it individualy if regen_type in REGEN_TYPE_GROUP_GROUP: group_group = self.GetGroupGroups(vnum) if group_group: for gg_member in group_group.GetMembers(): group = self.GetGroup(gg_member) if group: for member in group.GetMembers(): self.AppendMob(member, x, y) def GetRegen(self): return self.regen class Group: TYPE_GROUP = 0 TYPE_GROUP_GROUP = 1 def __init__(self): self.vnum = -1 self.type = self.TYPE_GROUP self.members = {} def SetType(self, type): self.type = type def SetVnum(self, vnum): self.vnum = vnum def SetMember(self, index, vnum): self.members[index] = vnum def SetLeader(self, vnum): self.SetMember(0, vnum) def IsMember(self, vnum): for index, mob in self.members.items(): if vnum == mob["vnum"]: return True return False def GetVnum(self): return self.vnum def GetMembers(self): return self.members.values() class Main: def __init__(self, locale_path): self.locale_path = locale_path self.groups = [] self.group_groups = [] self.maps = [] def ReadGroupFile(self, type): print("Loading %s/%s..." % (self.locale_path, GROUP_FILE_NAME[type])) with open("%s/%s" % (self.locale_path, GROUP_FILE_NAME[type])) as file: content = file.read() while True: group_start = content.find("{") group_end = content.find("}") # Finished everything! if group_start == -1 or group_end == -1: break # We get the data based off of the positions of the brackets group_data = content[group_start + 1 : group_end] group_data = group_data.split() # Now we get rid of them for the next read content = content.replace("}", "", 1) content = content.replace("{", "", 1) # Attempt to process the received data if not group_data: continue # Initialize our group group = Group() group.SetType(type) # First thing to set is the group vnum group_data.pop(0) # "Vnum" group_vnum = int(group_data.pop(0)) # Actual numeric value corresponding to the vnum group.SetVnum(group_vnum) # Now get all the members for i in range(0, len(group_data), 3): member_index = group_data[i] member_vnum = int(group_data[i + 1]) if type == Group.TYPE_GROUP_GROUP else int(group_data[i + 2]) if member_index == "Leader": group.SetLeader(member_vnum) else: group.SetMember(int(member_index), member_vnum) if type == Group.TYPE_GROUP: self.groups.append(group) elif type == Group.TYPE_GROUP_GROUP: self.group_groups.append(group) else: print("ERROR: Unknown group type (%d)." % type) break def ReadMapIndexFile(self): print("Loading maps...") with open("%s/map/index" % self.locale_path) as file: content = file.read().split() for i in range(0, len(content), 2): index = int(content[i]) map_name = content[i + 1] # Ooops! We don't want you! if map_name in MAPS_BLACKLIST: continue print("> Loading %s..." % map_name) # Create the map instance current_map = Map() current_map.SetIndex(index) current_map.SetName(map_name) current_map.SetPath(self.locale_path + "/map/") current_map.BindGroups(self.groups) current_map.BindGroupGroups(self.group_groups) current_map.ParseRegen() # Store it self.maps.append(current_map) def BuildMobLocationFile(self): print("Generating monster_location.lua...") with open(self.locale_path + "/monster_location.lua", "w") as file: file.write("monster_location = {\n") # Initialize the lua table for map in self.maps: regen = map.GetRegen() if not regen: # Since the regen of this map is empty, it serves no purpose using it continue file.write("\t-- %s\n" % map.GetName()) # Map identifier file.write("\t[%d] = {\n" % map.GetIndex()) # Initialize the current map table for mob in regen: # Iterate over the regen of this map file.write("\t\t[%d] = {" % mob.GetVnum()) # Now for our current mob # Add them locations! coordinates = mob.PopCoordinates() while coordinates: file.write("{%d, %d}" % (coordinates[0], coordinates[1])) coordinates = mob.PopCoordinates() if coordinates: file.write(", ") file.write("},\n") # Close mob's table file.write("\t},\n\n") # Close map's table file.write("}\n\n") # Close the main table # Finally, write the functions we are going to use in our quests file.write( "function show_mob_location(mob_vnum, map_index, map_x, map_y)\n" " if not monster_location[map_index] or not monster_location[map_index][mob_vnum] or pc.get_map_index() ~= map_index then\n" " return\n" " end\n\n" " for _, location in ipairs(monster_location[map_index][mob_vnum]) do\n" " addmapsignal(location[1]*100, location[2]*100)\n" " end\n\n" " setmapcenterposition(map_x or 200, map_y or 0)\n" "end\n\n" "function clear_mob_location()\n" " clearmapsignal()\n" " setskin(NOWINDOW)\n" "end" ) print("Done") main = Main(".") main.ReadGroupFile(Group.TYPE_GROUP) main.ReadGroupFile(Group.TYPE_GROUP_GROUP) main.ReadMapIndexFile() main.BuildMobLocationFile() As it follows one single file, I saw no purpose in uploading it - but if I see it fits, sure can do and edit the post later. All credits for the idea go to Shogun. Edited January 6, 2022 by PACI 7 8 10 when you return 0 and server doesn't boot: Link to comment Share on other sites More sharing options...
Ymir 4 Posted January 7, 2022 Share Posted January 7, 2022 Interesting, thanks for sharing. Link to comment Share on other sites More sharing options...
Recommended Posts