Jump to content

Recommended Posts

  • Honorable Member

First of all, thanks to @ Syreldar for bringing this matter to my attention and prompting me to examine this script more closely.
As the title suggests, you may be curious about the specific improvements and fixes that were made, as well as the nature of this script.

 

About the script and what it does.

The pre_qc.py script serves as a pre-compiler, specifically designed for the quest language. In the quest language, only local variables are allowed, and it does not support the usage of global variables that extend across states, whens, or functions. The language's structure restricts the use of global "variables" in an efficient manner. Consequently, creating a function like "setting()" to emulate a global constant becomes inefficient.

 

What was fixed?

The main fix made in this script was about the incomplete group implementation which is defined as `define group` instead of `define`.
With the fix, it is now possible not only to use `define group` correctly but also to index through these tables, similar to indexing a regular Lua table.
However, dynamic indexing of the global table is not supported, meaning you cannot pass a local variable to index the table (e.g., table[i]). It is necessary to index by number.

Another improvement made to the script pertains to strings. Previously, only contiguous strings without spaces were allowed. Now, it is possible to define strings that contain spaces.

 

How to define global values?

Defining them are very easy and simple, just put them at the top of your quest. They work very similar as C's preprocessor to allow the use of global constants.

define MACRO_A 9009
define MACRO_B "Hello World"
define group MACRO_C ["Hello", "World"]

quest pre_qc_test begin
	state start begin
		when MACRO_A.chat.MACRO_B begin

			say_title(MACRO_B) -- "Hello World"
			say(string.format("%s %s", MACRO_C[1], MACRO_C[2])) -- "Hello World"

			local a = MACRO_C -- { "Hello", "World" }
			say_reward(string.format("%s %s!", a[1], a[2])) -- "Hello World!"

			--[[
			for i = table.getn(MACRO_C), 1, -1 do
				print(MACRO_C[i]) -- UNSUPPORTED!
			end
			]]
		end
	end
end

 

The Script

The script itself remains largely unchanged in its original form. However, a few modifications have been made to enhance its functionality. Firstly, a main guard has been added to ensure proper execution. Additionally, the replace function has been edited to ensure the correct functioning of groups. Similarly, the my_split function has been modified to preserve strings accurately.

Furthermore, the token list has been slightly adjusted to accommodate the specific "macros" that need to be replaced. However, it can be further extended if additional macros require replacement in the future.

pre_qc.py

Spoiler
# -*- coding: 949 -*-

# ¸» ±×´ë·Î pre qc.
# ¿ì¸® Äù½ºÆ® ¾ð¾î¿¡´Â Áö¿ª º¯¼ö¸¸ÀÌ ÀÖ°í,
# state³ª, ½ÉÁö¾î when, functionÀ» ¾Æ¿ì¸£´Â Àü¿ª º¯¼ö¸¦ »ç¿ëÇÒ ¼ö ¾ø´Ù.
# Àü¿ª 'º¯¼ö'ÀÇ »ç¿ëÀº ¾ð¾îÀÇ ±¸Á¶»ó »ç¿ëÀÌ ºÒ°¡Çϰí, º° Àǹ̰¡ ¾ø´Ù.
# ÇÏÁö¸¸ Àü¿ª '»ó¼ö'ÀÇ »ç¿ëÀº Äù½ºÆ® view »óÀ¸·Î ²À ÇÊ¿äÇϱ⠶§¹®¿¡,
# fuction setting () °ú °°Àº ÇÔ¼ö¸¦ ÀÌ¿ëÇÏ¿©, 
# ¸Å¹ø Å×À̺íÀ» »ý¼ºÇÏ¿© Àü¿ª »ó¼ö¸¦ Èä³»³»¾î »ç¿ëÇÏ¿´´Ù.
# ÀÌ´Â ¸Å¿ì ºñÈ¿À²ÀûÀ̹ǷÎ,
# cÀÇ preprocesser¿Í °°ÀÌ pre qc¸¦ ¸¸µé¾î Àü¿ª »ó¼ö¸¦ »ç¿ëÇÒ ¼ö ÀÖµµ·Ï ÇÏ¿´´Ù.
# Äù½ºÆ®¸¦ qc·Î ÄÄÆÄÀÏ Çϱâ Àü¿¡ pre_qc.py¸¦ Åë°úÇϸé,
# pre_qc.py´Â define ±¸¹®À» ó¸®Çϰí, ±× °á°ú¸¦
# pre_qc/filename¿¡ ÀúÀåÇÑ´Ù.

import sys

TOKEN_LIST = [
	"-", "+", "*", "/",
	"<", ">", "!", "=", "~",
	"[", "]",
	"{", "}",
	"(", ")",
	"\t", "\n", "\r",
	" ", ",", ".",
]

def split_by_quat(buf):
	p = False
	l = list(buf)
	l.reverse()
	s = ""
	res = []
	while l:
		c = l.pop()
		if c == '"':
			if p == True:
				s += c
				res += [s]
				s = ""
			else:
				if len(s) != 0:
					res += [s]
				s = '"'
			p = not p
		elif c == "\\" and l[0] == '"':
			s += c
			s += l.pop()
		else:
			s += c

	if len(s) != 0:
		res += [s]

	return res

def AddSepMiddleOfElement(l, sep):
	l.reverse()
	new_list = [l.pop()]
	while l:
		new_list.append(sep)
		new_list.append(l.pop())
	return new_list

def my_split_with_seps(s, seps):
	res = [s]
	for sep in seps:
		new_res = []
		for r in res:
			sp = r.split(sep)
			sp = AddSepMiddleOfElement(sp, sep)
			new_res += sp
		res = new_res
	new_res = []
	for r in res:
		if r != "":
			new_res.append(r)
	return new_res

def my_split(s, seps):
	res = []
	curr_token = ""
	is_quoted = False

	for c in s:
		if c == '"':
			if is_quoted:
				curr_token += c
				res.append(curr_token)
				curr_token = ""
			else:
				if curr_token != "":
					res.append(curr_token)
				curr_token = '"'
			is_quoted = not is_quoted
		elif c in seps and not is_quoted:
			if curr_token != "":
				res.append(curr_token)
			curr_token = ""
		else:
			curr_token += c

	if curr_token != "":
		res.append(curr_token)

	return res

def MultiIndex(list, key):
	l = []
	i = 0
	for s in list:
		if s == key:
			l.append(i)
		i = i + 1
	return l

def Replace(lines, parameter_table, keys):
	r = []
	for string in lines:
		l = split_by_quat(string)
		for s in l:
			if s[0] == '"':
				r += [s]
			else:
				tokens = my_split_with_seps(s, TOKEN_LIST)
				for key in keys:
					try:
						indices = MultiIndex(tokens, key)
						for i in indices:
							if len(parameter_table[key]) > 1:
								if tokens[i + 1] == "[" and tokens[i + 3] == "]":
									tokens[i] = parameter_table[key][int(tokens[i + 2]) - 1]
									tokens[i + 1:i + 4] = ["", "", ""] # [x]
								else:
									tokens[i] = "{ " + ", ".join(str(x) for x in parameter_table[key]) + " }"
							else:
								tokens[i] = parameter_table[key][0]
					except:
						pass
				r += tokens
	return r

def MakeParameterTable(lines, parameter_table, keys):
	group_names = []
	group_values = []

	start = 0
	idx = 0
	for line in lines:
		idx += 1
		line = line.strip("\n")
		if (-1 != line.find("--")):
			line = line[0:line.find("--")]

		tokens = my_split(line, TOKEN_LIST)
		if len(tokens) == 0:
			continue

		if tokens[0] == "quest":
			start = idx
			break

		if tokens[0] == "define":
			if tokens[1] == "group":
				group_value = []
				for value in tokens[3:]:
					if parameter_table.get(value, 0) != 0:
						value = parameter_table[value]
					group_value.append(value)

				parameter_table[tokens[2]] = group_value
				keys.append(tokens[2])

			elif len(tokens) > 5:
				print("%d %s" % (idx, "Invalid syntax"))
				print("define <name> = <value>")
				print("define group <name> = [<val1>, <val2>, ...]")

			else:
				value = tokens[2]
				if parameter_table.get(value, 0) != 0:
					value = parameter_table[value]

				parameter_table[tokens[1]] = [value]
				keys.append(tokens[1])

	parameter_table = dict(zip(group_names, group_values))
	return start

def run(path, file_name):
	parameter_table = dict()
	keys = []

	path = path.strip("\n")
	if path == "":
		return

	if sys.version_info >= (3,):
		try:
			with open(path, "r", encoding="euc_kr") as f:
				lines = f.readlines()
		except UnicodeDecodeError as e:
			print("Encoding error in file %s : %s" % (file_name, e))
			return False
		except Exception as e:
			print("Unexpected error in file %s : %s" % (file_name, e))
			return False
	else:
		lines = open(path).readlines()

	start = MakeParameterTable(lines, parameter_table, keys)
	if len(keys) == 0:
		return False

	lines = lines[start - 1:]
	r = Replace(lines, parameter_table, keys)
	if sys.version_info >= (3,):
		try:
			with open("pre_qc/" + file_name, "w", encoding="euc_kr") as f:
				for s in r:
					f.write(s)
		except UnicodeDecodeError as e:
			print("Encoding error in file %s : %s" % (file_name, e))
			return False
		except Exception as e:
			print("Unexpected error in file %s : %s" % (file_name, e))
			return False
	else:
		f = open("pre_qc/" + file_name, "w")
		for s in r:
			f.write(s)

	return True

if __name__ == "__main__":
	if len(sys.argv) < 3:
		print("Usage: python pre_qc.py <input_file> <output_file>")
	else:
		run(sys.argv[1], sys.argv[2])

 

 

make.py

Spoiler

If you're using the original make.py script, search and replace the following

...

for line in open('locale_list'):
	line = line.strip()
	if not line:
		continue

	r = pre_qc.run(line, line)
	if r == True:
		filename = 'pre_qc/' + line
	else:
		filename = line

...

 

 

Input / Output Examples

Input

This is also an example of how you would use these global constants.

Spoiler
define NPC_VNUM 20011
define NPC_CHAT "The World of Metin2"
define group EMPIRE_NAMES ["Shinsoo", "Chunjo", "Jinno"]
define group EMPIRE_NAMES_AND_COLORS ["Shinsoo (red)", "Chunjo (yellow)", "Jinno (blue)"]

quest pre_qc_test begin
	state start begin
		when NPC_VNUM.chat.NPC_CHAT with pc.is_gm() begin
			say_title(NPC_CHAT)
			say(string.format("The World is divided into 3 Empires: %s, %s, %s", EMPIRE_NAMES_AND_COLORS[1], EMPIRE_NAMES_AND_COLORS[2], EMPIRE_NAMES_AND_COLORS[3]))

			local empire_name_table = EMPIRE_NAMES
			table.insert(empire_name_table, "Cancel")

			local idx = select_table(empire_name_table)
			if idx == 1 then
				say_title(empire_name_table[idx])
				say(string.format("The %s Empire is located in the southern", empire_name_table[idx]))
				say("part of the continent. Its inhabitants are")
				say("predominantly traders.")
			elseif idx == 2 then
				say_title(empire_name_table[idx])
				say(string.format("The %s Empire is situated in the western", empire_name_table[idx]))
				say("part of the continent. It is a theocratic")
				say("empire ruled by spiritual leaders.")
			elseif idx == 3 then
				say_title(empire_name_table[idx])
				say(string.format("The %s Empire is located in the eastern", empire_name_table[idx]))
				say("part of the continent. This empire is")
				say("based on its military might.")
			end
		end
	end
end

 

Output

This is the script file converted with the values of the global constants.
Note that this will be the file that will be compiled.

Spoiler
quest pre_qc_test begin
	state start begin
		when 20011.chat."The World of Metin2" with pc.is_gm() begin
			say_title("The World of Metin2")
			say(string.format("The World is divided into 3 Empires: %s, %s, %s", "Shinsoo (red)", "Chunjo (yellow)", "Jinno (blue)"))

			local empire_name_table = { "Shinsoo", "Chunjo", "Jinno" }
			table.insert(empire_name_table, "Cancel")

			local idx = select_table(empire_name_table)
			if idx == 1 then
				say_title(empire_name_table[idx])
				say(string.format("The %s Empire is located in the southern", empire_name_table[idx]))
				say("part of the continent. Its inhabitants are")
				say("predominantly traders.")
			elseif idx == 2 then
				say_title(empire_name_table[idx])
				say(string.format("The %s Empire is situated in the western", empire_name_table[idx]))
				say("part of the continent. It is a theocratic")
				say("empire ruled by spiritual leaders.")
			elseif idx == 3 then
				say_title(empire_name_table[idx])
				say(string.format("The %s Empire is located in the eastern", empire_name_table[idx]))
				say("part of the continent. This empire is")
				say("based on its military might.")
			end
		end
	end
end

 

 

Edited by Owsap
Updated pre_qc.py
  • Metin2 Dev 19
  • Love 4
  • Love 15
Link to comment
https://metin2.dev/topic/30470-fixed-quest-pre-compile-script-pre_qcpy/
Share on other sites

  • Bot

Nice work ?

Here is a token list I use: ["\t", "\r", "\n", ",", " ", "=", "[", "]", "+", "-", "<", ">", "~", "!", ".", "(", ")", "*", "{", "}", "/"]

So you can use: (and more)
local drops = {DROP1, DROP2, DROP3}
local count = COUNT1+COUNT2

  • Love 1

english_banner.gif

  • Honorable Member

With this feature, you can easily check a list of values and iterate them within triggers without needing to create global vars or functions. Real handy.

For example, let's say you have an item that you want to drop from specific dungeons' monsters, here's a one-liner:

define DROP_CHANCE 2
define ITEM_VNUM 30271
define group DROP_DUNGEONS_INDEXES [1, 21, 41]

..
    ..
        when kill with not npc.is_pc() and pc.in_dungeon() and table_is_in(DROP_DUNGEONS_INDEXES, math.floor(pc.get_map_index() / 10000)) and math.random(100) <= DROP_CHANCE begin
            game.drop_item_with_ownership(ITEM_VNUM, 1);
        end -- when
    ..
..

 

Edited by Syreldar
  • Metin2 Dev 1
  • Think 1
  • Good 1
  • Love 1

 

"Nothing's free in this life.

Ignorant people have an obligation to make up for their ignorance by paying those who help them.

Either you got the brains or cash, if you lack both you're useless."

Syreldar

Don't use any images from : imgur, turkmmop, freakgamers, inforge, hizliresim... Or your content will be deleted without notice...
Use : https://metin2.download/media/add/

Please use https://metin2.download/ when uploading files smaller than 100MB, otherwise the approval will take longer due to manual upload.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
×
×
  • 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.