pyscreeps-arena 0.3.0__py3-none-any.whl → 0.3a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,787 @@
1
+ import os.path
2
+
3
+ from pyscreeps_arena.core import *
4
+ from pyscreeps_arena.localization import *
5
+
6
+ import re
7
+ import shutil
8
+ import chardet
9
+ import subprocess
10
+ import pyperclip
11
+ from colorama import Fore
12
+
13
+ WAIT = Fore.YELLOW + ">>>" + Fore.RESET
14
+ GREEN = Fore.GREEN + "{}" + Fore.RESET
15
+ version_info = sys.version_info
16
+ python_version_cn = f"Python版本: {version_info.major}.{version_info.minor}.{version_info.micro}"
17
+ python_version_en = f"Compiler py-version: {version_info.major}.{version_info.minor}.{version_info.micro}"
18
+
19
+ # def InsertPragmaBefore(content:str) -> str:
20
+ # """
21
+ # 在content的开头插入__pragma__('noalias', 'undefined')等内容 |
22
+ # Insert __pragma__('noalias', 'undefined') at the beginning of content
23
+ # :param content: str
24
+ # :return: str
25
+ # """
26
+ # return PYFILE_PRAGMA_INSERTS + "\n" + content
27
+ class Compiler_Const:
28
+ PROTO_DEFINES_DIRS = ["builtin", "library"]
29
+ FILE_STRONG_REPLACE = {
30
+ "std": {
31
+ "==": "===",
32
+ "!=": "!==",
33
+ }
34
+ }
35
+ PYFILE_IGNORE_CHECK_FNAMES = ['game/const.py', 'game/proto.py', 'game/utils.py']
36
+
37
+ PYFILE_PRAGMA_INSERTS = """
38
+ # __pragma__('noalias', 'undefined')
39
+ # __pragma__('noalias', 'Infinity')
40
+ # __pragma__('noalias', 'clear')
41
+ # __pragma__('noalias', 'get')
42
+ """
43
+
44
+ TOTAL_INSERT_AT_HEAD = """
45
+ import {arenaInfo} from "game";
46
+ """
47
+
48
+ TOTAL_INSERT_BEFORE_MAIN = """
49
+ """
50
+
51
+ TOTAL_SIMPLE_REPLACE_WITH = {
52
+ }
53
+
54
+ PYFILE_WORD_WARNING_CHECK = {
55
+ r"\.\s*get\s*\(": LOC_PYFILE_WORD_WARNING_CHECK_GET,
56
+ r"import\s+math\s*": LOC_PYFILE_WORD_WARNING_CHECK_MATH,
57
+ r"\.\s*clear\s*\(": LOC_PYFILE_WORD_WARNING_CHECK_CLEAR,
58
+ r"\[\s*-\s*1\s*\]": LOC_PYFILE_WORD_WARNING_INDEX_MINUS_ONE,
59
+ }
60
+
61
+ PYFILE_EXIST_WARNING_CHECK = {
62
+ r"__pragma__\s*\(\s*['\"]noalias['\"]\s*,\s*['\"]undefined['\"]\s*\)": "Strongly suggest to add '__pragma__('noalias', 'undefined')'.",
63
+ r"__pragma__\s*\(\s*['\"]noalias['\"]\s*,\s*['\"]Infinity['\"]\s*\)": "Strongly suggest to add '__pragma__('noalias', 'Infinity')'.",
64
+ r"__pragma__\s*\(\s*['\"]noalias['\"]\s*,\s*['\"]clear['\"]\s*\)": "Strongly suggest to add '__pragma__('noalias', 'clear')'.",
65
+ r"__pragma__\s*\(\s*['\"]noalias['\"]\s*,\s*['\"]get['\"]\s*\)": "Strongly suggest to add '__pragma__('noalias', 'get')'.",
66
+ }
67
+
68
+ JS_VM = "org.transcrypt.__runtime__.js"
69
+
70
+ GAME_UTILS = './builtin.utils.js'
71
+ GAME_PROTO = './builtin.proto.js'
72
+ GAME_CONST = './builtin.const.js'
73
+ BUILTIN_TRANS = ["engine.js"]
74
+ OTHER_IGNORE_WITH = "./builtin"
75
+
76
+ SYSTEM_MODULES_IGNORE = {
77
+ GAME_UTILS: ['CircleVisualStyle', 'Color', 'CostMatrix', 'CreateConstructionSiteResult', 'Direction', 'DoesZapCodeSpaceFlag',
78
+ 'FindPathOptions', 'Goal', 'HeapInfo',
79
+ 'LineStyle', 'LineVisualStyle', 'PolyVisualStyle', 'RectVisualStyle', 'SearchPathOptions', 'SearchPathResult', 'Terrain',
80
+ 'TextAlign', 'TextVisualStyle',
81
+ 'Visual', 'getAt', 'searchPath'],
82
+ GAME_PROTO: ['BodyPartType', 'CreepAttackResult', 'CreepBuildResult', 'CreepDropResult', 'CreepHarvestResult', 'CreepHealResult',
83
+ 'CreepMoveResult', 'CreepPickupResult',
84
+ 'CreepPullResult', 'CreepRangedAttackResult', 'CreepRangedHealResult', 'CreepRangedMassAttackResult',
85
+ 'CreepTransferResult', 'CreepWithdrawResult',
86
+ 'ResourceType', 'SpawnCreepResult', 'Store', 'TowerAttackResult', 'TowerHealResult', 'Spawning', 'ScoreController',
87
+ 'Flag', 'Position'],
88
+ GAME_CONST: ['RESOURCE_SCORE'],
89
+ }
90
+ SYSTEM_MODULES_TRANSNAME = { # 对应的在js中的名字 | Corresponding name in js
91
+ GAME_UTILS: "game/utils",
92
+ GAME_PROTO: "game/prototypes",
93
+ GAME_CONST: "game/constants",
94
+ }
95
+ GENERATE_IGNORE_PYFILES = ['config.py'] # Won't be compiled into the final js code, only for defines.
96
+
97
+ JS_IMPORT_PAT = re.compile(r'from\s+[\'\"]([^\']+)[\'\"]')
98
+ INSERT_PAT = re.compile(r'#\s*insert\s+([^\n]*)') # 因为被判定的string为单line,所以不需要考虑多行的情况
99
+
100
+ TRANSCRYPT_ERROR_REPLACE = {
101
+ # 由于transcrypt的问题,导致编译后的js代码中存在一些错误,需要进行替换
102
+ r"new\s+set\s*\(": r"set(",
103
+ }
104
+
105
+ class Compiler_Utils(Compiler_Const):
106
+ last_output = False # 一个小标志位,我只想输出一次此类告警信息
107
+
108
+ @staticmethod
109
+ def auto_read(fpath: str) -> str:
110
+ """
111
+ 读取文件内容,自动应用编码
112
+ :param fpath: str 文件路径
113
+ """
114
+ if not os.path.exists(fpath):
115
+ if not Compiler_Utils.last_output:
116
+ Compiler_Utils.last_output = True
117
+ print()
118
+ core.warn('Compiler_Utils.auto_read', core.lformat(LOC_FILE_NOT_EXISTS, [fpath]), end='', head='\n', ln=config.language)
119
+ return ""
120
+
121
+ try:
122
+ with open(fpath, 'r', encoding='utf-8') as f:
123
+ return f.read()
124
+ except UnicodeDecodeError:
125
+ try:
126
+ with open(fpath, 'r', encoding='gbk') as f:
127
+ return f.read()
128
+ except UnicodeDecodeError:
129
+ # 如果使用检测到的编码读取失败,尝试使用chardet检测编码
130
+ with open(fpath, 'rb') as f: # 以二进制模式打开文件
131
+ raw_data = f.read() # 读取文件的原始数据
132
+ result = chardet.detect(raw_data) # 使用chardet检测编码
133
+ encoding = result['encoding'] # 获取检测到的编码
134
+ with open(fpath, 'r', encoding=encoding) as f: # 使用检测到的编码打开文件
135
+ return f.read()
136
+
137
+
138
+ def copy_to(self) -> list:
139
+ """
140
+ 复制src到build目录 | copy all files in src to build
141
+ * 注意到src下的文件应当全部为py文件 | all files in src should be py files
142
+ """
143
+ # copy to build dir
144
+ # print(Fore.YELLOW + '>>> ' + Fore.RESET + ' copying to build dir: %s ...' % self.build_dir, end='')
145
+ # LOC_COPYING_TO_BUILD_DIR
146
+
147
+ core.lprint(WAIT, core.lformat(LOC_COPYING_TO_BUILD_DIR, [self.build_dir]), end="", ln=config.language)
148
+
149
+ if os.path.exists(self.build_dir):
150
+ shutil.rmtree(self.build_dir)
151
+ shutil.copytree(self.src_dir, self.build_dir)
152
+ srcs = [] # src下所有python文件的路径 | paths of all python files under src
153
+ for root, dirs, files in os.walk(self.build_dir):
154
+ for file in files:
155
+ if file.endswith('.py'):
156
+ srcs.append(os.path.join(root, file))
157
+ # add libs
158
+ for lib in self.PROTO_DEFINES_DIRS:
159
+ shutil.copytree(lib, os.path.join(self.build_dir, lib))
160
+
161
+ # overwrite last to [Done]
162
+ # print(Fore.GREEN + '\r[1/6][Done]' + Fore.RESET + ' copying to build dir: %s' % self.build_dir)
163
+
164
+ core.lprint(GREEN.format('[1/6]'), LOC_DONE, " ", core.lformat(LOC_COPYING_TO_BUILD_DIR_FINISH, [self.build_dir]), sep="", head="\r", ln=config.language)
165
+ return srcs
166
+
167
+ @staticmethod
168
+ def potential_check(fpath:str, fname:str) -> bool:
169
+ """
170
+ 检查某个py文件内是否有潜在问题 | check if there are potential problems in a py file
171
+
172
+ 如果有的话,输出[Warn][{file_name}/{line_io}]{detail} | if there are, output [Warn][{file_name}/{line_io}]{detail}
173
+
174
+ Returns:
175
+ bool: 是否有警告
176
+ """
177
+ # 文件路径检查
178
+ if fpath.endswith('__init__.py') and fpath.find("builtin") == -1:
179
+ core.error("potential_check", LOC_NOT_SUPPORT_PYFILE_INIT, ln=config.language, ecode=-1, head='\n')
180
+ if fname in Compiler.PYFILE_IGNORE_CHECK_FNAMES:
181
+ return False
182
+
183
+ # # 文件内容检查
184
+ content = Compiler.auto_read(fpath)
185
+ warn_flag = False
186
+ # # 内容关键字检查
187
+ for pat, detail in Compiler.PYFILE_WORD_WARNING_CHECK.items():
188
+ for i, line in enumerate(content.split('\n')):
189
+ m = re.search(pat, line)
190
+ if m:
191
+ # 检查m前面同一行内是否有#,如果有则忽略
192
+ comment = re.search(r'#', line[:m.start()])
193
+
194
+ # 检查m后面同一行内是否有#\s*ignore;,如果有则忽略
195
+ ignore = re.search(r'#\s*>\s*ignore', line[m.end():])
196
+
197
+ if not comment and not ignore:
198
+ warn_flag = True
199
+ core.warn('Compiler.potential_check', f'[{os.path.basename(os.path.dirname(fpath))}/{fname} line:{i + 1}]:', detail, end='', head='\n', ln=config.language)
200
+ return warn_flag
201
+
202
+ @staticmethod
203
+ def preprocess_if_block(source_code: str, variables: dict[str, object]) -> str:
204
+ """
205
+ 预处理if块,将 # > if, # > elif, # > else, # > endif 替换为实际的程序内容 |
206
+ pre-process if blocks by replacing # > if, # > elif, # > else, # > endif with actual code.
207
+ """
208
+ lines = source_code.split('\n') # 按行分割源代码 | split source code into lines
209
+ stack = [] # 初始化一个栈,用于跟踪if条件 | initialize a stack to track if conditions
210
+ result = [] # 初始化一个列表,用于存储处理后的代码行 | initialize a list to store processed code lines
211
+
212
+ for i, line in enumerate(lines): # 遍历源代码的每一行 | iterate over each line of source code
213
+ striped = line.strip() # 去掉行首尾的空格和换行符 | strip leading and trailing whitespace
214
+ # 使用正则表达式匹配不同的条件语句 | use regex to match different conditional statements
215
+ if_match = re.match(r'#\s*>\s*if\s+([^:.]*)$', striped) # 匹配 '# > if' 语句 | match '# > if' statement
216
+ elif_match = re.match(r'#\s*>\s*elif\s+([^:.]*)$', striped) # 匹配 '# > elif' 语句 | match '# > elif' statement
217
+ else_match = re.match(r'#\s*>\s*else$', striped) # 匹配 '# > else' 语句 | match '# > else' statement
218
+ endif_match = re.match(r'#\s*>\s*endif$', striped) # 匹配 '# > endif' 语句 | match '# > endif' statement
219
+
220
+ if if_match: # 如果当前行是 '# > if' 语句 | if it's a '# > if' statement
221
+ condition = if_match.group(1) # 提取条件表达式 | extract the condition expression
222
+ stack.append(eval(condition, variables)) # 评估条件表达式并将其结果压入栈中 | evaluate condition and push result onto stack
223
+ elif elif_match and stack: # 如果当前行是 '# > elif' 语句,并且栈不为空 | if it's a '# > elif' and stack isn't empty
224
+ condition = elif_match.group(1) # 提取条件表达式 | extract the condition expression
225
+ stack[-1] = eval(condition, variables) # 评估条件表达式并更新栈顶 | evaluate condition and update the top of the stack
226
+ elif else_match and stack: # 如果当前行是 '# > else' 语句,并且栈不为空 | if it's a '# > else' and stack isn't empty
227
+ stack[-1] = not stack[-1] # 将栈顶元素取反 | negate the top of the stack
228
+ elif endif_match: # 如果当前行是 '# > endif' 语句 | if it's a '# > endif' statement
229
+ stack.pop() # 弹出栈顶元素 | pop the top of the stack
230
+ else: # 如果当前行不是条件语句 | if it's not a conditional statement
231
+ if not stack or all(stack): # 如果栈为空,或者栈中所有元素均为真 | if stack is empty or all elements are True
232
+ result.append(line) # 将当前行加入结果列表中 | add the current line to the result
233
+
234
+ return '\n'.join(result) # 将处理后的所有代码行连接成一个字符串,并返回最终结果 | join all processed lines into a string and return
235
+
236
+
237
+ class CompilerBase(Compiler_Utils):
238
+
239
+ def __init__(self, src_dir, build_dir):
240
+ # check
241
+ if not os.path.exists(src_dir):
242
+ core.error('Compiler.__init__', core.lformat(LOC_FILE_NOT_EXISTS, ['src', src_dir]), head='\n', ln=config.language)
243
+
244
+ src_dir = os.path.abspath(src_dir)
245
+ build_dir = os.path.abspath(build_dir)
246
+ base_dir = os.path.dirname(src_dir)
247
+ lib_dir = os.path.join(base_dir, 'library')
248
+ built_dir = os.path.join(base_dir, 'builtin')
249
+
250
+ # 在builtin文件下搜索需要跳过的文件,计入到PYFILE_IGNORE_CHECK_FNAMES中
251
+ for fname in os.listdir(os.path.join(base_dir, "builtin")):
252
+ if fname.endswith('.py') and fname not in ['const.py', 'proto.py', 'utils.py']:
253
+ self.PYFILE_IGNORE_CHECK_FNAMES.append(f'builtin/{fname}')
254
+
255
+ self.src_dir = os.path.abspath(src_dir)
256
+ self.lib_dir = os.path.abspath(lib_dir)
257
+ self.build_dir = os.path.abspath(build_dir)
258
+ self.built_dir = os.path.abspath(built_dir)
259
+ self.target_dir = os.path.join(self.build_dir, '__target__')
260
+ self.build_name = os.path.basename(self.build_dir)
261
+
262
+
263
+ @property
264
+ def builtin_py(self) -> str:
265
+ """
266
+ 返回builtin目录下的__init__.py的路径 | return the path of __init__.py in builtin
267
+ """
268
+ return os.path.join(self.built_dir, '__init__.py')
269
+
270
+ @property
271
+ def target_py(self) -> str:
272
+ """
273
+ 返回build下的main.py的路径 | return the path of main.py in build
274
+ """
275
+ return os.path.join(self.build_dir, 'main.py')
276
+
277
+ @property
278
+ def target_js(self):
279
+ """
280
+ 返回build下的main.js的路径 | return the path of main.js in build
281
+ """
282
+ return os.path.join(self.target_dir, 'main.js')
283
+
284
+
285
+
286
+ class Compiler(CompilerBase):
287
+ def pre_compile(self):
288
+ """
289
+ 预编译 | Precompile
290
+ """
291
+ src_paths = self.copy_to()
292
+
293
+ core.lprint(WAIT, LOC_PREPROCESSING, end="", ln=config.language)
294
+ py_fpath, py_names, warn_flag = [], [], False
295
+ for root, dirs, files in os.walk(self.build_dir):
296
+ for file in files:
297
+ if file.endswith('.py'):
298
+ fpath: str = str(os.path.join(root, file))
299
+
300
+ # 将PYFILE_PRAGMA_INSERTS.replace("\t", "").replace(" ", "")插入到文件开头
301
+ content = self.auto_read(fpath)
302
+ content = self.PYFILE_PRAGMA_INSERTS.replace("\t", "").replace(" ", "") + content
303
+ # 如果是main.py,则将self.built_dir下的__init__.py文件内容插入到main.py文件开头
304
+ if fpath == self.target_py:
305
+ content = self.auto_read(self.builtin_py) + content
306
+ with open(fpath, 'w', encoding='utf-8') as f: # 注意,这里修改的是build目录下的文件,不是源文件 | Note that the file under the build directory is modified here, not the source file
307
+ f.write(content)
308
+
309
+ # 得到src目录后面的内容
310
+ rel_name = os.path.relpath(fpath, self.build_dir).replace('\\', '/')
311
+ py_names.append(rel_name.replace('/', '.'))
312
+ py_fpath.append(fpath)
313
+ warn_flag |= self.potential_check(fpath, rel_name)
314
+ if warn_flag:
315
+ print() # 换行
316
+
317
+ _usubs_ = [] # update_subclass
318
+ _pre_import_, _pre_imp_detail_ = [], {} # > import
319
+ _pre_sort_ = {} # > sort
320
+ _pre_define_ = {} # > define
321
+ _js_replace_, _insert_id_ = {}, 0 # > insert
322
+
323
+
324
+ # ---------------------------------- UPDATABLE ---------------------------------- #
325
+ # 获取在src目录内定义的所有继承了Updatable或CreepLogic的类 |
326
+ # get all classes that inherit from Updatable or CreepLogic defined in the src directory
327
+ for i, fpath in enumerate(py_fpath):
328
+ if fpath in src_paths:
329
+ content = self.auto_read(fpath)
330
+ for line in content.split('\n'):
331
+ m = re.search(r'class\s+([^\s(]+)\s*\((Updatable|CreepLogic)', line)
332
+ if m:
333
+ _usubs_.append(m.group(1))
334
+
335
+ # change REPALCE: '/// $UPDATABLE.UPDATES$ ///'
336
+ _rp_key = "/// $UPDATABLE.UPDATES$ ///"
337
+ _rp_txt = ""
338
+ for usub in _usubs_:
339
+ _rp_txt += "try{" + f"if ({usub} && !Updatable.UPDATES.includes({usub}))Updatable.UPDATES.append({usub});" + "}catch(e){ if (!(e instanceof ReferenceError)) throw e; }\n"
340
+ self.TOTAL_SIMPLE_REPLACE_WITH[_rp_key] = _rp_txt
341
+
342
+
343
+ # ----------------------------------- IMPORT ----------------------------------- #
344
+ # 获取所有.py文件中的所有import的内容,并记录下来fname:[imp1, imp2, ...]
345
+ # | get all import in .py files, and record them fname:[imp1, imp2, ...]
346
+ for i, fpath in enumerate(py_fpath):
347
+ fname = py_names[i][:-3] + '.js'
348
+ content = self.auto_read(fpath)
349
+ for i, line in enumerate(content.split('\n')):
350
+ m = re.search(r'#\s*>\s*import\s+([^\n]+)', line)
351
+ if m:
352
+ _pre_import_.append('./' + re.sub(r'\s', '', m.group(1)) + '.js')
353
+ # 记录文件/lineio信息
354
+ _pre_imp_detail_[_pre_import_[-1]] = f'{fname}/line:{i + 1}'
355
+
356
+ # ----------------------------------- REMOVE ----------------------------------- #
357
+ # 移除所有# > remove所在行的内容
358
+ # | remove all # > remove in .py files
359
+ for fpath in py_fpath:
360
+ content = self.auto_read(fpath)
361
+ new_content = ""
362
+ for line in content.split('\n'):
363
+ if not re.search(r'#\s*>\s*remove', line):
364
+ new_content += line + '\n'
365
+
366
+ with open(fpath, 'w', encoding='utf-8') as f:
367
+ f.write(new_content)
368
+
369
+ # ------------------------------------ SORT ------------------------------------ #
370
+ # 获取所有.py文件中的所有# > sort的内容,并记录下来(不存在则默认为65535)
371
+ # | get all # > sort in .py files, and record them (default 65535 if not exists)
372
+ for i, fpath in enumerate(py_fpath):
373
+ fname = py_names[i][:-3] + '.js'
374
+ content = self.auto_read(fpath)
375
+ m = re.search(r'#\s*>\s*sort\s+(\d+)', content)
376
+ if m:
377
+ try:
378
+ sort_num = int(m.group(1))
379
+ except ValueError:
380
+ core.warn('Compiler.pre_compile', core.lformat(LOC_SORT_NUMBER_ERROR, [m.group(1)]), end='', head='\n', ln=config.language)
381
+ sort_num = 65535
382
+ _pre_sort_[fname] = sort_num
383
+ else:
384
+ _pre_sort_[fname] = 65535
385
+
386
+ # ------------------------------------ DEFINE ------------------------------------ #
387
+ # 扫描所有# > define的内容,然后在.py中移除这些行,并记录下来
388
+ # | get all # > define in .py files, and record them
389
+ for fpath in py_fpath:
390
+ content = self.auto_read(fpath)
391
+ new_content = ""
392
+ for line in content.split('\n'):
393
+ # re.compile(r'#\s*define\s+([^\s]+)\s+([^\n]*)')
394
+ m = re.search(r'#\s*>\s*define\s+([^\s]+)\s+([^\n]*)', line)
395
+ if m:
396
+ _pre_define_[m.group(1)] = m.group(2)
397
+ new_content += '\n'
398
+ else:
399
+ new_content += line + '\n'
400
+
401
+ with open(fpath, 'w', encoding='utf-8') as f:
402
+ f.write(new_content)
403
+
404
+ # 按照keys的顺序,先用前面的key对应的内容去依次替换后面的key对应的value中
405
+ # | replace the value of the key with the content of the previous key in order
406
+ _def_keys = list(_pre_define_.keys())
407
+ _keys_len = len(_def_keys)
408
+ for i in range(_keys_len - 1):
409
+ for j in range(i + 1, _keys_len):
410
+ _pre_define_[_def_keys[j]] = _pre_define_[_def_keys[j]].replace(_def_keys[i], _pre_define_[_def_keys[i]])
411
+
412
+ # ------------------------------------ DEFINE:REPLACE ------------------------------------ #
413
+ # 将刚才记录的define替换到.py中(注意优先替换更长的串)(因此先排序)
414
+ # | replace the defined content to .py files (replace the longer string first)
415
+ _def_keys.sort(key=lambda x: len(x), reverse=True)
416
+ for fpath in py_fpath:
417
+ content = self.auto_read(fpath)
418
+
419
+ # std.py PYSCREEPS_ARENA_PYTHON_VERSION replace
420
+ if os.path.basename(fpath).lower() == 'std.py':
421
+ content = content.replace('PYSCREEPS_ARENA_PYTHON_VERSION_EN', f'\"{python_version_en}\"')
422
+ content = content.replace('PYSCREEPS_ARENA_PYTHON_VERSION_CN', f'\"{python_version_cn}\"')
423
+
424
+ for key in _def_keys:
425
+ content = re.sub(r'[^_A-Za-z0-9]' + key, self._kfc_wrapper(_pre_define_[key]), content)
426
+
427
+ with open(fpath, 'w', encoding='utf-8') as f:
428
+ f.write(content)
429
+
430
+ # ------------------------------------ IF BLOCK ------------------------------------ #
431
+ # 预处理if块,将 # > if, # > elif, # > else, # > endif 替换为实际的程序内容
432
+ # | preprocess if block, replace # > if, # > elif, # > else, # > endif to actual code
433
+ for fpath in py_fpath:
434
+ content = self.auto_read(fpath)
435
+
436
+ content = self.preprocess_if_block(content, _pre_define_)
437
+
438
+ with open(fpath, 'w', encoding='utf-8') as f:
439
+ f.write(content)
440
+
441
+ # ------------------------------------ INSERT ------------------------------------ #
442
+ # 扫描所有# > insert的内容,然后在.py中将整行替换为# __pragma__("js", __JS_INSERT_{id})
443
+ # | get all # > insert in .py files, and replace the whole line with # __pragma__("js", __JS_INSERT_{id})
444
+ for fpath in py_fpath:
445
+ content = self.auto_read(fpath)
446
+ new_content = ""
447
+ for line in content.split('\n'):
448
+ # re.compile(r'#\s*insert\s*([^\n]*)')
449
+ # '# > insert if(obj && obj.body) for(var p of obj.body) if (p.type == MOVE) return true;'
450
+ m = re.search(r'#\s*>\s*insert\s+([^\n]*)', line)
451
+ if m:
452
+ _sign_index_ = line.find('#') # 必然存在
453
+ _js_key_ = f"__JS_INSERT_{_insert_id_:08d}"
454
+ _js_replace_[_js_key_] = m.group(1)
455
+
456
+ new_content += line[:_sign_index_] + f'# __pragma__("js", "{_js_key_}")\n'
457
+ _insert_id_ += 1
458
+ else:
459
+ new_content += line + '\n'
460
+
461
+ with open(fpath, 'w', encoding='utf-8') as f:
462
+ f.write(new_content)
463
+
464
+ core.lprint(GREEN.format('[2/6]'), LOC_DONE, " ", LOC_PREPROCESSING_FINISH, sep="", head="\r", ln=config.language)
465
+ return _pre_import_, _pre_imp_detail_, _pre_sort_, _pre_define_, _js_replace_
466
+
467
+ def clear_not_generate_pyfile(self):
468
+ """
469
+ 清空不需要编译的py文件 | clear not generate py file
470
+ :return:
471
+ """
472
+ for root, dirs, files in os.walk(self.build_dir):
473
+ for file in files:
474
+ if file.endswith('.py') and file in self.GENERATE_IGNORE_PYFILES:
475
+ with open(os.path.join(root, file), 'w', encoding='utf-8') as f:
476
+ f.write('')
477
+
478
+ def transcrypt_cmd(self):
479
+ # 执行cmd命令: transcrypt -b -m -n -s -e 6 target | execute cmd: transcrypt -b -m -n -s -e 6 target
480
+ # 并获取cmd得到的输出 | and get the output of the cmd
481
+ cmd = 'transcrypt -b -m -n -s -e 6 %s' % self.target_py
482
+ core.lprint(WAIT, core.lformat(LOC_TRANSCRYPTING, [cmd]), end="", ln=config.language)
483
+ p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
484
+ stdout, stderr = p.communicate()
485
+ if 'Error while compiling' in stdout.decode():
486
+ print('\r' + stdout.decode())
487
+ core.error('Compiler.transcrypt_cmd', LOC_TRANSCRYPTING_ERROR, indent=1, head='\n', ln=config.language)
488
+ core.lprint(GREEN.format('[3/6]'), LOC_DONE, " ", LOC_TRANSCRYPTING_FINISH, sep="", head="\r", ln=config.language)
489
+
490
+ @staticmethod
491
+ def _keep_lbracket(matched) -> str:
492
+ """
493
+ 如果第一个字符是{, 则返回'{',否则返回'' | if the first char is {, return '{', else return ''
494
+ :param matched:
495
+ :return:
496
+ """
497
+ return '{' if matched.group(0)[0] == '{' else ''
498
+
499
+ @staticmethod
500
+ def _keep_first_char(matched:re.Match) -> str:
501
+ """
502
+ 保留第一个字符 | keep the first char
503
+ :param matched: re.match object | re.match对象
504
+ :return:
505
+ """
506
+ return matched.group(0)[0]
507
+
508
+ @staticmethod
509
+ def _kfc_wrapper(replace:str) -> callable:
510
+ """
511
+ 获取一个保留第一个字符的函数 | get a function to keep the first char
512
+ :param replace: str
513
+ :return: function
514
+ """
515
+
516
+ def _kfc(matched) -> str:
517
+ return matched.group(0)[0] + replace
518
+
519
+ return _kfc
520
+
521
+ def analyze_rebuild_main_js(self, defs:dict[str, object]) -> tuple[str, list[str]]:
522
+ """
523
+ 分析main.js中导入的模块名称和先后顺序, 并重新生成main.js | analyze the module names and order imported in main.js, and rebuild main.js
524
+ * 主要移除非SYSTEM_MODULES_IGNORE中的模块导入语句 | mainly remove the module import statements that are not in SYSTEM_MODULES_IGNORE
525
+ :param defs: dict{define: value} 定义的变量 | defined variables
526
+ :return: imports : str, modules (names: str)
527
+ imports是一段用于放在js主体开头的import语句 | imports is a string of import statements to be placed at the beginning of the js body
528
+ modules是一个list,包含了所有的模块名称 | modules is a list containing all module names
529
+ 其中的内容可能是这样的: ['./game.utils.js', './game.proto.js', './game.const.js', ...]
530
+ """
531
+
532
+ # create undefined
533
+ imports = ""
534
+
535
+ if defs.get('USE_TUTORIAL_FLAG', '0') == '0' and defs.get('USE_ARENA_FLAG', '0') == '0':
536
+ imports += 'var Flag = undefined;\n'
537
+ if defs.get('USE_SCORE_COLLECTOR', '0') == '0':
538
+ imports += 'var ScoreController = undefined;\nvar RESOURCE_SCORE = undefined;\n'
539
+ imports += '\n'
540
+
541
+ core.lprint(WAIT, LOC_ANALYZING_AND_REBUILDING_MAIN_JS, end="", ln=config.language)
542
+
543
+ content = self.auto_read(self.target_js)
544
+ modules, new_content = [], ""
545
+ for line in content.split('\n'):
546
+ m = re.search(self.JS_IMPORT_PAT, line)
547
+ if not m:
548
+ new_content += line + '\n'
549
+ continue
550
+ # remove ignore if in SYSTEM_MODULES_IGNORE
551
+ module = m.group(1)
552
+ for ignore in self.SYSTEM_MODULES_IGNORE.get(module, []):
553
+ line = re.sub(r'[\s{]' + ignore + ',?', self._keep_lbracket, line)
554
+
555
+ # do not add in modules if in system_modules_ignores
556
+ if module in self.SYSTEM_MODULES_IGNORE:
557
+ if module in self.SYSTEM_MODULES_TRANSNAME:
558
+ line = line.replace(module, self.SYSTEM_MODULES_TRANSNAME[module]) # 调整为js中的名称
559
+ imports += line + '\n'
560
+ continue
561
+
562
+ _ignore = False
563
+ if module in modules: _ignore = True
564
+ if not _ignore and module.startswith(self.OTHER_IGNORE_WITH): _ignore = True
565
+ for keeps in self.BUILTIN_TRANS:
566
+ if module.endswith(keeps): _ignore = False
567
+ if not _ignore: modules.append(module)
568
+
569
+ # save raw main.js
570
+ with open(self.target_js[:-3] + ".raw.js", 'w', encoding='utf-8') as f:
571
+ f.write(content)
572
+
573
+ # write rebuild main.js
574
+ with open(self.target_js, 'w', encoding='utf-8') as f:
575
+ f.write(new_content)
576
+
577
+ core.lprint(GREEN.format('[4/6]'), LOC_DONE, " ", LOC_ANALYZING_AND_REBUILDING_MAIN_JS_FINISH, sep="", head="\r", ln=config.language)
578
+
579
+ return imports, modules
580
+
581
+ @staticmethod
582
+ def remove_js_import(raw) -> str:
583
+ """
584
+ 移除js中的import行
585
+ :param raw:
586
+ :return:
587
+ """
588
+ return re.sub(r'import[^\n]*\n', '', raw)
589
+
590
+ def generate_total_js(self, usr_modules, t_imps: dict, f_sorts, f_replaces, g_replaces) -> str:
591
+ """
592
+ 生成总的main.js
593
+ 按照如下顺序组合:
594
+ ./org.transcrypt.__runtime__.js
595
+ ./game.const.js # IGNORE
596
+ ./game.proto.js # IGNORE
597
+ ./game.utils.js # IGNORE
598
+ {usr_modules}
599
+ :param usr_modules: list[str] # js vm + 用户自定义模块
600
+ :param t_imps: dict{module_name: detail}
601
+ :param f_sorts: dict{module_name: sort_priority}
602
+ :param f_replaces: dict{module_name: dict{old: new}}
603
+ :param g_replaces: dict{old: new}
604
+ :return: str
605
+ """
606
+ total_js = "" + self.TOTAL_INSERT_AT_HEAD
607
+
608
+ core.lprint(WAIT, LOC_GENERATING_TOTAL_MAIN_JS, end="", ln=config.language)
609
+
610
+ # resort modules
611
+ f_sorts[self.JS_VM] = -1
612
+
613
+ err_flag = False
614
+ for module in usr_modules:
615
+ if module[2:] not in f_sorts:
616
+ print(Fore.RED + '\n[Error] ' + Fore.RESET + f'"{module[2:-3]}.py" is not a user module.')
617
+ imp_detail = t_imps.get(module, None)
618
+ if imp_detail:
619
+ print("\t\t-- May be imported by user in: %s" % imp_detail)
620
+ else:
621
+ print("\t\t-- Please move this file into the 'src' directory.")
622
+ err_flag = True
623
+ if err_flag:
624
+ sys.exit(1)
625
+ for i in range(len(usr_modules)):
626
+ for j in range(i + 1, len(usr_modules)):
627
+ if f_sorts[usr_modules[i][2:]] > f_sorts[usr_modules[j][2:]]:
628
+ usr_modules[i], usr_modules[j] = usr_modules[j], usr_modules[i]
629
+
630
+ # write modules
631
+ for module in usr_modules:
632
+ content = self.auto_read(os.path.join(self.target_dir, module))
633
+ content = self.remove_js_import(content)
634
+ for old, new in f_replaces.get(module, {}).items():
635
+ content = re.sub(old, new, content)
636
+ for old, new in self.TRANSCRYPT_ERROR_REPLACE.items():
637
+ content = re.sub(old, new, content)
638
+ total_js += f"\n// ---------------------------------------- Module:{module} "
639
+ total_js += "----------------------------------------\n\n"
640
+ total_js += content + '\n'
641
+
642
+
643
+ total_js += self.TOTAL_INSERT_BEFORE_MAIN
644
+
645
+ # write main.js
646
+ content = self.auto_read(self.target_js)
647
+ for old, new in self.TRANSCRYPT_ERROR_REPLACE.items():
648
+ content = re.sub(old, new, content)
649
+ total_js += content
650
+
651
+ # global replace
652
+ for old, new in g_replaces.items():
653
+ total_js = re.sub(old, new, total_js)
654
+
655
+ core.lprint(GREEN.format('[5/6]'), LOC_DONE, " ", LOC_GENERATING_TOTAL_MAIN_JS_FINISH, sep="", head="\r", ln=config.language)
656
+
657
+ # REPACE
658
+ for old, new in self.TOTAL_SIMPLE_REPLACE_WITH.items():
659
+ total_js = total_js.replace(old, new)
660
+
661
+ return total_js
662
+
663
+ def __parse_js_file_sort(self, fpath):
664
+ """
665
+ 解析js文件中的sort
666
+ :param fpath:
667
+ :return:
668
+ """
669
+ content = self.auto_read(fpath)
670
+ m = re.search(r'//\s*>\s*sort\s+(\d+)', content)
671
+ if m:
672
+ return int(m.group(1))
673
+ return 65535
674
+
675
+
676
+ def find_add_pure_js_files(self, sorts, modules):
677
+ """
678
+ 找到所有的纯js文件,并添加到modules中
679
+ :param sorts:
680
+ :param modules:
681
+ :return:
682
+ """
683
+ for root, dirs, files in os.walk(self.lib_dir):
684
+ for file in files:
685
+ if file.endswith('.js') and file not in modules:
686
+ fpath = str(os.path.join(root, file))
687
+ fname = file.replace('\\', '/')
688
+ # copy file to target
689
+ shutil.copy(fpath, os.path.join(self.target_dir, fname))
690
+ sorts[fname] = self.__parse_js_file_sort(fpath)
691
+ modules.append("./" + fname)
692
+
693
+ def compile(self, export_to=None, paste=False):
694
+ """
695
+ 编译
696
+ :param export_to: 指定main.mjs的输出路径(/main.mjs)
697
+ :param paste: 是否复制到剪贴板
698
+ :return:
699
+ """
700
+ imps, imp_ts, sorts, defs, reps = self.pre_compile()
701
+ self.clear_not_generate_pyfile() # 清空不需要编译的py文件(请确保在pre_compile之后)
702
+ self.transcrypt_cmd()
703
+ imports, modules = self.analyze_rebuild_main_js(defs)
704
+ self.find_add_pure_js_files(sorts, modules)
705
+ total_js = imports + "\n" + self.generate_total_js(list(set(modules + imps)), imp_ts, sorts, self.FILE_STRONG_REPLACE, reps)
706
+
707
+ core.lprint(WAIT, LOC_EXPORTING_TOTAL_MAIN_JS, end="", ln=config.language)
708
+
709
+ # ensure exported main.mjs path
710
+ build_main_mjs = os.path.join(self.build_dir, 'main.mjs')
711
+
712
+ if not export_to:
713
+ # 尝试读取defs
714
+ mjs_path = defs.get('MAIN_JS_PATH', build_main_mjs)
715
+ else:
716
+ mjs_path = export_to
717
+
718
+ if not mjs_path.endswith('js'):
719
+ mjs_path = os.path.join(mjs_path, 'main.mjs')
720
+
721
+ # write main.mjs
722
+ with open(build_main_mjs, 'w', encoding='utf-8') as f:
723
+ f.write(total_js)
724
+
725
+ # export main.mjs
726
+ dir_path = os.path.dirname(mjs_path)
727
+ if not os.path.exists(dir_path):
728
+ core.error('Compiler.compile', core.lformat(LOC_EXPORT_DIR_PATH_NOT_EXISTS, [dir_path]), head='\n', ln=config.language)
729
+ with open(mjs_path, 'w', encoding='utf-8') as f:
730
+ f.write(total_js)
731
+
732
+ core.lprint(GREEN.format('[6/6]'), LOC_DONE, " ", LOC_EXPORTING_TOTAL_MAIN_JS_FINISH, sep="", head="\r", ln=config.language)
733
+
734
+ if mjs_path != build_main_mjs:
735
+ core.lprint(Fore.LIGHTBLUE_EX + '[Info] ' + Fore.RESET, core.lformat(LOC_USR_EXPORT_INFO, [mjs_path]), ln=config.language)
736
+
737
+ # copy to clipboard
738
+ if paste:
739
+ pyperclip.copy(total_js)
740
+ core.lprint(LOC_DONE, " ", LOC_COPY_TO_CLIPBOARD, ln=config.language)
741
+
742
+ def clean(self):
743
+ """
744
+ 清除build目录下除了main.mjs之外的所有文件和目录
745
+ * 先复制main.mjs到src目录下,然后删除build目录,再将main.mjs剪切回build目录
746
+ :return:
747
+ """
748
+ core.lprint(WAIT, LOC_CLEAN_BUILD_DIR, end="", ln=config.language)
749
+ if not os.path.exists(self.build_dir):
750
+ core.error('Compiler.clean', LOC_BUILD_DIR_NOT_EXISTS, indent=1, head='\n', ln=config.language)
751
+
752
+ if not os.path.exists(os.path.join(self.build_dir, 'main.mjs')):
753
+ core.error('Compiler.clean', LOC_MAIN_MJS_NOT_EXISTS, indent=1, head='\n', ln=config.language)
754
+
755
+ # copy main.mjs to src
756
+ shutil.copy(os.path.join(self.build_dir, 'main.mjs'), os.path.join(self.src_dir, 'main.mjs'))
757
+
758
+ # remove build dir
759
+ shutil.rmtree(self.build_dir)
760
+
761
+ # create build dir
762
+ os.makedirs(self.build_dir)
763
+
764
+ # move main.mjs to build dir
765
+ shutil.move(os.path.join(self.src_dir, 'main.mjs'), os.path.join(self.build_dir, 'main.mjs'))
766
+
767
+ core.lprint(GREEN.format('[Done]'), LOC_CLEAN_BUILD_DIR_FINISH, head="\r", ln=config.language)
768
+
769
+ def clear(self):
770
+ """
771
+ 清除build目录下所有文件和目录
772
+ :return:
773
+ """
774
+ core.lprint(WAIT, LOC_CLEAN_BUILD_DIR, end="", ln=config.language)
775
+ if not os.path.exists(self.build_dir):
776
+ core.lprint(LOC_BUILD_DIR_NOT_EXISTS, ln=config.language)
777
+
778
+ shutil.rmtree(self.build_dir)
779
+ os.makedirs(self.build_dir)
780
+
781
+ core.lprint(GREEN.format('[Done]'), LOC_CLEAN_BUILD_DIR_FINISH, head="\r", ln=config.language)
782
+
783
+
784
+ if __name__ == '__main__':
785
+ compiler = Compiler('src', 'library', 'build')
786
+ compiler.compile()
787
+ compiler.clean()