Jump to content

[Python] Monster Location Module


Recommended Posts

  • Developer

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.luaquestlib.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 by PACI
  • Metin2 Dev 7
  • Good 8
  • Love 10

when you return 0 and server doesn't boot:

unknown.png

Link to comment
Share on other sites

Announcements



×
×
  • Create New...

Important Information

Terms of Use / Privacy Policy / Guidelines / We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.