게임 안의 특정 구성요소를 외부 스크립트로 빼낼 때 약간 골칫거리가 되는 것이 게임 안에서 쓰이는 상수다. 예를 들어 플레이어 인벤토리의 최대 크기라던지, 각종 능력치 최대값 같은 것들 말이다. 스크립트 코드를 작성할 때 이런 상수들을 직접 써주는 방법도 있지만, C/C++ 소스에서 자동으로 뽑아내는 것이 좋다. 에러가 발생할 확률도 줄고, 상수들이 변경되었을 때도 쉽게 처리할 수 있기 때문이다.
이런 작업 자체는 C/C++로 짜기보다는 정규식 처리가 가능한 스크립트 언어를 이용하는 것이 좋다. 완벽한 파서를 만들면 좋겠지만, 딱히 매달릴 필요는 없으며, 원하는 값들만 뽑아낼 수 있을 정도라면, 땜빵이라도 괜찮다고 생각한다.
# -*- coding: euc-kr -*- # @file extract_enums.py # @author excel96 # @brief C++ 소스에서 열거값 데이터를 LUA 소스로 뽑아내기 위한 스크립트 import re import string # 문자열에서 C++ identifier를 뽑아낸다. # "this_is_identifier = 0;" --> "this_is_identifier" def extract_id(text): m = re.search(r"[a-zA-Z_][a-zA-Z0-9_]*", text) if m != None: return m.group(0) return "" # 문자열에서 코멘트 문자열을 뽑아낸다. # 원래 라인 주석은 "//" 뒤쪽의 모든 문자열이지만, # 현재 소스의 특수성(doxygen)을 고려하여 "///<"에 대한 처리도 집어넣었다. # "// this is comment" --> "this is comment" # "///< this is comment" --> "this is comment" def extract_comment(text): m = re.search(r"(//[/<]*)(.*)", text) if m != None: return m.group(2).strip() return "" # 주어진 N개의 파일들에서 열거값을 뽑아내어, # 하나의 출력 파일에다 그 값들을 기록한다. def generate(input_filenames, output_filename): # 출력 파일을 연다. output = file(output_filename, "w") # 입력 파일 목록 모두를 열어서 열거값을 출력 파일에다 기록한다. for filename in input_filenames: input = file(filename, "r") bEnum = 0 # 현재 열거값 내부를 처리 중인가? count = 0 # 열거값 처리 중이라면 현재 열거값의 실제 값은? for line in input.readlines(): # 열거값 처리 중이라면... if bEnum: # 열거값 구문의 끝에 도달했는지 체크한다. if re.search("^};$", line) != None: output.write("\n") bEnum = 0 # 열거값 구문의 끝이 아니라면, 열거값과 그 값을 기록한다. else: id = extract_id(line) if len(id) > 0: text = id + " = " + str(count) # 열거값 뒤쪽에 붙은 주석을 처리한다. comment = extract_comment(line) if len(comment) > 0: text = text + " -- " + comment output.write(text + "\n") count = count + 1 # 열거값 구문이 새로 시작되는지 체크한다. elif re.search(r"(^enum)([\t ]+[a-zA-Z_][a-zA-Z0-9_]*)*", line) != None: bEnum = 1 count = 0 input.close() output.close() # 해당하는 파일의 들여쓰기를 예쁘게(-_-) 만든다. def decorate(filename): f = file(filename, "r") originals = f.readlines() seperator = "=" comment = "//" lhs_max_length = 0 for line in originals: pos = string.find(line, seperator) if pos != -1: lhs = line[:pos].strip() rhs = line[pos+1:].strip() lhs_max_length = max(lhs_max_length, len(lhs)) f.close() f = file(filename, "w") for line in originals: pos = string.find(line, seperator) text = line if pos != -1: lhs = line[:pos].strip() rhs = line[pos+1:].strip() needed_space = lhs_max_length - len(lhs); for i in range(0, needed_space): lhs = lhs + " "; text = lhs + " " + seperator + " " + rhs + "\n" f.write(text) f.close() input_files = [ "D:\\Project\\Binary\\Shared\\_NetLib\\TypesSkill.h", "D:\\Project\\Binary\\Shared\\_NetLib\\TypesCreature.h" ] generate(input_files, "test.lua") decorate("test.lua")
--[[ @file extract_enums.lua @author excel96 @brief C++ 소스에서 열거값 데이터를 LUA 소스로 뽑아내기 위한 스크립트 --]] -- 문자열에서 앞뒷쪽의 빈 공간을 날린다. function strip(text) return string.gsub(text, "^[ \t\r\n]+|[ \t\r\n]+$", "") end -- 문자열에서 C++ identifier를 뽑아낸다. -- "this_is_identifier = 0;" --> "this_is_identifier" function extract_id(text) if text == nil then return "" end local b, e = string.find(text, "[a-zA-Z_][a-zA-Z0-9_]*") if b ~= nil then return strip(string.sub(text, b, e)) end return "" end -- 문자열에서 코멘트 문자열을 뽑아낸다. -- 원래 라인 주석은 "//" 뒤쪽의 모든 문자열이지만, -- 현재 소스의 특수성(doxygen)을 고려하여 "///<"에 대한 처리도 집어넣었다. -- "// this is comment" --> "this is comment" -- "///< this is comment" --> "this is comment" function extract_comment(text) local _, _, m1, m2 = string.find(text, "(//[/<]*)(.*)") if m2 ~= nil then return strip(m2) end return "" end -- 주어진 N개의 파일들에서 열거값을 뽑아내어, -- 하나의 출력 파일에다 그 값들을 기록한다. function generate(input_filenames, output_filename) -- 출력 파일을 연다. local output = assert(io.open(output_filename, "w")) -- 입력 파일 목록 모두를 열어서 열거값을 출력 파일에다 기록한다. for _, filename in pairs(input_filenames) do local bEnum = false -- 현재 열거값 내부를 처리 중인가? local count = 0 -- 열거값 처리 중이라면 현재 열거값의 실제 값은? for line in io.lines(filename) do -- 열거값 처리 중이라면... if bEnum then -- 열거값 구문의 끝에 도달했는지 체크한다. if string.find(line, "};") ~= nil then output:write("\n") bEnum = false -- 열거값 구문의 끝이 아니라면, 열거값과 그 값을 기록한다. else local id = extract_id(line) if string.len(id) > 0 then local text = id .. " = " .. tostring(count) -- 열거값 뒤쪽에 붙은 주석을 처리한다. local comment = extract_comment(line) if string.len(comment) > 0 then text = text .. " -- " .. comment end output:write(text .. "\n") count = count + 1 end end -- 열거값 구문이 새로 시작되는지 체크한다. elseif string.find(line, "^enum") ~= nil then bEnum = true count = 0 end end end output:close() end -- 해당하는 파일의 들여쓰기를 예쁘게(-_-) 만든다. function decorate(filename) local originals = {} local seperator = "=" local comment = "//" local lhs_max_length = 0 for line in io.lines(filename) do local b, e = string.find(line, seperator) if b ~= nil then local lhs = strip(string.sub(line, 1, b-1)) local rhs = strip(string.sub(line, b+1, string.len(line))) lhs_max_length = math.max(lhs_max_length, string.len(lhs)) end table.insert(originals, line) end f = assert(io.open(filename, "w")) for _, line in pairs(originals) do local text = line local b, e = string.find(line, seperator) if b ~= nil then local lhs = strip(string.sub(line, 1, b-1)) local rhs = strip(string.sub(line, b+1, string.len(line))) local needed_space = lhs_max_length - string.len(lhs) for i=1,needed_space do lhs = lhs .. " " end text = lhs .. seperator .. rhs .. "\n" else text = text .. "\n" end f:write(text) end f:close() end input_files = { "D:\\Project\\Binary\\Shared\\_NetLib\\TypesSkill.h", "D:\\Project\\Binary\\Shared\\_NetLib\\TypesCreature.h" } generate(input_files, "test.lua") decorate("test.lua")
# \file extract_enums.py # \brief C++ 소스에서 열거값 데이터를 LUA 소스로 뽑아내기 위한 스크립트 # 문자열에서 C++ identifier를 뽑아낸다. # "this_is_identifier = 0;" --> "this_is_identifier" def extract_id(text) match = text.scan(/[a-zA-Z_][a-zA-Z0-9_]*/) return match.length > 0 ? match[0] : nil end # 문자열에서 C++ identifier를 뽑아낸다. # "this_is_identifier = 0;" --> "this_is_identifier" def extract_id_ex(text) match = text.scan(/([a-zA-Z_][a-zA-Z0-9_]*)([ \t]*=[ \t]*)/) return match.length > 0 ? match[0][0] : nil end # 문자열에서 " = value" 구문을 찾은 다음, "value" 부분을 뽑아낸다. # "a = 32" --> "32" def extract_value(text) match = text.scan(/(=)([ \t\r\n]*)([\"a-zA-Z0-9_.]*)/) return match.length > 0 ? match[0][2] : nil end # 문자열이 숫자로만 이루어졌는지의 여부를 반환한다. def is_digit(text) return text.scan(/^[0-9]+$/).length > 0 end # 문자열에서 코멘트 문자열을 뽑아낸다. # 원래 라인 주석은 "//" 뒤쪽의 모든 문자열이지만, # 현재 소스의 특수성(doxygen)을 고려하여 "///<"에 대한 처리도 집어넣었다. # "// this is comment" --> "this is comment" # "///> this is comment" --> "this is comment" def extract_comment(text) match = text.scan(/(\/\/[\/<]*)(.*)/) return match.length > 0 ? match[0][1].strip : nil end # 해당하는 파일의 들여쓰기를 예쁘게(-_-) 만든다. def decorate(filename) originals = IO.readlines(filename) seperator = "=" comment = "//" lhs_max_length = 0 for line in originals pos = line.index(seperator) if pos != nil lhs = line[0, pos].to_s.strip # nil인 경우가 있어서 to_s 처리 rhs = line[pos+1..-1].to_s.strip # nil인 경우가 있어서 to_s 처리 lhs_max_length = [lhs_max_length, lhs.length].max end end f = File.open(filename, "w") for line in originals text = line pos = line.index(seperator) if pos != nil lhs = line[0, pos].to_s.strip # nil인 경우가 있어서 to_s 처리 rhs = line[pos+1..-1].to_s.strip # nil인 경우가 있어서 to_s 처리 lhs = lhs + (" " * (lhs_max_length - lhs.length)) text = lhs.to_s + " " + seperator + " " + rhs.to_s + "\n" end f.write(text) end f.close end # 주어진 N개의 파일들에서 열거값을 뽑아내어, # 하나의 출력 파일에다 그 값들을 기록한다. def generate(input_filenames, output_filename) # 출력 파일을 연다. output = File.open(output_filename, "w") # 헤더를 적어준다. output.write("--[[\n") output.write("\\file " + output_filename + "\n") output.write("\\author excel96\n") output.write("\\brief 자동으로 생성된 파일입니다.\n") output.write("잘못된 것이 있다면 수동으로 고치지 말고 스크립트를 손봐야 합니다.\n") output.write("--]]\n\n") # 입력 파일 목록 모두를 열어서 열거값을 출력 파일에다 기록한다. for filename in input_filenames bEnum = false # 현재 열거값 내부를 처리 중인가? count = 0 # 열거값 처리 중이라면 현재 열거값의 실제 값은? # 원본 파일에 대한 정보를 기록한다. header = "-- EXTRACTED FROM " + filename output.write("-" * (header.length + 1) + "\n") output.write(header + "\n") output.write("-" * (header.length + 1) + "\n\n") for line in IO.readlines(filename) # 열거값 처리 중이라면... if bEnum # 열거값 구문의 끝에 도달했는지 체크한다. if line.scan(/^\s*\}\s*;/).length > 0 output.write("\n") bEnum = false # 열거값 구문의 끝이 아니라면, 열거값과 그 값을 기록한다. else if id = extract_id(line) text = id # 값 문자열이 있는지 확인한다. if value = extract_value(line) if is_digit(value) text = text + " = " + value count = value.to_i else text = text + " = " + value end else text = text + " = " + count.to_s end # 뒤쪽에 붙은 주석을 처리한다. if comment = extract_comment(line) text = text + " -- " + comment end output.write(text + "\n") count = count + 1 end end # 열거값 구문이 새로 시작되는지 체크한다. elsif line.scan(/(^[ \t]*enum)([\t ]+[a-zA-Z_][a-zA-Z0-9_]*)*/).length > 0 bEnum = 1 count = 0 # 상수 구문인지를 체크한다. elsif line.scan(/(static|)\s*const\s*(static|)\s*(unsigned|)\s*(int|float)\s*[A-Za-z0-9_]+\s*=\s*[\"A-Za-z0-9.]+;/).length > 0 id = extract_id_ex(line) value = extract_value(line) text = id + " = " + value # 뒤쪽에 붙은 주석을 처리한다. if comment = extract_comment(line) text = text + " -- " + comment end output.write(text + "\n\n") end end end output.close # 들여쓰기를 손본다. decorate(output_filename) end files1 = [ "D:\\Project\\XM\\Server\\ZTestAI\\Globals.h", "D:\\Project\\XM\\Server\\ZTestAI\\NPC.h" ] generate(files1, "Constant.lua")