#!/usr/local/bin/python3 # -*- coding: utf-8 -*- # File: conv_lvs.py # Desc: 将用到的关卡文件压缩成程序使用的protobuf格式 # Date: 2017-04-18 import os import sys import json import levelData_pb2 import time def read_json(file_path): with open(file_path, 'r') as file: return json.load(file) def write_json(data, file_path): with open(file_path, 'w+') as file: json.dump(data, file, indent=4) # 将 ../conf/levelInstInfo.csv 里面的每一个实例的模板名加上“tf_”前缀 def add_prefix_to_inst_name(): with open('../conf/levelInstInfo.csv', 'r') as f: hdr_done = False with open('../conf/levelInstInfo_new.csv', 'w+') as f_new: for line in f.readlines(): if line.startswith('#'): f_new.write(line) continue if not hdr_done: f_new.write(line) hdr_done = True continue line = line.split(',') if line[1].startswith('tf_'): f_new.write(line) continue line[1] = 'tf_' + line[1] f_new.write(','.join(line)) # 解析 ../conf/levelInstInfo.csv 文件,得到所有被用到的模板列表 def parse_level_inst_info(groups): temp_list = set() for grp in groups: fn = '../conf/levelInstInfo.csv' if len(grp) != 0: fn = '../conf/levelInstInfo-' + grp + '.csv' with open(fn, 'r') as f: # 跳过第一行 f.readline() for line in f.readlines(): if line.startswith('#'): continue temp_list.add(line.split(',')[1]) return temp_list # 将某个tiled的json文件转换成protobuf格式 def conver_json_to_proto(tDir, tFileName, outDir): global lvFileInfo, lvInfo jsonFile = os.path.join(tDir, tFileName + '.json') with open(jsonFile, 'r') as f:#, encoding='utf-8' jsonData = json.load(f) levelData = levelData_pb2.LevelData() levelData.tileWidth = jsonData['width'] levelData.tileHeight = jsonData['height'] cntUndefined = 0 tilesByPos = {} maxZ = 0 maxX = 0 maxY = 0 # 首先得到stacked的坐标数据 stacked_map = {} for item in jsonData['layers']: name = item['name'] if name == 'Marks': data = item['data'] for i in range(0, len(data)): if data[i] == 0: continue x = (int)(i % levelData.tileWidth) y = (int)(i / (int)(levelData.tileWidth)) stacked_map[(x, y)] = 0 # 处理各层的tile数据,并去掉中间的空层 cntTiles = 0 z = 0 for item in jsonData['layers']: name = item['name'] if not name.startswith('Tile_'): # stacked continue data = item['data'] is_empty = True for i in range(0, len(data)): if data[i] == 0: continue is_empty = False tile = levelData.tiles.add() tile.x = (int)(i % levelData.tileWidth) tile.y = (int)(i / (int)(levelData.tileWidth)) tile.z = z if (tile.x, tile.y) in stacked_map: stacked_map[(x, y)] += 1 tiledata = tile.tileData tiledata.zv = 0 tiledata.weight = 0 tiledata.id = data[i] if tiledata.id == -10: cntUndefined += 1 tiledata.type = 1 tiledata.subtype = 0 # 一些统计信息 tilesByPos[(tile.x, tile.y, tile.z)] = tile if tile.z > maxZ: maxZ = tile.z if tile.x > maxX: maxX = tile.x if tile.y > maxY: maxY = tile.y cntTiles += 1 if not is_empty: z += 1 print('Tile 总数: %d' % (cntTiles,)) print('Tile 总数: %d' % (cntTiles,)) print('Tile 总数: %d' % (cntTiles,)) if cntTiles%3 != 0: print('Error: Tile总数不是3的倍数 3: %d' % (cntTiles,)) exit(1) # 检查每层的tile是否有重叠 pos_conflict = {} for z in range(0, maxZ+1): for x in range(0, maxX+1): for y in range(0, maxY+1): if (x,y,z) in tilesByPos: adjs = [(0,1),(0,-1),(1,0),(-1,0),(1,1),(1,-1),(-1,1),(-1,-1)] for adj in adjs: p = (x+adj[0],y+adj[1],z) if p in tilesByPos: if (x,y) not in pos_conflict: pos_conflict[(x,y,z)] = [] pos_conflict[(x,y,z)].append(p) for p,ps in pos_conflict.items(): print("位置冲突:", p, ps) if len(pos_conflict) > 0: exit(1) # 刷新io缓冲区 sys.stdout.flush() time.sleep(3) # 根据上面的布局信息,计算每个tile的几个信息: # 1. 每个tile的视觉层级(不同于上面z的信息,那是一个布局信息,相同的z可能出在不同的视觉层级) # 从maxZ开始,逐层向下计算;对于每一个位置,如果该位置有tile,则计算该tile的视觉层级 # 对于每一个tile,如果其上方(从该tile的z+1层,一直到maxZ层)有tile,则其视觉层级为上方tile的视觉层级+1 for z in range(0, maxZ+1): z = maxZ - z for x in range(0, maxX+1): for y in range(0, maxY+1): if (x,y,z) in tilesByPos: tile = tilesByPos[(x, y, z)] adjs = [(x,y),(x,y+1),(x,y-1),(x+1,y),(x+1,y+1),(x+1,y-1),(x-1,y),(x-1,y-1),(x-1,y+1)] # 确定视觉层级 zvMax = -1 for zup in range(z+1, maxZ+1): for adj in adjs: if (adj[0],adj[1],zup) in tilesByPos: zv = tilesByPos[adj[0],adj[1],zup].tileData.zv if zv > zvMax: zvMax = zv tile.tileData.zv = zvMax + 1 # 计算权重 # 从最底下开始,逐层向上计算 # 每个tile的初始权重为4,然后加上其下方压住的tile的权重:如果压了全部,则该加上被压住的tile权重; # 如果压住了一半,则加上被压住的tile权重的一半;如果是压住1/4,则加上被压住的tile权重的1/4 for z in range(0, maxZ+1): for x in range(0, maxX+1): for y in range(0, maxY+1): if (x,y,z) in tilesByPos: tile = tilesByPos[(x,y,z)] adjs = [(x,y,1.0),(x,y+1,0.5),(x,y-1,0.5),(x+1,y,0.5),(x+1,y+1,0.25),(x+1,y-1,0.25),(x-1,y,0.5),(x-1,y-1,0.25),(x-1,y+1,0.25)] weight = 4 for adj in adjs: # 从临近层往下,在某个位置找到了tile,则该tile被压住了,就不再找了 for zb in range(0, z): zb = z-1-zb if (adj[0], adj[1], zb) in tilesByPos: tileB = tilesByPos[(adj[0], adj[1], zb)] weight += tileB.tileData.weight * adj[2] break tile.tileData.weight = int(weight) pass for item in stacked_map: stack_data = levelData.stacks.add() stack_data.x = int(item[0]) stack_data.y = int(item[1]) stack_data.direction = 0 # 先默认都是0 # 数据序列化 level_protobuf_data = levelData.SerializeToString() protoFile = os.path.join(outDir, tFileName + '.bin') with open(protoFile, 'w+b') as f: f.write(level_protobuf_data) def gen_index_and_data(binDir, group = None): lvelsIndex = levelData_pb2.LevelsIndex() if group is not None: protoDataTrunk = os.path.join(binDir, 'levels-' + str(group) + '.bin') protoDataIndex = os.path.join(binDir, 'levelsIndex-' + str(group) + '.bin') else: protoDataTrunk = os.path.join(binDir, 'levels' + '.bin') protoDataIndex = os.path.join(binDir, 'levelsIndex' + '.bin') offset = 0 for filename in os.listdir(binDir): my_message = levelData_pb2.LevelData() binFile = os.path.splitext(filename)[0] + '.bin' binFile = os.path.join(binDir, binFile) if os.path.exists(binFile) : # print(binFile) with open(binFile, "rb") as f: binary_data = f.read() my_message.ParseFromString(binary_data) os.remove(binFile) with open(protoDataTrunk, 'ab') as f: f.write(binary_data) lvelsIndex.LevelsIndex[os.path.splitext(filename)[0]].len = len(binary_data) lvelsIndex.LevelsIndex[os.path.splitext(filename)[0]].offset = offset offset += len(binary_data) level_protobuf_index = lvelsIndex.SerializePartialToString() with open(protoDataIndex, 'w+b') as f: f.write(level_protobuf_index) # 将所有的模板文件转换成protobuf格式 def convert_all_templates(): # 清空loadable目录 for root, dirs, files in os.walk('../loadable'): for name in files: os.remove(os.path.join(root, name)) temp_list = parse_level_inst_info(['','3']) dirs = ['../tf_templates', '../miniGame', '../templates'] for temp_name in temp_list: print(f"Converting template {temp_name}") # 判断正确的目录 for d in dirs: if os.path.exists(os.path.join(d, temp_name + '.json')): conver_json_to_proto(d, temp_name, '../loadable') break gen_index_and_data('../loadable') # 修改levelInstInfo.csv文件: 对于每一行,如果其try1一直到try10的随机数都是100,则一次将其加1 def correct_random_seed(): for f in ['../conf/levelInstInfo.csv', '../conf/levelInstInfo-2.csv']: lines = None with open(f, 'r') as file: lines = file.readlines() with open(f, 'w') as file: for line in lines: if line.startswith('#'): file.write(line) continue fs = line.split(',') if len(fs) < 12: file.write(line) continue for i in range(2, 12): if fs[i].split('|')[1] == '100': fs[i] = fs[i].replace('100', '%d' % (100+i)) file.write(','.join(fs)) # 将关卡实例每个关卡单独保存一个,避免改动的时候相互影响 def split_level_inst_info(): # 首先读取levelInfo.csv文件,对于每一个关卡,解析其所使用的实例名字信息 # 然后读取levelInstInfo.csv文件,得到每一个实例的详细信息 # 新建一个levelInstInfo-new.csv文件,对于每一个关卡,将其所使用的实例的信息改名成inst-关卡名,并存入上一步的详细信息 # 最后将levelInstInfo-new.csv文件保存 lvs_inst = {} with open('../conf/levelInfo-2.csv', 'r') as file: for line in file.readlines(): if line.startswith('#'): continue fs = line.split(',') lvs_inst[fs[0]] = (fs[1], fs) inst_info = {} with open('../conf/levelInstInfo-2.csv', 'r') as file: for line in file.readlines(): if line.startswith('#'): continue fs = line.split(',') inst_info[fs[0]] = ",".join(fs[1:]) # 更新info with open('../conf/levelInfo-2-new.csv', 'w') as file: for lv, (inst,li) in lvs_inst.items(): li[1] = 'inst-%s' % (lv,) file.write(",".join(li)) # 写入实例信息 with open('../conf/levelInstInfo-2-new.csv', 'w') as file: for lv, (inst,li) in lvs_inst.items(): if inst in inst_info: file.write("inst-%s,%s" % (lv, inst_info[inst])) else: print(f"Error: {inst} not found in levelInstInfo.csv") pass # 更新关卡实例的随机值 def updateRandomSeed(lvInfoFN, instInfoFN, seedsInfo): # seedsInfo每一行的格式:[Tile] resovler id: 31,37710|539110|14697|465925|662106|151463|667120|234313|60714|840794| # 解析seedsInfo,得到关卡和实例的时机值 seeds = {} for line in seedsInfo.split('\n'): fs = line.split(' ') if len(fs) < 4: continue fs = fs[3].split(',') seeds[fs[0]] = fs[1].split('|') # 先通过lvInfoFN文件,得到所有的关卡对应的实例名称 seeds4Inst = {} with open(lvInfoFN, 'r') as file: for line in file.readlines(): if line.startswith('#'): continue fs = line.split(',') if fs[0] in seeds: seeds4Inst[fs[1]] = seeds[fs[0]] # 对于instInfoFN文件,对于每一行,如果lvid在seeds里面,则用上面的随机值来更新一下;否则原封不动写入新文件 with open(instInfoFN, 'r') as infile: lines = infile.readlines() with open(instInfoFN + '-new', 'w') as outfile: for line in lines: fs = line.split(',') if len(fs) < 2: outfile.write(line) continue if fs[0] in seeds4Inst: for i in range(2, 12): v = seeds4Inst[fs[0]][i-2].split('-') ps = fs[i].split('|') ps[0] = v[0] ps[1] = v[1] fs[i] = '|'.join(ps) outfile.write(','.join(fs)) # 将instInfoFN-new文件重命名为instInfoFN os.rename(instInfoFN + '-new', instInfoFN) pass # 将所有的关卡文件中tile的尺寸调整一下 def adjust_tile_size(templates_dir): # 遍历目录中的所有json文件,将属性 tileheight 和 tilewidth 调整为 40; # 将tilesets中所有元素的 imageheight 和 imagewidth 调整为 800, tilecount 调整为 100, tileheight 和 tilewidth 调整为 80 for root, dirs, files in os.walk(templates_dir): for name in files: if not name.endswith('.json'): continue jsonFile = os.path.join(root, name) with open(jsonFile, 'r') as f: jsonData = json.load(f) jsonData['tilewidth'] = 40 jsonData['tileheight'] = 40 for tileset in jsonData['tilesets']: tileset['imageheight'] = 800 tileset['imagewidth'] = 800 tileset['tilecount'] = 100 tileset['tileheight'] = 80 tileset['tilewidth'] = 80 with open(jsonFile, 'w') as f: json.dump(jsonData, f, indent=4) if __name__ == '__main__': # 获取当前工作目录 current_dir = os.getcwd() script_dir = os.path.dirname(os.path.abspath(__file__)) os.chdir(script_dir) # 得到可以发布的protobuf文件 # convert_all_templates() # correct_random_seed() # split_level_inst_info() # 更新随机信息 seedsInfo = """ """ #updateRandomSeed('../conf/levelInfo-3.csv', '../conf/levelInstInfo-3.csv', seedsInfo) adjust_tile_size("../tf_templates") # 恢复当前工作目录 os.chdir(current_dir)