Jump to content

Fixed Quest Pre-Compile Script (pre_qc.py)


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

# -*- 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에 저장한다.

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 = []

	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

	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)
	f = open("pre_qc/" + file_name, "w")
	for s in r:
		f.write(s)

	return True

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

 

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
Better topic clarification and minimal modification of the script with newer improvements.
  • Metin2 Dev 15
  • Love 4
  • Love 9
Link to comment
Share on other sites

  • Active+ Member

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
Link to comment
Share on other sites

  • Premium

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
  • 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

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.