From 20ff79efc2d04f827765528587acf11eec527cb4 Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Sat, 1 May 2021 19:05:07 +0200 Subject: [PATCH 01/20] Add python converter --- tools/gamesave-convert/gamesave_convert.py | 546 +++++++++++++++++++++ 1 file changed, 546 insertions(+) create mode 100644 tools/gamesave-convert/gamesave_convert.py diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py new file mode 100644 index 000000000..93f2c1794 --- /dev/null +++ b/tools/gamesave-convert/gamesave_convert.py @@ -0,0 +1,546 @@ +#!python + +import argparse +import os +import struct +import zlib +from enum import IntEnum + + +FILEINFO_CONFIG = { + 'ver 1.0.7': { 'str_encoding': 'cp1251', 'obj_id_format': 'QQQ' }, + 'ver 1.7.3': { 'str_encoding': 'utf-8', 'obj_id_format': 'Q' } +} + + +class VarType(IntEnum): + Integer = 6 + Float = 7 + String = 8 + Object = 9 + Reference = 10 + ArrayReference = 11 + Pointer = 12 + + +# Deserialization + +def read_int8_16_32(buffer, cur_ptr): + int8 = buffer[cur_ptr] + cur_ptr += 1 + if int8 < 0xfe: + return int8, cur_ptr + elif int8 == 0xfe: + int16 = struct.unpack_from('H', buffer, cur_ptr)[0] + cur_ptr += 2 + return int16, cur_ptr + else: + int32 = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + return int32, cur_ptr + + +def read_string(buffer, cur_ptr, encoding): + str_len, cur_ptr = read_int8_16_32(buffer, cur_ptr) + if str_len == 0: + return None, cur_ptr + s = struct.unpack_from(f'{str_len-1}s', buffer, cur_ptr)[0] # str_len-1 to skip trailing '\0' + s = s.decode(encoding) + cur_ptr += str_len + return s, cur_ptr + + +def read_attributes_data(buffer, v, cur_ptr, str_encoding): + attributes = v['attr'] + num_subclasses, cur_ptr = read_int8_16_32(buffer, cur_ptr) + name_code, cur_ptr = read_int8_16_32(buffer, cur_ptr) + value, cur_ptr = read_string(buffer, cur_ptr, str_encoding) + attributes.append({ + 'num_subclasses': num_subclasses, + 'name_code': name_code, + 'value': value + }) + + for _ in range(num_subclasses): + v, cur_ptr = read_attributes_data(buffer, v, cur_ptr, str_encoding) + + return v, cur_ptr + + +def read_value(var_type, buffer, cur_ptr, str_encoding, obj_id_format): + if var_type == VarType.Integer: + v = struct.unpack_from('l', buffer, cur_ptr)[0] + cur_ptr += 4 + elif var_type == VarType.Float: + v = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + elif var_type == VarType.String: + v, cur_ptr = read_string(buffer, cur_ptr, str_encoding) + elif var_type == VarType.Object: + v = {} + v['id'] = struct.unpack_from(obj_id_format, buffer, cur_ptr) + cur_ptr += struct.calcsize(obj_id_format) + v['attr'] = [] + v, cur_ptr = read_attributes_data(buffer, v, cur_ptr, str_encoding) + elif var_type == VarType.Reference: + v = {} + var_index, cur_ptr = read_int8_16_32(buffer, cur_ptr) + v['var_index'] = var_index + if var_index != 0xffffffff: + array_index, cur_ptr = read_int8_16_32(buffer, cur_ptr) + v['array_index'] = array_index + elif var_type == VarType.ArrayReference: + v = {} + var_index, cur_ptr = read_int8_16_32(buffer, cur_ptr) + v['var_index'] = var_index + if var_index != 0xffffffff: + array_index, cur_ptr = read_int8_16_32(buffer, cur_ptr) + v['array_index'] = array_index + s, cur_ptr = read_string(buffer, cur_ptr, str_encoding) + v['str'] = s + elif var_type == VarType.Pointer: + v = struct.unpack_from('P', buffer, cur_ptr)[0] + cur_ptr += 8 + else: + raise Error(msg=f'{var_type} was not handled') + + return v, cur_ptr + + +def read_variable(buffer, cur_ptr, str_encoding, obj_id_format): + name, cur_ptr = read_string(buffer, cur_ptr, str_encoding) + type = struct.unpack_from('I', buffer, cur_ptr)[0] + type = VarType(type) + cur_ptr += 4 + + num_values = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += struct.calcsize('I') + + values = [] + for _ in range(num_values): + v, cur_ptr = read_value(type, buffer, cur_ptr, str_encoding, obj_id_format) + values.append(v) + + var = { + 'name': name, + 'type': type, + 'values': values + } + + return var, cur_ptr + + +def read_save(file_name): + with open(file_name, 'rb') as f: + save_data = dict() + file_info = struct.unpack('32s', f.read(32))[0] + file_info = file_info[0:file_info.find(b'\x00')] + file_info = file_info.decode('utf-8') + save_data['file_info'] = file_info + + str_encoding = FILEINFO_CONFIG[file_info]['str_encoding'] + obj_id_format = FILEINFO_CONFIG[file_info]['obj_id_format'] + + extdata_offset = struct.unpack('I', f.read(4))[0] + extdata_size_decompressed = struct.unpack('I', f.read(4))[0] + + size_decompressed = struct.unpack('I', f.read(4))[0] + size_compressed = struct.unpack('I', f.read(4))[0] + + buffer_compressed = struct.unpack(f'{size_compressed}s', f.read(size_compressed))[0] + buffer = zlib.decompress(buffer_compressed) + + cur_ptr = 0 + save_data['program_dir'], cur_ptr = read_string(buffer, cur_ptr, str_encoding) + + save_data['strings'] = [] + num_strings, cur_ptr = read_int8_16_32(buffer, cur_ptr) + for _ in range(num_strings): + s, cur_ptr = read_string(buffer, cur_ptr, str_encoding) + save_data['strings'].append(s) + + save_data['segments'] = [] + num_segments, cur_ptr = read_int8_16_32(buffer, cur_ptr) + for _ in range(num_segments): + s, cur_ptr = read_string(buffer, cur_ptr, str_encoding) + save_data['segments'].append(s) + + save_data['vars'] = [] + num_vars, cur_ptr = read_int8_16_32(buffer, cur_ptr) + for i in range(num_vars): + var, cur_ptr = read_variable(buffer, cur_ptr, str_encoding, obj_id_format) + save_data['vars'].append(var) + + assert(cur_ptr == size_decompressed) + + # read extdata + assert(extdata_offset == f.tell()) + extdata_size_compressed = struct.unpack('I', f.read(4))[0] + extdata_buffer_compressed = struct.unpack(f'{extdata_size_compressed}s', f.read(extdata_size_compressed))[0] + extdata_buffer = zlib.decompress(extdata_buffer_compressed) + cur_ptr = 0 + save_name_len = struct.unpack_from('l', extdata_buffer, cur_ptr)[0] + cur_ptr += 4 + save_icon_size = struct.unpack_from('l', extdata_buffer, cur_ptr)[0] + cur_ptr += 4 + save_name = struct.unpack_from(f'{save_name_len-1}s', extdata_buffer, cur_ptr)[0] # len-1 to skip trailing '\0' + save_data['save_name'] = save_name.decode(str_encoding) + cur_ptr += save_name_len + save_data['save_icon'] = struct.unpack_from(f'{save_icon_size}s', extdata_buffer, cur_ptr)[0] + cur_ptr += save_icon_size + + assert(cur_ptr == extdata_size_decompressed) + + # check EOF + assert(f.tell() == os.fstat(f.fileno()).st_size) + + return save_data + + +# Serialization + +def write_int8_16_32(value, buffer): + if value < 0xfe: + buffer += struct.pack('B', value) + return buffer + elif value < 0xffff: + buffer += struct.pack('B', 0xfe) + buffer += struct.pack('H', value) + return buffer + else: + buffer += struct.pack('B', 0xff) + buffer += struct.pack('I', value) + return buffer + + +def write_string(s, buffer, encoding): + if s is not None: + s = s.encode(encoding) + b'\x00' + str_len = len(s) + buffer = write_int8_16_32(str_len, buffer) + buffer += struct.pack(f'{str_len}s', s) + else: + buffer += b'\x00' + return buffer + + +def write_attributes_data(attr, buffer, str_encoding): + a = attr[0] + buffer = write_int8_16_32(a['num_subclasses'], buffer) + buffer = write_int8_16_32(a['name_code'], buffer) + buffer = write_string(a['value'], buffer, str_encoding) + + for _ in range(a['num_subclasses']): + attr = attr[1:] + buffer, attr = write_attributes_data(attr, buffer, str_encoding) + return buffer, attr + + +def write_value(var_type, value, buffer, fileinfo_config): + if var_type == VarType.Integer: + buffer += struct.pack('l', value) + elif var_type == VarType.Float: + buffer += struct.pack('f', value) + elif var_type == VarType.String: + buffer = write_string(value, buffer, fileinfo_config['str_encoding']) + elif var_type == VarType.Object: + id = value['id'] + buffer += struct.pack(fileinfo_config['obj_id_format'], *id) + if value['attr']: + buffer, _ = write_attributes_data(value['attr'], buffer, fileinfo_config['str_encoding']) + elif var_type == VarType.Reference: + buffer = write_int8_16_32(value['var_index'], buffer) + if value['var_index'] != 0xffffffff: + buffer = write_int8_16_32(value['array_index'], buffer) + elif var_type == VarType.ArrayReference: + buffer = write_int8_16_32(value['var_index'], buffer) + if value['var_index'] != 0xffffffff: + buffer = write_int8_16_32(value['array_index'], buffer) + buffer = write_string(value['str'], buffer, fileinfo_config['str_encoding']) + elif var_type == VarType.Pointer: + struct.pack_into('P', buffer, value) + else: + raise Error(msg=f'{var_type} was not handled') + + return buffer + + +def write_variable(var, buffer, fileinfo_config): + buffer = write_string(var['name'], buffer, fileinfo_config['str_encoding']) + buffer += struct.pack('I', var['type'].value) + buffer += struct.pack('I', len(var['values'])) + + for value in var['values']: + buffer = write_value(var['type'], value, buffer, fileinfo_config) + + return buffer + + +def write_save(save_data, filename): + file_info = save_data['file_info'] + fileinfo_config = FILEINFO_CONFIG[file_info] + str_encoding = fileinfo_config['str_encoding'] + obj_id_format = fileinfo_config['obj_id_format'] + + buffer = bytearray() + buffer = write_string(save_data['program_dir'], buffer, str_encoding) + + strings = save_data['strings'] + buffer = write_int8_16_32(len(strings), buffer) + for s in strings: + buffer = write_string(s, buffer, str_encoding) + + segments = save_data['segments'] + buffer = write_int8_16_32(len(segments), buffer) + for s in segments: + buffer = write_string(s, buffer, str_encoding) + + variables = save_data['vars'] + buffer = write_int8_16_32(len(variables), buffer) + for var in variables: + buffer = write_variable(var, buffer, fileinfo_config) + buffer_compressed = zlib.compress(buffer, zlib.Z_BEST_COMPRESSION) + + # write extdata + extdata_buffer = bytearray() + save_name_bytes = save_data['save_name'].encode(str_encoding) + b'\x00' + save_name_len = len(save_name_bytes) + extdata_buffer += struct.pack('l', save_name_len) + save_icon_size = len(save_data['save_icon']) + extdata_buffer += struct.pack('l', save_icon_size) + extdata_buffer += struct.pack(f'{save_name_len}s', save_name_bytes) + extdata_buffer += struct.pack(f'{save_icon_size}s', save_data['save_icon']) + extdata_buffer_compressed = zlib.compress(extdata_buffer, zlib.Z_BEST_COMPRESSION) + + # compose all the data + raw_data = bytearray() + raw_data += struct.pack('32s', file_info.encode(str_encoding) + b'\x00') + raw_data += struct.pack('I', struct.calcsize('32sIIII') + len(buffer_compressed)) # extdata_offset + raw_data += struct.pack('I', len(extdata_buffer)) + raw_data += struct.pack('I', len(buffer)) + raw_data += struct.pack('I', len(buffer_compressed)) + raw_data += buffer_compressed + raw_data += struct.pack('I', len(extdata_buffer_compressed)) + raw_data += extdata_buffer_compressed + + with open(filename, 'wb') as f: + f.write(raw_data) + + +# Conversion + +def convert_107_to_173(save_data): + if (save_data['file_info'] == 'ver 1.7.3'): + return save_data + + save_data['file_info'] = 'ver 1.7.3' + + removed_vars = [ + 'bNetActive', + 'Net', + 'NetServer', + 'NetClient', + 'sMasterServerAddress', + 'iMasterServerPort', + 'sUserFacesPath', + 'sUserSailsPath', + 'sUserFlagsPath', + 'sNetBortNames', + 'NetModes', + 'NetTeamColor', + 'NetTeamName', + 'NSSortedPlayers', + 'NSClients', + 'NSBanList', + 'NSPlayers', + 'iServerTime', + 'iPingTime', + 'bNetServerIsStarted', + 'wClientID', + 'NCBalls', + 'NCConsole', + 'NCIsland', + 'NCCoastFoam', + 'NCSail', + 'NCVant', + 'NCRope', + 'NCFlag', + 'NCSay', + 'NCShipTracks', + 'NCLightPillar', + 'NCServer', + 'NCClients', + 'NCProfiles', + 'NCFavorites', + 'NCInetServers', + 'iLangNetClient', + 'sClientNetCamera', + 'iClientTime', + 'iClientServerTime', + 'iClientDeltaTime', + 'iTestDeltaTime', + 'bFG_Deathmatch', + 'bFG_TeamDeathmatch', + 'bFG_DefendConvoy', + 'bFG_CaptureTheFort', + 'bFG_ActiveServers', + 'bFG_WOPassword', + 'bFG_FreeServers', + 'iFG_Credit', + 'iFG_MaxShipClass', + 'iFG_Ping', + 'iCurrentServersList', + 'iNumLanServers', + 'iNumInternetServers', + 'iNumLanServersList', + 'iNumInternetServersList', + 'iNumFavServersList', + 'LanServers', + 'InternetServers', + 'LanServersList', + 'InternetServersList', + 'FavServersList', + 'g_nCompanionTaskQuantity', + 'g_objCompanionTask', + 'g_nTaskCheckRetResult', + 'iNetCannonsNum', + 'NETCANNON_CANNON', + 'NETCANNON_CULVERINE', + 'NETCANNON_MORTAR', + 'NetGoods', + 'iNetGoodsNum', + 'iNumNetIslands', + 'iNetIslandsIndex', + 'NetPerks', + 'NetShips', + 'iNetShipsNum', + 'NetSkills', + 'iNetSkillsNum', + 'NetRanks', + 'NetRewardAccuracy', + 'NetRewardVitality', + 'NetRewardVictorious', + 'iNetRanksNum', + 'NetShipHullUpgrades', + 'NetShipSailUpgrade', + 'NSBalls', + 'iServerBallIndex', + 'iServerCurrentBallIndex', + 'bServerGameStarted', + 'iServerEnvSMsg', + 'NSSail', + 'NSIsland', + 'NSTouch', + 'iServerDeltaTimeFraction', + 'iSecondsToStartGame', + 'NSFortModel', + 'NSFortBlots', + 'TopList', + 'NSWeather', + 'iServerCurWeatherNum', + 'fServerWeatherDelta', + 'fServerWeatherAngle', + 'fServerWeatherSpeed', + 'bServerWeatherIsNight', + 'bServerWeatherIsStorm', + 'bServerWhrTornado', + 'sServerLightingPath', + 'NCShipCamera', + 'NCDeckCamera', + 'NCShipLights', + 'iClientDeltaTimeFraction', + 'NCFortModel', + 'NCFortBlots', + 'rgbGameMessage', + 'iClientShipPriorityExecute', + 'iClientShipPriorityRealize', + 'iClientLastPingCode', + 'iClientLastPingTime', + 'iClientLastPingCodeTime', + 'oGameOver', + 'NCWeather', + 'iClientCurWeatherNum', + 'sClientCurrentFog', + 'bClientWeatherIsNight', + 'bClientWeatherIsLight', + 'bClientWeatherIsRain', + 'bClientWeatherIsStorm', + 'sClientLightingPath', + 'NetBI_intNRetValue', + 'NetBI_ChargeState', + 'NetBI_retComValue', + 'NetBInterface', + 'NetCannons', + 'iServerLightningSubTexX', + 'iServerLightningSubTexY', + 'NSSea', + 'NCLightning', + 'fClientLightningScaleX', + 'fClientLightningScaleY', + 'NCRain', + 'NCSea', + 'NCSky', + 'NCSunGlow', + 'NCAstronomy' + ] + to_remove = [x for x in save_data['vars'] if x['name'] in removed_vars] + for var in to_remove: + save_data['vars'].remove(var) + + to_int = [ + 'sCurrentSeaExecute', + 'sCurrentSeaRealize', + 'sNewExecuteLayer', + 'sNewRealizeLayer' + ] + for var in save_data['vars']: + if var['name'] in to_int: + assert(var['type'] == VarType.String) + var['type'] = VarType.Integer + + assert(len(var['values']) == 1) + if var['values'][0] == 'execute': + var['values'][0] = 0 + elif var['values'][0] == 'realize': + var['values'][0] = 1 + else: + raise Error(msg=f"Unexpected value: {var['values'][0]}") + + if var['type'] == VarType.Object: + for val in var['values']: + val['id'] = (sum(val['id']),) # one item tuple + + return save_data + + +def dump_save(save_data, filename): + def fallback(v): + return f'Unsupported type: {type(v)}' + + import json + with open(filename, 'w', encoding='utf-8') as f: + copy = dict(save_data) + copy['strings'] = sorted(save_data['strings']) + copy['vars'] = sorted(save_data['vars'], key=lambda k: k['name']) + json.dump(copy, f, ensure_ascii=False, indent=4, default=fallback) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('input_file', help='name of a save file to convert', type=str) + args = parser.parse_args() + + input_file = os.path.abspath(args.input_file) + save_data = read_save(input_file) + + filename, file_extension = os.path.splitext(input_file) + + # dump_save(save_data, f'{filename}_read.txt') + + save_data = convert_107_to_173(save_data) + filename += '_173' + + # dump_save(save_data, f'{filename}_write.txt') + + output_file = filename + file_extension + write_save(save_data, output_file) From 6693ac4a9b09aebce70e587647a2e31213bd8ccc Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Fri, 7 May 2021 23:38:16 +0200 Subject: [PATCH 02/20] Add missing values of str->int mapping for layers --- tools/gamesave-convert/gamesave_convert.py | 39 ++++++++++++++++++---- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index 93f2c1794..a008eaf27 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -493,18 +493,45 @@ def convert_107_to_173(save_data): 'sNewExecuteLayer', 'sNewRealizeLayer' ] + layers = { + 'execute': 0, + 'realize': 1, + 'sea_execute': 2, + 'sea_realize': 3, + 'interface_execute': 4, + 'interface_realize': 5, + 'fader_execute': 6, + 'fader_realize': 7, + 'lighter_execute': 8, + 'lighter_realize': 9, + 'video_execute': 10, + 'video_realize': 11, + 'editor_realize': 12, + 'info_realize': 13, + 'sound_debug_realize': 14, + 'sea_reflection': 15, + 'sea_reflection2': 16, + 'sea_sunroad': 17, + 'sun_trace': 18, + 'sails_trace': 19, + 'hull_trace': 20, + 'mast_island_trace': 21, + 'mast_ship_trace': 22, + 'ship_cannon_trace': 23, + 'fort_cannon_trace': 24, + 'island_trace': 25, + 'shadow': 26, + 'blood': 27, + 'rain_drops': 28 + } for var in save_data['vars']: if var['name'] in to_int: assert(var['type'] == VarType.String) var['type'] = VarType.Integer assert(len(var['values']) == 1) - if var['values'][0] == 'execute': - var['values'][0] = 0 - elif var['values'][0] == 'realize': - var['values'][0] = 1 - else: - raise Error(msg=f"Unexpected value: {var['values'][0]}") + strval = var['values'][0] + var['values'][0] = layers[strval] if var['type'] == VarType.Object: for val in var['values']: From 89efb7530a89e871c15a46db8a0b0decf112f435 Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Fri, 7 May 2021 23:38:53 +0200 Subject: [PATCH 03/20] Fix raising exceptions --- tools/gamesave-convert/gamesave_convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index a008eaf27..5f60e76ec 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -102,7 +102,7 @@ def read_value(var_type, buffer, cur_ptr, str_encoding, obj_id_format): v = struct.unpack_from('P', buffer, cur_ptr)[0] cur_ptr += 8 else: - raise Error(msg=f'{var_type} was not handled') + raise RuntimeError(f'{var_type} was not handled') return v, cur_ptr @@ -260,7 +260,7 @@ def write_value(var_type, value, buffer, fileinfo_config): elif var_type == VarType.Pointer: struct.pack_into('P', buffer, value) else: - raise Error(msg=f'{var_type} was not handled') + raise RuntimeError(f'{var_type} was not handled') return buffer From b55679cb4a1f7facac397e96936a57d85d4654a2 Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Tue, 8 Jun 2021 21:27:30 +0200 Subject: [PATCH 04/20] Add string database to the gamesave converter --- tools/gamesave-convert/gamesave_convert.py | 126 +++++++---- tools/gamesave-convert/seasave.py | 245 +++++++++++++++++++++ tools/gamesave-convert/str_db.py | 41 ++++ 3 files changed, 369 insertions(+), 43 deletions(-) create mode 100644 tools/gamesave-convert/seasave.py create mode 100644 tools/gamesave-convert/str_db.py diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index 5f60e76ec..90a2383be 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -1,10 +1,18 @@ #!python +# Python 3.7: Dictionary iteration order is guaranteed to be in order of insertion +import sys +MIN_PYTHON = (3, 7) +if sys.version_info < MIN_PYTHON: + sys.exit("Python {'.'.join([str(n) for n in MIN_PYTHON])} or later is required.") import argparse import os import struct import zlib -from enum import IntEnum +from enum import Enum + +import str_db +import seasave FILEINFO_CONFIG = { @@ -13,7 +21,7 @@ } -class VarType(IntEnum): +class VarType(Enum): Integer = 6 Float = 7 String = 8 @@ -21,6 +29,7 @@ class VarType(IntEnum): Reference = 10 ArrayReference = 11 Pointer = 12 + BinaryBlob = 13 # Deserialization @@ -50,24 +59,28 @@ def read_string(buffer, cur_ptr, encoding): return s, cur_ptr -def read_attributes_data(buffer, v, cur_ptr, str_encoding): - attributes = v['attr'] - num_subclasses, cur_ptr = read_int8_16_32(buffer, cur_ptr) +def read_attributes_data(buffer, cur_ptr, s_db, str_encoding): + num_attributes, cur_ptr = read_int8_16_32(buffer, cur_ptr) name_code, cur_ptr = read_int8_16_32(buffer, cur_ptr) value, cur_ptr = read_string(buffer, cur_ptr, str_encoding) - attributes.append({ - 'num_subclasses': num_subclasses, + + a = { 'name_code': name_code, 'value': value - }) + } - for _ in range(num_subclasses): - v, cur_ptr = read_attributes_data(buffer, v, cur_ptr, str_encoding) + if num_attributes > 0: + attributes = {} + for _ in range(num_attributes): + attr, cur_ptr = read_attributes_data(buffer, cur_ptr, s_db, str_encoding) + attr_name = str_db.get_str(s_db, attr['name_code']) + attributes[attr_name] = attr + a['attributes'] = attributes - return v, cur_ptr + return a, cur_ptr -def read_value(var_type, buffer, cur_ptr, str_encoding, obj_id_format): +def read_value(var_type, buffer, cur_ptr, s_db, str_encoding, obj_id_format): if var_type == VarType.Integer: v = struct.unpack_from('l', buffer, cur_ptr)[0] cur_ptr += 4 @@ -80,8 +93,12 @@ def read_value(var_type, buffer, cur_ptr, str_encoding, obj_id_format): v = {} v['id'] = struct.unpack_from(obj_id_format, buffer, cur_ptr) cur_ptr += struct.calcsize(obj_id_format) - v['attr'] = [] - v, cur_ptr = read_attributes_data(buffer, v, cur_ptr, str_encoding) + attributes = {} + attr, cur_ptr = read_attributes_data(buffer, cur_ptr, s_db, str_encoding) + attr_name = str_db.get_str(s_db, attr['name_code']) + attributes[attr_name] = attr + v['attributes'] = attributes + elif var_type == VarType.Reference: v = {} var_index, cur_ptr = read_int8_16_32(buffer, cur_ptr) @@ -107,8 +124,7 @@ def read_value(var_type, buffer, cur_ptr, str_encoding, obj_id_format): return v, cur_ptr -def read_variable(buffer, cur_ptr, str_encoding, obj_id_format): - name, cur_ptr = read_string(buffer, cur_ptr, str_encoding) +def read_variable(buffer, cur_ptr, s_db, str_encoding, obj_id_format): type = struct.unpack_from('I', buffer, cur_ptr)[0] type = VarType(type) cur_ptr += 4 @@ -118,11 +134,10 @@ def read_variable(buffer, cur_ptr, str_encoding, obj_id_format): values = [] for _ in range(num_values): - v, cur_ptr = read_value(type, buffer, cur_ptr, str_encoding, obj_id_format) + v, cur_ptr = read_value(type, buffer, cur_ptr, s_db, str_encoding, obj_id_format) values.append(v) var = { - 'name': name, 'type': type, 'values': values } @@ -159,17 +174,26 @@ def read_save(file_name): s, cur_ptr = read_string(buffer, cur_ptr, str_encoding) save_data['strings'].append(s) + s_db = str_db.create_db(save_data['strings']) + save_data['segments'] = [] num_segments, cur_ptr = read_int8_16_32(buffer, cur_ptr) for _ in range(num_segments): s, cur_ptr = read_string(buffer, cur_ptr, str_encoding) save_data['segments'].append(s) - save_data['vars'] = [] + vars = {} num_vars, cur_ptr = read_int8_16_32(buffer, cur_ptr) - for i in range(num_vars): - var, cur_ptr = read_variable(buffer, cur_ptr, str_encoding, obj_id_format) - save_data['vars'].append(var) + for _ in range(num_vars): + name, cur_ptr = read_string(buffer, cur_ptr, str_encoding) + var, cur_ptr = read_variable(buffer, cur_ptr, s_db, str_encoding, obj_id_format) + vars[name] = var + + #if 'oSeaSave' in vars: + # seasave_data = vars['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] + # seasave.read_seasave(bytes.fromhex(seasave_data[8:])) + + save_data['vars'] = vars assert(cur_ptr == size_decompressed) @@ -224,16 +248,16 @@ def write_string(s, buffer, encoding): return buffer -def write_attributes_data(attr, buffer, str_encoding): - a = attr[0] - buffer = write_int8_16_32(a['num_subclasses'], buffer) +def write_attributes_data(a, buffer, str_encoding): + num_attributes = len(a['attributes']) if 'attributes' in a else 0 + buffer = write_int8_16_32(num_attributes, buffer) buffer = write_int8_16_32(a['name_code'], buffer) buffer = write_string(a['value'], buffer, str_encoding) - for _ in range(a['num_subclasses']): - attr = attr[1:] - buffer, attr = write_attributes_data(attr, buffer, str_encoding) - return buffer, attr + if num_attributes > 0: + for attr in a['attributes'].values(): + buffer = write_attributes_data(attr, buffer, str_encoding) + return buffer def write_value(var_type, value, buffer, fileinfo_config): @@ -246,8 +270,8 @@ def write_value(var_type, value, buffer, fileinfo_config): elif var_type == VarType.Object: id = value['id'] buffer += struct.pack(fileinfo_config['obj_id_format'], *id) - if value['attr']: - buffer, _ = write_attributes_data(value['attr'], buffer, fileinfo_config['str_encoding']) + root_attr = next(iter(value['attributes'])) + buffer = write_attributes_data(value['attributes'][root_attr], buffer, fileinfo_config['str_encoding']) elif var_type == VarType.Reference: buffer = write_int8_16_32(value['var_index'], buffer) if value['var_index'] != 0xffffffff: @@ -266,7 +290,6 @@ def write_value(var_type, value, buffer, fileinfo_config): def write_variable(var, buffer, fileinfo_config): - buffer = write_string(var['name'], buffer, fileinfo_config['str_encoding']) buffer += struct.pack('I', var['type'].value) buffer += struct.pack('I', len(var['values'])) @@ -297,8 +320,9 @@ def write_save(save_data, filename): variables = save_data['vars'] buffer = write_int8_16_32(len(variables), buffer) - for var in variables: - buffer = write_variable(var, buffer, fileinfo_config) + for varname in variables: + buffer = write_string(varname, buffer, str_encoding) + buffer = write_variable(variables[varname], buffer, fileinfo_config) buffer_compressed = zlib.compress(buffer, zlib.Z_BEST_COMPRESSION) # write extdata @@ -483,9 +507,8 @@ def convert_107_to_173(save_data): 'NCSunGlow', 'NCAstronomy' ] - to_remove = [x for x in save_data['vars'] if x['name'] in removed_vars] - for var in to_remove: - save_data['vars'].remove(var) + for var in removed_vars: + del save_data['vars'][var] to_int = [ 'sCurrentSeaExecute', @@ -524,8 +547,8 @@ def convert_107_to_173(save_data): 'blood': 27, 'rain_drops': 28 } - for var in save_data['vars']: - if var['name'] in to_int: + for name, var in save_data['vars'].items(): + if name in to_int: assert(var['type'] == VarType.String) var['type'] = VarType.Integer @@ -537,18 +560,35 @@ def convert_107_to_173(save_data): for val in var['values']: val['id'] = (sum(val['id']),) # one item tuple + # itemModels array should have ITEMS_QUANTITY elements (see program/items/items.h) + #if name == 'itemModels': + # assert(var['type'] == VarType.Object) + # assert(len(var['values']) == 498) + # elem_copy = dict(var['values'][-1]) + # for _ in range(500-498): + # var['values'].append(elem_copy) + return save_data def dump_save(save_data, filename): def fallback(v): + if isinstance(v, Enum): + return v.name return f'Unsupported type: {type(v)}' import json with open(filename, 'w', encoding='utf-8') as f: copy = dict(save_data) - copy['strings'] = sorted(save_data['strings']) - copy['vars'] = sorted(save_data['vars'], key=lambda k: k['name']) + #copy['strings'] = sorted(save_data['strings']) + #copy['vars'] = sorted(save_data['vars'], key=lambda k: k['name']) + + #if var['type'] == VarType.Object: + # for v in var['values']: + # for a in v['attributes']: + # a['name'] = str_db.get_str(s_db, a['name_code']) + + json.dump(copy, f, ensure_ascii=False, indent=4, default=fallback) @@ -562,12 +602,12 @@ def fallback(v): filename, file_extension = os.path.splitext(input_file) - # dump_save(save_data, f'{filename}_read.txt') + dump_save(save_data, f'{filename}_read.json') save_data = convert_107_to_173(save_data) filename += '_173' - # dump_save(save_data, f'{filename}_write.txt') + dump_save(save_data, f'{filename}_write.json') output_file = filename + file_extension write_save(save_data, output_file) diff --git a/tools/gamesave-convert/seasave.py b/tools/gamesave-convert/seasave.py new file mode 100644 index 000000000..dfb05ac89 --- /dev/null +++ b/tools/gamesave-convert/seasave.py @@ -0,0 +1,245 @@ +import struct + +def read_buffer(buffer, cur_ptr): + size = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + buf = struct.unpack_from(f'{size}s', buffer, cur_ptr)[0] + cur_ptr += size + return buf, cur_ptr + + +def read_string(buffer, cur_ptr, encoding): + str_len = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + if str_len == 0: + return '', cur_ptr + s = struct.unpack_from(f'{str_len-1}s', buffer, cur_ptr)[0] # str_len-1 to skip trailing '\0' + s = s.decode(encoding) + cur_ptr += str_len + return s, cur_ptr + + +def read_attr_ptr(buffer, cur_ptr): + index = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + s, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + attr_ptr = { + 'index' : index, + 'str': s + } + return attr_ptr, cur_ptr + + +def read_aiballs(buffer, cur_ptr): + return + + +def read_aicannon(): + return + + +def read_aifort(): + return + + +def read_aigroup(): + return + + +def read_aihelper(buffer, cur_ptr): + gravity = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + num_relations = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + relations, cur_ptr = read_buffer(buffer, cur_ptr) + sea_cameras, cur_ptr = read_attr_ptr(buffer, cur_ptr) + + num_characters = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + characters = [] + for _ in range(num_characters): + char_ptr, cur_ptr = read_attr_ptr(buffer, cur_ptr) + characters.append(char_ptr) + + num_main_characters = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + main_characters = [] + for _ in range(num_main_characters): + char_ptr, cur_ptr = read_attr_ptr(buffer, cur_ptr) + main_characters.append(char_ptr) + + aihelper = { + 'gravity': gravity, + 'num_relations': num_relations, + 'relations': relations, + 'sea_cameras': sea_cameras, + 'characters': characters, + 'main_characters': main_characters + } + return aihelper, cur_ptr + + +def read_ship(): + return + + +def read_aiship(): + return + + +def read_aiship_camera_controller(): + return + + +def read_aiship_cannon_controller(): + return + + +def read_aiship_move_controller(): + return + + +def read_aiship_rotate_controller(): + return + + +def read_aiship_speed_controller(): + return + + +def read_aiship_task_controller(): + return + + +def read_aiship_touch_controller(): + return + + +def read_deck_cam(buffer, cur_ptr): + vertices, cur_ptr = read_buffer(buffer, cur_ptr) + + format = '13f3f3f3f3f3ff' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + + deck_cam = { + 'distance_sensivity': data[0], + 'height_angle_sensivity': data[1], + 'azimuth_angle_sensivity': data[2], + 'rocking_x': data[3], + 'rocking_z': data[4], + 'men_step_up': data[5], + 'men_step_min': data[6], + 'height_max': data[7], + 'height_min': data[8], + 'height_step': data[9], + 'cam_max_x': data[10], + 'cam_min_x': data[11], + 'default_height': data[12], + 'g_vec1': { 'x': data[13], 'y': data[14], 'z': data[15] }, + 'g_vec2': { 'x': data[16], 'y': data[17], 'z': data[18] }, + 'g_vec3': { 'x': data[19], 'y': data[20], 'z': data[21] }, + 'cam_pos': { 'x': data[22], 'y': data[23], 'z': data[24] }, + 'cam_angle': { 'x': data[25], 'y': data[26], 'z': data[27] }, + 'eye_height': data[28] + } + assert(len(data) == 29) + + deck_cam['screen_rect'], cur_ptr = read_buffer(buffer, cur_ptr) + + format = '4If' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + deck_cam['lock_x'] = data[0] + deck_cam['lock_y'] = data[1] + deck_cam['is_on'] = data[2] + deck_cam['is_active'] = data[3] + deck_cam['perspective'] = data[4] + assert(len(data) == 5) + + deck_cam['character'], cur_ptr = read_attr_ptr(buffer, cur_ptr) + + return deck_cam, cur_ptr + + +def read_free_cam(buffer, cur_ptr): + format = '3f3ffIIIf' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + + free_cam = { + 'pos': { 'x': data[0], 'y': data[1], 'z': data[2] }, + 'angle': { 'x': data[3], 'y': data[4], 'z': data[5] }, + 'fov': data[6], + 'lock_x': data[7], + 'lock_y': data[8], + 'is_onland': data[9], + 'cam_onland_height': data[10] + } + assert(len(data) == 11) + + return free_cam, cur_ptr + + +def read_ship_cam(buffer, cur_ptr): + format = 'II19f3f3ff4If' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + + ship_cam = { + 'lock_x': data[0], + 'lock_y': data[1], + 'min_height_on_sea': data[2], + 'max_height_on_sea': data[3], + 'distance': data[4], + 'max_distance': data[5], + 'min_distance': data[6], + 'distance_delta': data[7], + 'distance_inertia': data[8], + 'min_angle_x': data[9], + 'max_angle_x': data[10], + 'angle_x_delta': data[11], + 'angle_x_inertia': data[12], + 'angle_y_delta': data[13], + 'angle_y_inertia': data[14], + 'distance_sensivity': data[15], + 'azimuth_angle_sensivity': data[16], + 'height_angle_sensivity': data[17], + 'height_angle_onship_sensivity': data[18], + 'invert_mouse_x': data[19], + 'invert_mouse_y': data[20], + 'center': { 'x': data[21], 'y': data[22], 'z': data[23] }, + 'angle': { 'x': data[24], 'y': data[25], 'z': data[26] }, + 'model_atan_y': data[27], + 'ship_code': data[28], + 'num_islands': data[29], + 'is_on': data[30], + 'is_active': data[31], + 'perspective': data[32] + } + assert(len(data) == 33) + + ship_cam['character'], cur_ptr = read_attr_ptr(buffer, cur_ptr) + + return ship_cam, cur_ptr + + +def read_fireplace(): + return + + +def read_seasave(buffer): + seasave_data = {} + + cur_ptr = 0 + aihelper, cur_ptr = read_aihelper(buffer, cur_ptr) + + ship_cam, cur_ptr = read_ship_cam(buffer, cur_ptr) + free_cam, cur_ptr = read_free_cam(buffer, cur_ptr) + deck_cam, cur_ptr = read_deck_cam(buffer, cur_ptr) + + aiballs, curptr = read_aiballs(buffer, cur_ptr) + + +def write_seasave(): + return \ No newline at end of file diff --git a/tools/gamesave-convert/str_db.py b/tools/gamesave-convert/str_db.py new file mode 100644 index 000000000..d60937207 --- /dev/null +++ b/tools/gamesave-convert/str_db.py @@ -0,0 +1,41 @@ +HASH_TABLE_SIZE = 512 + +def hash(str): + h = 0 + str = str.lower() + for c in str: + h = (h << 4) + ord(c) + g = h & 0xf0000000 + if g != 0: + h ^= g >> 24 + h ^= g + + return h + + +def get_int(db, str): + h = hash(str) + row_id = h % HASH_TABLE_SIZE + elem_id = db[row_id].index(str) + return row_id << 16 | elem_id + + +def get_str(db, i): + row_id = i >> 16 + elem_id = i & 0xffff + if row_id >= HASH_TABLE_SIZE or elem_id >= len(db[row_id]): + return f'str_db[{hex(i)}]' + return db[row_id][elem_id] + + +def create_db(str_list): + db = [] + for i in range(HASH_TABLE_SIZE): + db.append([]) + + for str in str_list: + h = hash(str) + row_id = h % HASH_TABLE_SIZE + db[row_id].append(str) + + return db From e02da48f002e5b26992820f43b4dc651f4e84a27 Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Tue, 8 Jun 2021 23:07:10 +0200 Subject: [PATCH 05/20] Don't add null strings to the database --- tools/gamesave-convert/gamesave_convert.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index 90a2383be..bfe18c813 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -172,7 +172,8 @@ def read_save(file_name): num_strings, cur_ptr = read_int8_16_32(buffer, cur_ptr) for _ in range(num_strings): s, cur_ptr = read_string(buffer, cur_ptr, str_encoding) - save_data['strings'].append(s) + if s is not None: + save_data['strings'].append(s) s_db = str_db.create_db(save_data['strings']) From ef9d8589572fea4e39154a64f7a358780b253e60 Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Tue, 8 Jun 2021 23:20:37 +0200 Subject: [PATCH 06/20] Rename ArrayReference to AttributeReference --- tools/gamesave-convert/gamesave_convert.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index bfe18c813..962ca683a 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -27,9 +27,8 @@ class VarType(Enum): String = 8 Object = 9 Reference = 10 - ArrayReference = 11 + AttributeReference = 11 Pointer = 12 - BinaryBlob = 13 # Deserialization @@ -106,7 +105,7 @@ def read_value(var_type, buffer, cur_ptr, s_db, str_encoding, obj_id_format): if var_index != 0xffffffff: array_index, cur_ptr = read_int8_16_32(buffer, cur_ptr) v['array_index'] = array_index - elif var_type == VarType.ArrayReference: + elif var_type == VarType.AttributeReference: v = {} var_index, cur_ptr = read_int8_16_32(buffer, cur_ptr) v['var_index'] = var_index @@ -277,7 +276,7 @@ def write_value(var_type, value, buffer, fileinfo_config): buffer = write_int8_16_32(value['var_index'], buffer) if value['var_index'] != 0xffffffff: buffer = write_int8_16_32(value['array_index'], buffer) - elif var_type == VarType.ArrayReference: + elif var_type == VarType.AttributeReference: buffer = write_int8_16_32(value['var_index'], buffer) if value['var_index'] != 0xffffffff: buffer = write_int8_16_32(value['array_index'], buffer) From 340cfae573e07ae5673ada6c876519010149d30b Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Fri, 18 Jun 2021 23:31:22 +0200 Subject: [PATCH 07/20] Detect non-ascii strings and attribute names --- tools/gamesave-convert/gamesave_convert.py | 127 +++++++++++++-------- tools/gamesave-convert/str_db.py | 48 ++++++-- 2 files changed, 113 insertions(+), 62 deletions(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index 962ca683a..7dbbeed96 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -1,6 +1,8 @@ #!python + # Python 3.7: Dictionary iteration order is guaranteed to be in order of insertion import sys + MIN_PYTHON = (3, 7) if sys.version_info < MIN_PYTHON: sys.exit("Python {'.'.join([str(n) for n in MIN_PYTHON])} or later is required.") @@ -9,15 +11,15 @@ import os import struct import zlib +import logging from enum import Enum import str_db import seasave - FILEINFO_CONFIG = { - 'ver 1.0.7': { 'str_encoding': 'cp1251', 'obj_id_format': 'QQQ' }, - 'ver 1.7.3': { 'str_encoding': 'utf-8', 'obj_id_format': 'Q' } + 'ver 1.0.7': {'str_encoding': 'cp1251', 'obj_id_format': 'QQQ'}, + 'ver 1.7.3': {'str_encoding': 'utf-8', 'obj_id_format': 'Q'} } @@ -52,7 +54,7 @@ def read_string(buffer, cur_ptr, encoding): str_len, cur_ptr = read_int8_16_32(buffer, cur_ptr) if str_len == 0: return None, cur_ptr - s = struct.unpack_from(f'{str_len-1}s', buffer, cur_ptr)[0] # str_len-1 to skip trailing '\0' + s = struct.unpack_from(f'{str_len - 1}s', buffer, cur_ptr)[0] # str_len-1 to skip trailing '\0' s = s.decode(encoding) cur_ptr += str_len return s, cur_ptr @@ -74,7 +76,7 @@ def read_attributes_data(buffer, cur_ptr, s_db, str_encoding): attr, cur_ptr = read_attributes_data(buffer, cur_ptr, s_db, str_encoding) attr_name = str_db.get_str(s_db, attr['name_code']) attributes[attr_name] = attr - a['attributes'] = attributes + a['attributes'] = attributes return a, cur_ptr @@ -124,8 +126,8 @@ def read_value(var_type, buffer, cur_ptr, s_db, str_encoding, obj_id_format): def read_variable(buffer, cur_ptr, s_db, str_encoding, obj_id_format): - type = struct.unpack_from('I', buffer, cur_ptr)[0] - type = VarType(type) + t = struct.unpack_from('I', buffer, cur_ptr)[0] + t = VarType(t) cur_ptr += 4 num_values = struct.unpack_from('I', buffer, cur_ptr)[0] @@ -133,11 +135,11 @@ def read_variable(buffer, cur_ptr, s_db, str_encoding, obj_id_format): values = [] for _ in range(num_values): - v, cur_ptr = read_value(type, buffer, cur_ptr, s_db, str_encoding, obj_id_format) + v, cur_ptr = read_value(t, buffer, cur_ptr, s_db, str_encoding, obj_id_format) values.append(v) var = { - 'type': type, + 'type': t, 'values': values } @@ -146,7 +148,7 @@ def read_variable(buffer, cur_ptr, s_db, str_encoding, obj_id_format): def read_save(file_name): with open(file_name, 'rb') as f: - save_data = dict() + save_data = {} file_info = struct.unpack('32s', f.read(32))[0] file_info = file_info[0:file_info.find(b'\x00')] file_info = file_info.decode('utf-8') @@ -167,14 +169,19 @@ def read_save(file_name): cur_ptr = 0 save_data['program_dir'], cur_ptr = read_string(buffer, cur_ptr, str_encoding) - save_data['strings'] = [] + strings = [] num_strings, cur_ptr = read_int8_16_32(buffer, cur_ptr) for _ in range(num_strings): s, cur_ptr = read_string(buffer, cur_ptr, str_encoding) if s is not None: - save_data['strings'].append(s) + #if not s.isascii(): + #logging.warning(f'non ascii string: {s} (cp1251: {s.encode("cp1251").hex(" ")}, utf-8: {s.encode("utf-8").hex(" ")})') + strings.append(s) - s_db = str_db.create_db(save_data['strings']) + s_db = str_db.create_db(strings, str_encoding) + save_data['strings'] = {} + for s in strings: + save_data['strings'][s] = str_db.get_int(s_db, s, str_encoding) save_data['segments'] = [] num_segments, cur_ptr = read_int8_16_32(buffer, cur_ptr) @@ -188,17 +195,17 @@ def read_save(file_name): name, cur_ptr = read_string(buffer, cur_ptr, str_encoding) var, cur_ptr = read_variable(buffer, cur_ptr, s_db, str_encoding, obj_id_format) vars[name] = var - - #if 'oSeaSave' in vars: + + # if 'oSeaSave' in vars: # seasave_data = vars['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] # seasave.read_seasave(bytes.fromhex(seasave_data[8:])) save_data['vars'] = vars - assert(cur_ptr == size_decompressed) + assert (cur_ptr == size_decompressed) # read extdata - assert(extdata_offset == f.tell()) + assert (extdata_offset == f.tell()) extdata_size_compressed = struct.unpack('I', f.read(4))[0] extdata_buffer_compressed = struct.unpack(f'{extdata_size_compressed}s', f.read(extdata_size_compressed))[0] extdata_buffer = zlib.decompress(extdata_buffer_compressed) @@ -213,12 +220,12 @@ def read_save(file_name): save_data['save_icon'] = struct.unpack_from(f'{save_icon_size}s', extdata_buffer, cur_ptr)[0] cur_ptr += save_icon_size - assert(cur_ptr == extdata_size_decompressed) + assert (cur_ptr == extdata_size_decompressed) # check EOF - assert(f.tell() == os.fstat(f.fileno()).st_size) + assert (f.tell() == os.fstat(f.fileno()).st_size) - return save_data + return save_data, s_db # Serialization @@ -310,7 +317,7 @@ def write_save(save_data, filename): strings = save_data['strings'] buffer = write_int8_16_32(len(strings), buffer) - for s in strings: + for s in strings.keys(): buffer = write_string(s, buffer, str_encoding) segments = save_data['segments'] @@ -321,7 +328,7 @@ def write_save(save_data, filename): variables = save_data['vars'] buffer = write_int8_16_32(len(variables), buffer) for varname in variables: - buffer = write_string(varname, buffer, str_encoding) + buffer = write_string(varname, buffer, str_encoding) buffer = write_variable(variables[varname], buffer, fileinfo_config) buffer_compressed = zlib.compress(buffer, zlib.Z_BEST_COMPRESSION) @@ -339,7 +346,7 @@ def write_save(save_data, filename): # compose all the data raw_data = bytearray() raw_data += struct.pack('32s', file_info.encode(str_encoding) + b'\x00') - raw_data += struct.pack('I', struct.calcsize('32sIIII') + len(buffer_compressed)) # extdata_offset + raw_data += struct.pack('I', struct.calcsize('32sIIII') + len(buffer_compressed)) # extdata_offset raw_data += struct.pack('I', len(extdata_buffer)) raw_data += struct.pack('I', len(buffer)) raw_data += struct.pack('I', len(buffer_compressed)) @@ -353,9 +360,9 @@ def write_save(save_data, filename): # Conversion -def convert_107_to_173(save_data): - if (save_data['file_info'] == 'ver 1.7.3'): - return save_data +def convert_107_to_173(save_data, s_db): + if save_data['file_info'] == 'ver 1.7.3': + return save_data, s_db save_data['file_info'] = 'ver 1.7.3' @@ -547,28 +554,57 @@ def convert_107_to_173(save_data): 'blood': 27, 'rain_drops': 28 } + for name, var in save_data['vars'].items(): if name in to_int: - assert(var['type'] == VarType.String) + assert (var['type'] == VarType.String) var['type'] = VarType.Integer - assert(len(var['values']) == 1) + assert (len(var['values']) == 1) strval = var['values'][0] var['values'][0] = layers[strval] if var['type'] == VarType.Object: for val in var['values']: - val['id'] = (sum(val['id']),) # one item tuple + val['id'] = (sum(val['id']),) # one item tuple + + # cleanup strings + used_str = {} + + def visit_attribute(hierarchy, name, a): + used_str[name] = used_str[name] + 1 if name in used_str else 1 + if not name.isascii(): + logging.warning(f'non ascii attribute name "{name}" in {".".join(hierarchy)}') + if 'attributes' in a: + for attr_name, attr in a['attributes'].items(): + visit_attribute([*hierarchy, name], attr_name, attr) + + for name, var in save_data['vars'].items(): + if var['type'] == VarType.Object: + for i, val in enumerate(var['values']): + root_attr_name, root_attr = next(iter(val['attributes'].items())) + visit_attribute([f'{name}[{i}]'], root_attr_name, root_attr) + + # s_db = str_db.remove_unused(s_db, used_str, 'cp1251') + s_db = str_db.create_db(used_str.keys(), 'utf-8') + save_data['strings'] = {} + for s in used_str.keys(): + save_data['strings'][s] = str_db.get_int(s_db, s, 'utf-8') + + # assign new name codes to attributes + def fix_attribute(name, a): + a['name_code'] = str_db.get_int(s_db, name, 'utf-8') + if 'attributes' in a: + for attr_name, attr in a['attributes'].items(): + fix_attribute(attr_name, attr) - # itemModels array should have ITEMS_QUANTITY elements (see program/items/items.h) - #if name == 'itemModels': - # assert(var['type'] == VarType.Object) - # assert(len(var['values']) == 498) - # elem_copy = dict(var['values'][-1]) - # for _ in range(500-498): - # var['values'].append(elem_copy) + for name, var in save_data['vars'].items(): + if var['type'] == VarType.Object: + for val in var['values']: + root_attr_name, root_attr = next(iter(val['attributes'].items())) + fix_attribute(root_attr_name, root_attr) - return save_data + return save_data, s_db def dump_save(save_data, filename): @@ -580,15 +616,6 @@ def fallback(v): import json with open(filename, 'w', encoding='utf-8') as f: copy = dict(save_data) - #copy['strings'] = sorted(save_data['strings']) - #copy['vars'] = sorted(save_data['vars'], key=lambda k: k['name']) - - #if var['type'] == VarType.Object: - # for v in var['values']: - # for a in v['attributes']: - # a['name'] = str_db.get_str(s_db, a['name_code']) - - json.dump(copy, f, ensure_ascii=False, indent=4, default=fallback) @@ -598,16 +625,16 @@ def fallback(v): args = parser.parse_args() input_file = os.path.abspath(args.input_file) - save_data = read_save(input_file) + save_data, s_db = read_save(input_file) filename, file_extension = os.path.splitext(input_file) - dump_save(save_data, f'{filename}_read.json') + # dump_save(save_data, f'{filename}_read.json') - save_data = convert_107_to_173(save_data) + save_data, s_db = convert_107_to_173(save_data, s_db) filename += '_173' - dump_save(save_data, f'{filename}_write.json') + # dump_save(save_data, f'{filename}_write.json') output_file = filename + file_extension write_save(save_data, output_file) diff --git a/tools/gamesave-convert/str_db.py b/tools/gamesave-convert/str_db.py index d60937207..fb3d7c985 100644 --- a/tools/gamesave-convert/str_db.py +++ b/tools/gamesave-convert/str_db.py @@ -1,10 +1,14 @@ +import logging + HASH_TABLE_SIZE = 512 -def hash(str): +def hash(s, str_encoding): h = 0 - str = str.lower() - for c in str: - h = (h << 4) + ord(c) + bin = s.encode(str_encoding) + for b in bin: + if ord('A') <= b and b <= ord('Z'): + b += ord('a') - ord('A') + h = (h << 4) + b g = h & 0xf0000000 if g != 0: h ^= g >> 24 @@ -13,29 +17,49 @@ def hash(str): return h -def get_int(db, str): - h = hash(str) +def get_int(db, s, str_encoding): + h = hash(s, str_encoding) row_id = h % HASH_TABLE_SIZE - elem_id = db[row_id].index(str) + if row_id >= HASH_TABLE_SIZE or s not in db[row_id]: + logging.warning(f"couldn't find string {s} in the string database") + return -1 + elem_id = db[row_id].index(s) return row_id << 16 | elem_id - + def get_str(db, i): row_id = i >> 16 elem_id = i & 0xffff if row_id >= HASH_TABLE_SIZE or elem_id >= len(db[row_id]): + logging.warning(f"couldn't find name code {i} in the string database") return f'str_db[{hex(i)}]' - return db[row_id][elem_id] + entry = db[row_id][elem_id] + return entry - -def create_db(str_list): +def create_db(str_list, str_encoding): db = [] for i in range(HASH_TABLE_SIZE): db.append([]) for str in str_list: - h = hash(str) + h = hash(str, str_encoding) row_id = h % HASH_TABLE_SIZE db[row_id].append(str) return db + + +def remove_unused(db, used_str, str_encoding): + unused_str = [] + for row in db: + for elem in row: + if elem not in used_str: + unused_str.append(elem) + + for s in unused_str: + h = hash(s, str_encoding) + row_id = h % HASH_TABLE_SIZE + elem_id = db[row_id].index(s) + del db[row_id][elem_id] + + return db \ No newline at end of file From d941556619ad1f2fb1cf89d38d9cfabb48e819d4 Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Wed, 1 Sep 2021 21:59:51 +0200 Subject: [PATCH 08/20] Implement seasave deserialization --- tools/gamesave-convert/gamesave_convert.py | 7 +- tools/gamesave-convert/seasave.py | 600 +++++++++++++++++++-- 2 files changed, 574 insertions(+), 33 deletions(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index 7dbbeed96..b66b86d53 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -196,9 +196,10 @@ def read_save(file_name): var, cur_ptr = read_variable(buffer, cur_ptr, s_db, str_encoding, obj_id_format) vars[name] = var - # if 'oSeaSave' in vars: - # seasave_data = vars['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] - # seasave.read_seasave(bytes.fromhex(seasave_data[8:])) + if 'oSeaSave' in vars: + seasave_data = vars['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] + data = seasave.read_seasave(bytes.fromhex(seasave_data[8:])) + vars['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] = data save_data['vars'] = vars diff --git a/tools/gamesave-convert/seasave.py b/tools/gamesave-convert/seasave.py index dfb05ac89..65800a1c9 100644 --- a/tools/gamesave-convert/seasave.py +++ b/tools/gamesave-convert/seasave.py @@ -1,5 +1,6 @@ import struct + def read_buffer(buffer, cur_ptr): size = struct.unpack_from('I', buffer, cur_ptr)[0] cur_ptr += 4 @@ -31,19 +32,160 @@ def read_attr_ptr(buffer, cur_ptr): def read_aiballs(buffer, cur_ptr): - return + num_ball_types = 4 # hardcode + ball_types = [] + for _ in range(num_ball_types): + size = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + balls = [] + for _ in range(size): + first_pos = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 3 * 4 + pos = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 3 * 4 + particle_ptr = struct.unpack_from('p', buffer, cur_ptr)[0] + cur_ptr += 8 + ball_event, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + + format = 'IffffffffffI' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + + ball_params = { + 'first_pos': first_pos, + 'pos': pos, + 'particle_ptr': particle_ptr, + 'ball_event': ball_event, + 'ball_owner': data[0], + 'time': data[1], + 'speedV0': data[2], + 'dir_x': data[3], + 'dir_z': data[4], + 'sin_angle': data[5], + 'cos_angle': data[6], + 'height_multiply': data[7], + 'size_multiply': data[8], + 'time_speed_multiply': data[9], + 'max_fire_distance': data[10], + 'cannon_type': data[11] + } + assert (len(data) == 12) + + balls.append(ball_params) + + ball_types.append(balls) + + aiballs = {'ball_types': ball_types} + + return aiballs, cur_ptr + + +def read_aicannon(buffer, cur_ptr): + fmt = '3f3ffff3fIfIIIII' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) + + cannon = { + 'pos': {'x': data[0], 'y': data[1], 'z': data[2]}, + 'dir': {'x': data[3], 'y': data[4], 'z': data[5]}, + 'time2action': data[6], + 'total_time2action': data[7], + 'speedV0': data[8], + 'enemy_pos': {'x': data[9], 'y': data[10], 'z': data[11]}, + 'empty': data[12], + 'damaged': data[13], + 'fired': data[14], + 'ready2fire': data[15], + 'recharged': data[16], + 'load': data[17], + 'can_recharge': data[18] + } + assert (len(data) == 19) + return cannon, cur_ptr -def read_aicannon(): - return +def read_aifort(buffer, cur_ptr): + min_cannon_damage_distance = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 -def read_aifort(): - return + num_forts = 4 # hardcode + forts = [] + for _ in range(num_forts): + pos_data = struct.unpack_from('3f', buffer, cur_ptr) + pos = {'x': pos_data[0], 'y': pos_data[1], 'z': pos_data[2]} + cur_ptr += 3*4 + + num_cannons = 4 # hardcode + cannons = [] + for _ in range(num_cannons): + cannon, cur_ptr = read_aicannon(buffer, cur_ptr) + cannons.append(cannon) + + num_culevrins = 4 # hardcode + culevrins = [] + for _ in range(num_culevrins): + culevrin, cur_ptr = read_aicannon(buffer, cur_ptr) + culevrins.append(culevrin) + + num_mortars = 4 # hardcode + mortars = [] + for _ in range(num_cannons): + mortar, cur_ptr = read_aicannon(buffer, cur_ptr) + mortars.append(mortar) + + fort = { + 'pos': pos, + 'cannons': cannons, + 'culevrins': culevrins, + 'mortars': mortars, + } + forts.append(fort) + + aifort = { + 'min_cannon_damage_distance': min_cannon_damage_distance, + 'forts': forts, + } + return aifort, cur_ptr + + +def read_aigroup(buffer, cur_ptr): + commander, cur_ptr = read_attr_ptr(buffer, cur_ptr) + group_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + command, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + command_group, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + location_near_other_group, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + group_type, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + init_group_pos_data = struct.unpack_from('3f', buffer, cur_ptr) + init_group_pos = {'x': init_group_pos_data[0], 'y': init_group_pos_data[1], 'z': init_group_pos_data[2]} + cur_ptr += 3 * 4 + move_point_data = struct.unpack_from('3f', buffer, cur_ptr) + move_point = {'x': move_point_data[0], 'y': move_point_data[1], 'z': move_point_data[2]} + cur_ptr += 3 * 4 + first_execute = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + num_ships = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + ships = [] + for _ in range(num_ships): + ship, cur_ptr = read_aiship(buffer, cur_ptr) + ships.append(ship) + + aigroup = { + 'commander': commander, + 'group_name': group_name, + 'command': command, + 'command_group': command_group, + 'location_near_other_group': location_near_other_group, + 'group_type': group_type, + 'init_group_pos': init_group_pos, + 'move_point': move_point, + 'first_execute': first_execute, + 'ships': ships, + } -def read_aigroup(): - return + return aigroup, cur_ptr def read_aihelper(buffer, cur_ptr): @@ -79,40 +221,401 @@ def read_aihelper(buffer, cur_ptr): return aihelper, cur_ptr -def read_ship(): - return +def read_ship(buffer, cur_ptr): + character, cur_ptr = read_attr_ptr(buffer, cur_ptr) + ship_attr_ptr, cur_ptr = read_attr_ptr(buffer, cur_ptr) + realize_layer, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + execute_layer, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + ship_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + _ = struct.unpack_from('I', buffer, cur_ptr)[0] # skip + cur_ptr += 4 + gravity = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + sail_state = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + uni_idx = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + use = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + ship_points, cur_ptr = read_buffer(buffer, cur_ptr) + speed_accel_data = struct.unpack_from('3f', buffer, cur_ptr) + speed_accel = {'x': speed_accel_data[0], 'y': speed_accel_data[1], 'z': speed_accel_data[2]} + cur_ptr += 3 * 4 + sp, cur_ptr = read_buffer(buffer, cur_ptr) + pos_data = struct.unpack_from('3f', buffer, cur_ptr) + pos = {'x': pos_data[0], 'y': pos_data[1], 'z': pos_data[2]} + cur_ptr += 3 * 4 + angle_data = struct.unpack_from('3f', buffer, cur_ptr) + angle = {'x': angle_data[0], 'y': angle_data[1], 'z': angle_data[2]} + cur_ptr += 3 * 4 + water_line = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + is_dead = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + is_visible = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + dead_dir_data = struct.unpack_from('3f', buffer, cur_ptr) + dead_dir = {'x': dead_dir_data[0], 'y': dead_dir_data[1], 'z': dead_dir_data[2]} + cur_ptr += 3 * 4 + cur_dead_dir_data = struct.unpack_from('3f', buffer, cur_ptr) + cur_dead_dir = {'x': cur_dead_dir_data[0], 'y': cur_dead_dir_data[1], 'z': cur_dead_dir_data[2]} + cur_ptr += 3 * 4 + keel_contour, cur_ptr = read_buffer(buffer, cur_ptr) + is_ship2strand = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + is_mounted = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + is_keel_contour = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + is_perk_turn_active = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + initial_perk_angle = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + result_perk_angle = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + strength, cur_ptr = read_buffer(buffer, cur_ptr) + is_set_light_and_fog = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + save_ambient = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + save_fog_color = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + save_light, cur_ptr = read_buffer(buffer, cur_ptr) + state, cur_ptr = read_buffer(buffer, cur_ptr) + + num_masts = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + masts = [] + for _ in range(num_masts): + fmt = '3f3fIIf' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) + + mast = { + 'v_src': {'x': data[0], 'y': data[1], 'z': data[2]}, + 'v_dst': {'x': data[3], 'y': data[4], 'z': data[5]}, + 'mast_num': data[6], + 'is_broken': data[7], + 'damage': data[8] + } + assert (len(data) == 9) + masts.append(mast) + + num_hulls = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + hulls = [] + for _ in range(num_hulls): + fmt = '3f3fIIf' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) + + hull = { + 'v_src': {'x': data[0], 'y': data[1], 'z': data[2]}, + 'v_dst': {'x': data[3], 'y': data[4], 'z': data[5]}, + 'hull_num': data[6], + 'is_broken': data[7], + 'damage': data[8] + } + assert (len(data) == 9) + hulls.append(hull) + + num_fireplaces = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + fireplaces = [] + for _ in range(num_fireplaces): + fireplace, cur_ptr = read_fireplace(buffer, cur_ptr) + fireplaces.append(fireplace) + + x_heel = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + z_heel = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + + ship = { + 'character': character, + 'ship_attr_ptr': ship_attr_ptr, + 'realize_layer': realize_layer, + 'execute_layer': execute_layer, + 'ship_name': ship_name, + 'gravity': gravity, + 'sail_state': sail_state, + 'uni_idx': uni_idx, + 'use': use, + 'ship_points': ship_points, + 'speed_accel': speed_accel, + 'sp': sp, + 'pos': pos, + 'angle': angle, + 'water_line': water_line, + 'is_dead': is_dead, + 'is_visible': is_visible, + 'dead_dir': dead_dir, + 'cur_dead_dir': cur_dead_dir, + 'keel_contour': keel_contour, + 'is_ship2strand': is_ship2strand, + 'is_mounted': is_mounted, + 'is_keel_contour': is_keel_contour, + 'is_perk_turn_active': is_perk_turn_active, + 'initial_perk_angle': initial_perk_angle, + 'result_perk_angle': result_perk_angle, + 'is_set_light_and_fog': is_set_light_and_fog, + 'save_ambient': save_ambient, + 'save_fog_color': save_fog_color, + 'save_light': save_light, + 'state': state, + 'masts': masts, + 'hulls': hulls, + 'fireplaces': fireplaces, + 'x_heel': x_heel, + 'z_heel': z_heel + } + + return ship, cur_ptr + + +def read_aiship(buffer, cur_ptr): + base_ship, cur_ptr = read_ship(buffer, cur_ptr) + + character, cur_ptr = read_attr_ptr(buffer, cur_ptr) + obj_type = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + is_dead = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + + cannon_ctrl, cur_ptr = read_aiship_cannon_controller(buffer, cur_ptr) + + has_cam_ctrl = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + if has_cam_ctrl: + cam_ctrl, cur_ptr = read_aiship_camera_controller(buffer, cur_ptr) + else: + cam_ctrl = None + + move_ctrl, cur_ptr = read_aiship_move_controller(buffer, cur_ptr) + rotate_ctrl, cur_ptr = read_aiship_rotate_controller(buffer, cur_ptr) + speed_ctrl, cur_ptr = read_aiship_speed_controller(buffer, cur_ptr) + task_ctrl, cur_ptr = read_aiship_task_controller(buffer, cur_ptr) + touch_ctrl, cur_ptr = read_aiship_touch_controller(buffer, cur_ptr) + + aiship = { + 'base_ship': base_ship, + 'character': character, + 'obj_type': obj_type, + 'is_dead': is_dead, + 'cannon_ctrl': cannon_ctrl, + 'cam_ctrl': cam_ctrl, + 'move_ctrl': move_ctrl, + 'rotate_ctrl': rotate_ctrl, + 'speed_ctrl': speed_ctrl, + 'task_ctrl': task_ctrl, + 'touch_ctrl': touch_ctrl, + } + return aiship, cur_ptr -def read_aiship(): - return +def read_aiship_camera_controller(buffer, cur_ptr): + fmt = 'IfI' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) -def read_aiship_camera_controller(): - return + controller = { + 'target': data[0], + 'delta': data[1], + 'cam_outside': data[2] + } + assert (len(data) == 3) + return controller, cur_ptr -def read_aiship_cannon_controller(): - return +def read_aiship_cannon_controller(buffer, cur_ptr): + fmt = '3I' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) -def read_aiship_move_controller(): - return + controller = { + 'reload': data[0], + 'not_enough_balls': data[1], + 'temp_flag': data[2] + } + assert (len(data) == 3) + num_borts = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + borts = [] + for _ in range(num_borts): + name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + + fmt = '7fIff3f' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) + + bort = { + 'fire_zone': data[0], + 'fire_angle_min': data[1], + 'fire_angle_max': data[2], + 'fire_dir': data[3], + 'fire_heigh': data[4], + 'charge_percent': data[5], + 'cos_fire_zone': data[6], + 'num_cannons': data[7], + 'speedV0': data[8], + 'max_fire_distance': data[9], + 'direction': {'x': data[10], 'y': data[11], 'z': data[12]} + } + assert (len(data) == 13) + + num_cannons = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + cannons = [] + for _ in range(num_cannons): + cannon, cur_ptr = read_aicannon(buffer, cur_ptr) + cannons.append(cannon) + + bort['cannons'] = cannons + borts.append(bort) + + controller['borts'] = borts + + return controller, cur_ptr + + +def read_aiship_move_controller(buffer, cur_ptr): + fmt = 'I3f3f3ffI' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) + + controller = { + 'stopped': data[0], + 'dest_point': {'x': data[1], 'y': data[2], 'z': data[3]}, + 'braking_force': {'x': data[4], 'y': data[5], 'z': data[6]}, + 'deviation_force': {'x': data[7], 'y': data[8], 'z': data[9]}, + 'move_time': data[10], + 'cur_point': data[11] + } + assert (len(data) == 12) -def read_aiship_rotate_controller(): - return + return controller, cur_ptr -def read_aiship_speed_controller(): - return +def read_aiship_rotate_controller(buffer, cur_ptr): + fmt = 'I5f' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) + controller = { + 'rotate_num': data[0], + 'rotate_mode': data[1], + 'rotate_time': data[2], + 'rotate_smooth': data[3], + 'rotate': data[4], + 'global_multiply': data[5] + } + assert (len(data) == 6) -def read_aiship_task_controller(): - return + return controller, cur_ptr -def read_aiship_touch_controller(): - return +def read_aiship_speed_controller(buffer, cur_ptr): + fmt = 'I5f' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) + + controller = { + 'speed_num': data[0], + 'speed_smooth': data[1], + 'speed': data[2], + 'speed_time': data[3], + 'top_speed': data[4], + 'global_multiply': data[5] + } + assert (len(data) == 6) + + return controller, cur_ptr + + +def read_aiship_task_controller(buffer, cur_ptr): + extra_distance = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + + # primary + active = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + task_type = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + task_character, cur_ptr = read_attr_ptr(buffer, cur_ptr) + fmt = '3f' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) + task_point = {'x': data[0], 'y': data[1], 'z': data[2]} + assert (len(data) == 3) + primary_task = { + 'active': active, + 'task_type': task_type, + 'task_character': task_character, + 'task_point': task_point + } + + # secondary + active = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + task_type = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + task_character, cur_ptr = read_attr_ptr(buffer, cur_ptr) + fmt = '3f' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) + task_point = {'x': data[0], 'y': data[1], 'z': data[2]} + assert (len(data) == 3) + secondary_task = { + 'active': active, + 'task_type': task_type, + 'task_character': task_character, + 'task_point': task_point + } + + controller = { + 'extra_distance': extra_distance, + 'primary_task': primary_task, + 'secondary_task': secondary_task + } + + return controller, cur_ptr + + +def read_aiship_touch_controller(buffer, cur_ptr): + num_rays = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + rays = [] + for _ in range(num_rays): + fmt = '3f' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) + ray = {'x': data[0], 'y': data[1], 'z': data[2]} + assert (len(data) == 3) + rays.append(ray) + + fmt = '5f15f' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) + + controller = { + 'rays': rays, + 'left_rays_free': data[0], + 'right_rays_free': data[1], + 'ray_size': data[2], + 'speed_factor': data[3], + 'rotate_factor': data[4], + 'box1': {'x': data[5], 'y': data[6], 'z': data[7]}, + 'box2': {'x': data[8], 'y': data[9], 'z': data[10]}, + 'box3': {'x': data[11], 'y': data[12], 'z': data[13]}, + 'box4': {'x': data[14], 'y': data[15], 'z': data[16]}, + 'box5': {'x': data[17], 'y': data[18], 'z': data[19]} + } + assert (len(data) == 20) + + return controller, cur_ptr def read_deck_cam(buffer, cur_ptr): @@ -224,13 +727,27 @@ def read_ship_cam(buffer, cur_ptr): return ship_cam, cur_ptr -def read_fireplace(): - return +def read_fireplace(buffer, cur_ptr): + format = '3fIfI' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + fireplace = { + 'orig_pos': {'x': data[0], 'y': data[1], 'z': data[2]}, + 'active': data[3], + 'run_time': data[4], + 'ball_character_index': data[5] + } + assert (len(data) == 6) -def read_seasave(buffer): - seasave_data = {} + particle_smoke_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + particle_fire_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + sound_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + + return fireplace, cur_ptr + +def read_seasave(buffer): cur_ptr = 0 aihelper, cur_ptr = read_aihelper(buffer, cur_ptr) @@ -238,8 +755,31 @@ def read_seasave(buffer): free_cam, cur_ptr = read_free_cam(buffer, cur_ptr) deck_cam, cur_ptr = read_deck_cam(buffer, cur_ptr) - aiballs, curptr = read_aiballs(buffer, cur_ptr) + aiballs, cur_ptr = read_aiballs(buffer, cur_ptr) + + num_groups = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + aigroups = [] + for _ in range(num_groups): + group, cur_ptr = read_aigroup(buffer, cur_ptr) + aigroups.append(group) + + if len(buffer) > cur_ptr: + aifort, cur_ptr = read_aifort(buffer, cur_ptr) + else: + aifort = None + + seasave = { + 'aihelper': aihelper, + 'ship_cam': ship_cam, + 'free_cam': free_cam, + 'deck_cam': deck_cam, + 'aiballs': aiballs, + 'aigroups': aigroups, + 'aifort': aifort + } + return seasave def write_seasave(): return \ No newline at end of file From a7d8c3c8b26d6bbc66f10c69afa98f0dc3c8b382 Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Wed, 1 Sep 2021 22:01:00 +0200 Subject: [PATCH 09/20] Add seasave serialization stubs --- tools/gamesave-convert/seasave.py | 449 +++++++++++++++++++++++++++++- 1 file changed, 447 insertions(+), 2 deletions(-) diff --git a/tools/gamesave-convert/seasave.py b/tools/gamesave-convert/seasave.py index 65800a1c9..925e9432f 100644 --- a/tools/gamesave-convert/seasave.py +++ b/tools/gamesave-convert/seasave.py @@ -781,5 +781,450 @@ def read_seasave(buffer): return seasave -def write_seasave(): - return \ No newline at end of file + +# Serialization + +def write_buffer(buf, buffer): + size = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + buf = struct.unpack_from(f'{size}s', buffer, cur_ptr)[0] + cur_ptr += size + return buffer + + +def write_string(s, buffer, encoding): + str_len = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + if str_len == 0: + return '', cur_ptr + s = struct.unpack_from(f'{str_len - 1}s', buffer, cur_ptr)[0] # str_len-1 to skip trailing '\0' + s = s.decode(encoding) + cur_ptr += str_len + return buffer + + +def write_attr_ptr(attr_ptr, buffer): + index = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + s, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + attr_ptr = { + 'index': index, + 'str': s + } + return buffer + + +def write_aiballs(aiballs, buffer): + num_ball_types = 4 + for _ in range(num_ball_types): + size = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + for _ in range(size): + first_pos = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 3 * 4 + pos = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 3 * 4 + particle_ptr = struct.unpack_from('p', buffer, cur_ptr)[0] + cur_ptr += 8 + ball_event, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + + format = 'IffffffffffI' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + + ball_params = { + 'first_pos': first_pos, + 'pos': pos, + 'particle_ptr': particle_ptr, + 'ball_event': ball_event, + 'ball_owner': data[0], + 'time': data[1], + 'speedV0': data[2], + 'dir_x': data[3], + 'dir_z': data[4], + 'sin_angle': data[5], + 'cos_angle': data[6], + 'height_multiply': data[7], + 'size_multiply': data[8], + 'time_speed_multiply': data[9], + 'max_fire_distance': data[10], + 'cannon_type': data[11] + } + assert (len(data) == 12) + + return buffer + + +def write_aicannon(aicannon, buffer): + format = '3f3ffff3fIfIIIII' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + + cannon = { + 'pos': data[0], + 'dir': data[1], + 'time2action': data[2], + 'total_time2action': data[3], + 'speedV0': data[4], + 'enemy_pos': data[5], + 'empty': data[6], + 'damaged': data[7], + 'fired': data[8], + 'ready2fire': data[9], + 'recharged': data[10], + 'load': data[11], + 'can_recharge': data[12] + } + assert (len(data) == 13) + return buffer + + +def write_aifort(aifort, buffer): + min_cannon_damage_distance = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + num_forts = 4 + for _ in range(num_forts): + pos = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 4 + num_cannons = 4 + for _ in range(num_cannons): + cannon, cur_ptr = read_aicannon(buffer, cur_ptr) + num_culevrins = 4 + for _ in range(num_culevrins): + culevrin, cur_ptr = read_aicannon(buffer, cur_ptr) + num_mortars = 4 + for _ in range(num_cannons): + mortar, cur_ptr = read_aicannon(buffer, cur_ptr) + return buffer + + +def write_ship(ship, buffer): + character, cur_ptr = read_attr_ptr(buffer, cur_ptr) + ship, cur_ptr = read_attr_ptr(buffer, cur_ptr) + realize_layer, cur_ptr = read_string(buffer, cur_ptr) + execute_layer, cur_ptr = read_string(buffer, cur_ptr) + ship_name, cur_ptr = read_string(buffer, cur_ptr) + _ = struct.unpack_from('I', buffer, cur_ptr)[0] # skip + cur_ptr += 4 + gravity = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + sail_state = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + uni_idx = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + use = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + ship_points, cur_ptr = read_buffer(buffer, cur_ptr) + speed_accel = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 3 * 4 + sp, cur_ptr = read_buffer(buffer, cur_ptr) + pos = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 3 * 4 + angle = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 3 * 4 + water_line = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + is_dead = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + is_visible = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + dead_dir = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 3 * 4 + cur_dead_dir = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 3 * 4 + keel_contour, cur_ptr = read_buffer(buffer, cur_ptr) + is_ship2strand = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + is_mounted = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + is_keel_contour = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + is_perk_turn_active = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + initial_perk_angle = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + result_perk_angle = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + strength, cur_ptr = read_buffer(buffer, cur_ptr) + is_set_light_and_fog = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + save_ambient = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + save_fog_color = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + save_light, cur_ptr = read_buffer(buffer, cur_ptr) + state, cur_ptr = read_buffer(buffer, cur_ptr) + + num_masts = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + masts = [] + for _ in range(num_masts): + format = '3f3fIIf' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + + mast = { + 'v_src': {'x': data[0], 'y': data[1], 'z': data[2]}, + 'v_dst': {'x': data[3], 'y': data[4], 'z': data[5]}, + 'mast_num': data[6], + 'is_broken': data[7], + 'damage': data[8] + } + assert (len(data) == 9) + + num_hulls = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + hulls = [] + for _ in range(num_hulls): + format = '3f3fIIf' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + + hull = { + 'v_src': {'x': data[0], 'y': data[1], 'z': data[2]}, + 'v_dst': {'x': data[3], 'y': data[4], 'z': data[5]}, + 'hull_num': data[6], + 'is_broken': data[7], + 'damage': data[8] + } + assert (len(data) == 9) + + num_fireplaces = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + fireplaces = [] + for _ in range(num_fireplaces): + fireplace, cur_ptr = read_fireplace(buffer, cur_ptr) + + x_heel = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + z_heel = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + + return buffer + + +def write_aiship(aiship, buffer): + ship, cur_ptr = read_ship(buffer, cur_ptr) + + character, cur_ptr = read_attr_ptr(buffer, cur_ptr) + obj_type = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + is_dead = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + + cannon_ctrl, cur_ptr = read_aiship_cannon_controller(buffer, cur_ptr) + + has_cam_ctrl = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + if has_cam_ctrl: + cam_ctrl, cur_ptr = read_aiship_camera_controller(buffer, cur_ptr) + + move_ctrl, cur_ptr = read_aiship_move_controller(buffer, cur_ptr) + rotate_ctrl, cur_ptr = read_aiship_rotate_controller(buffer, cur_ptr) + speed_ctrl, cur_ptr = read_aiship_speed_controller(buffer, cur_ptr) + task_ctrl, cur_ptr = read_aiship_task_controller(buffer, cur_ptr) + touch_ctrl, cur_ptr = read_aiship_touch_controller(buffer, cur_ptr) + + return buffer + + +def write_aiship_camera_controller(controller, buffer): + return buffer + + +def write_aiship_cannon_controller(controller, buffer): + return buffer + + +def write_aiship_move_controller(controller, buffer): + return buffer + + +def write_aiship_rotate_controller(controller, buffer): + return buffer + + +def write_aiship_speed_controller(controller, buffer): + return buffer + + +def write_aiship_task_controller(controller, buffer): + return buffer + + +def write_aiship_touch_controller(controller, buffer): + return buffer + + +def write_aigroup(aigroup, buffer): + commander, cur_ptr = read_attr_ptr(buffer, cur_ptr) + group_name, cur_ptr = read_string(buffer, cur_ptr) + command, cur_ptr = read_string(buffer, cur_ptr) + command_group, cur_ptr = read_string(buffer, cur_ptr) + location_near_other_group, cur_ptr = read_string(buffer, cur_ptr) + group_type, cur_ptr = read_string(buffer, cur_ptr) + init_group_pos = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 3 * 4 + move_point = struct.unpack_from('3f', buffer, cur_ptr)[0] + cur_ptr += 3 * 4 + first_execute = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + num_ships = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + for _ in range(num_ships): + ship, cur_ptr = read_ship(buffer, cur_ptr) + + return buffer + + +def write_aihelper(aihelper, buffer): + gravity = struct.unpack_from('f', buffer, cur_ptr)[0] + cur_ptr += 4 + num_relations = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + relations, cur_ptr = read_buffer(buffer, cur_ptr) + sea_cameras, cur_ptr = read_attr_ptr(buffer, cur_ptr) + + num_characters = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + characters = [] + for _ in range(num_characters): + char_ptr, cur_ptr = read_attr_ptr(buffer, cur_ptr) + characters.append(char_ptr) + + num_main_characters = struct.unpack_from('I', buffer, cur_ptr)[0] + cur_ptr += 4 + main_characters = [] + for _ in range(num_main_characters): + char_ptr, cur_ptr = read_attr_ptr(buffer, cur_ptr) + main_characters.append(char_ptr) + + aihelper = { + 'gravity': gravity, + 'num_relations': num_relations, + 'relations': relations, + 'sea_cameras': sea_cameras, + 'characters': characters, + 'main_characters': main_characters + } + return buffer + + +def write_deck_cam(deck_cam, buffer): + vertices, cur_ptr = read_buffer(buffer, cur_ptr) + + format = '13f3f3f3f3f3ff' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + + deck_cam = { + 'distance_sensivity': data[0], + 'height_angle_sensivity': data[1], + 'azimuth_angle_sensivity': data[2], + 'rocking_x': data[3], + 'rocking_z': data[4], + 'men_step_up': data[5], + 'men_step_min': data[6], + 'height_max': data[7], + 'height_min': data[8], + 'height_step': data[9], + 'cam_max_x': data[10], + 'cam_min_x': data[11], + 'default_height': data[12], + 'g_vec1': {'x': data[13], 'y': data[14], 'z': data[15]}, + 'g_vec2': {'x': data[16], 'y': data[17], 'z': data[18]}, + 'g_vec3': {'x': data[19], 'y': data[20], 'z': data[21]}, + 'cam_pos': {'x': data[22], 'y': data[23], 'z': data[24]}, + 'cam_angle': {'x': data[25], 'y': data[26], 'z': data[27]}, + 'eye_height': data[28] + } + assert (len(data) == 29) + + deck_cam['screen_rect'], cur_ptr = read_buffer(buffer, cur_ptr) + + format = '4If' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + deck_cam['lock_x'] = data[0] + deck_cam['lock_y'] = data[1] + deck_cam['is_on'] = data[2] + deck_cam['is_active'] = data[3] + deck_cam['perspective'] = data[4] + assert (len(data) == 5) + + deck_cam['character'], cur_ptr = read_attr_ptr(buffer, cur_ptr) + + return buffer + + +def write_free_cam(free_cam, buffer): + format = '3f3ffIIIf' + data = struct.unpack_from(format, buffer, cur_ptr) + cur_ptr += struct.calcsize(format) + + free_cam = { + 'pos': {'x': data[0], 'y': data[1], 'z': data[2]}, + 'angle': {'x': data[3], 'y': data[4], 'z': data[5]}, + 'fov': data[6], + 'lock_x': data[7], + 'lock_y': data[8], + 'is_onland': data[9], + 'cam_onland_height': data[10] + } + assert (len(data) == 11) + + return buffer + + +def write_ship_cam(ship_cam, buffer): + buffer += struct.pack('I', ship_cam['lock_x']) + buffer += struct.pack('I', ship_cam['lock_y']) + buffer += struct.pack('f', ship_cam['min_height_on_sea']) + buffer += struct.pack('f', ship_cam['max_height_on_sea']) + buffer += struct.pack('f', ship_cam['distance']) + buffer += struct.pack('f', ship_cam['max_distance']) + buffer += struct.pack('f', ship_cam['min_distance']) + buffer += struct.pack('f', ship_cam['distance_delta']) + buffer += struct.pack('f', ship_cam['distance_inertia']) + buffer += struct.pack('f', ship_cam['min_angle_x']) + buffer += struct.pack('f', ship_cam['max_angle_x']) + buffer += struct.pack('f', ship_cam['angle_x_delta']) + buffer += struct.pack('f', ship_cam['angle_x_inertia']) + buffer += struct.pack('f', ship_cam['angle_y_delta']) + buffer += struct.pack('f', ship_cam['angle_y_inertia']) + buffer += struct.pack('f', ship_cam['distance_sensivity']) + buffer += struct.pack('f', ship_cam['azimuth_angle_sensivity']) + buffer += struct.pack('f', ship_cam['height_angle_sensivity']) + buffer += struct.pack('f', ship_cam['invert_mouse_x']) + buffer += struct.pack('f', ship_cam['invert_mouse_y']) + buffer += struct.pack('3f', ship_cam['center']['x'], ship_cam['center']['y'], ship_cam['center']['z']) + buffer += struct.pack('3f', ship_cam['angle']['x'], ship_cam['angle']['y'], ship_cam['angle']['z']) + buffer += struct.pack('f', ship_cam['model_atan_y']) + buffer += struct.pack('I', ship_cam['ship_code']) + buffer += struct.pack('I', ship_cam['num_islands']) + buffer += struct.pack('I', ship_cam['is_on']) + buffer += struct.pack('I', ship_cam['is_active']) + buffer += struct.pack('f', ship_cam['perspective']) + + ship_cam['character'], cur_ptr = read_attr_ptr(buffer, cur_ptr) + + return buffer + + +def write_fireplace(fireplace, buffer): + buffer += struct.pack('I', var['type'].value) + buffer += struct.pack('I', len(var['values'])) + + return buffer + + +def write_seasave(seasave, buffer): + buffer = write_aihelper(aihelper, buffer) + + buffer = write_ship_cam(ship_cam, buffer) + buffer = write_free_cam(free_cam, buffer) + buffer = write_deck_cam(deck_cam, buffer) + + buffer = write_aiballs(aiballs, buffer) + + return buffer From 73b553eafb1e2ee27c803d828c96a842cd40847d Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Sat, 4 Sep 2021 15:34:12 +0200 Subject: [PATCH 10/20] Implement seasave serialization and conversion --- tools/gamesave-convert/gamesave_convert.py | 12 + tools/gamesave-convert/seasave.py | 744 +++++++++++---------- 2 files changed, 401 insertions(+), 355 deletions(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index b66b86d53..2774fa043 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -328,6 +328,13 @@ def write_save(save_data, filename): variables = save_data['vars'] buffer = write_int8_16_32(len(variables), buffer) + if 'oSeaSave' in variables: + seasave_data = variables['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] + seasave_buf = bytes() + seasave_buf = seasave.write_seasave(seasave_data, seasave_buf).hex() + seasave_size = f'{len(seasave_buf):08x}' # as hexadecimal string without prefix 8 bytes long + variables['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] = seasave_size + seasave_buf + for varname in variables: buffer = write_string(varname, buffer, str_encoding) buffer = write_variable(variables[varname], buffer, fileinfo_config) @@ -569,6 +576,11 @@ def convert_107_to_173(save_data, s_db): for val in var['values']: val['id'] = (sum(val['id']),) # one item tuple + if name == 'oSeaSave': + seasave_data = var['values'][0]['attributes']['skip']['attributes']['save']['value'] + seasave_data = seasave.convert_107_to_173(seasave_data) + var['values'][0]['attributes']['skip']['attributes']['save']['value'] = seasave_data + # cleanup strings used_str = {} diff --git a/tools/gamesave-convert/seasave.py b/tools/gamesave-convert/seasave.py index 925e9432f..e26f454f7 100644 --- a/tools/gamesave-convert/seasave.py +++ b/tools/gamesave-convert/seasave.py @@ -13,19 +13,19 @@ def read_string(buffer, cur_ptr, encoding): str_len = struct.unpack_from('I', buffer, cur_ptr)[0] cur_ptr += 4 if str_len == 0: - return '', cur_ptr + return None, cur_ptr s = struct.unpack_from(f'{str_len-1}s', buffer, cur_ptr)[0] # str_len-1 to skip trailing '\0' s = s.decode(encoding) cur_ptr += str_len return s, cur_ptr -def read_attr_ptr(buffer, cur_ptr): +def read_attr_ptr(buffer, cur_ptr): index = struct.unpack_from('I', buffer, cur_ptr)[0] cur_ptr += 4 s, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') attr_ptr = { - 'index' : index, + 'index': index, 'str': s } return attr_ptr, cur_ptr @@ -47,9 +47,9 @@ def read_aiballs(buffer, cur_ptr): cur_ptr += 8 ball_event, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') - format = 'IffffffffffI' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) + fmt = 'IffffffffffI' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) ball_params = { 'first_pos': first_pos, @@ -358,6 +358,7 @@ def read_ship(buffer, cur_ptr): 'is_perk_turn_active': is_perk_turn_active, 'initial_perk_angle': initial_perk_angle, 'result_perk_angle': result_perk_angle, + 'strength': strength, 'is_set_light_and_fog': is_set_light_and_fog, 'save_ambient': save_ambient, 'save_fog_color': save_fog_color, @@ -452,6 +453,7 @@ def read_aiship_cannon_controller(buffer, cur_ptr): cur_ptr += struct.calcsize(fmt) bort = { + 'name': name, 'fire_zone': data[0], 'fire_angle_min': data[1], 'fire_angle_max': data[2], @@ -621,14 +623,15 @@ def read_aiship_touch_controller(buffer, cur_ptr): def read_deck_cam(buffer, cur_ptr): vertices, cur_ptr = read_buffer(buffer, cur_ptr) - format = '13f3f3f3f3f3ff' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) + fmt = '13f3f3f3f3f3ff' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) deck_cam = { - 'distance_sensivity': data[0], - 'height_angle_sensivity': data[1], - 'azimuth_angle_sensivity': data[2], + 'vertices': vertices, + 'distance_sensitivity': data[0], + 'height_angle_sensitivity': data[1], + 'azimuth_angle_sensitivity': data[2], 'rocking_x': data[3], 'rocking_z': data[4], 'men_step_up': data[5], @@ -650,9 +653,9 @@ def read_deck_cam(buffer, cur_ptr): deck_cam['screen_rect'], cur_ptr = read_buffer(buffer, cur_ptr) - format = '4If' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) + fmt = '4If' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) deck_cam['lock_x'] = data[0] deck_cam['lock_y'] = data[1] deck_cam['is_on'] = data[2] @@ -785,446 +788,477 @@ def read_seasave(buffer): # Serialization def write_buffer(buf, buffer): - size = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - buf = struct.unpack_from(f'{size}s', buffer, cur_ptr)[0] - cur_ptr += size + size = len(buf) + buffer += struct.pack('I', size) + buffer += struct.pack(f'{size}s', buf) return buffer -def write_string(s, buffer, encoding): - str_len = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - if str_len == 0: - return '', cur_ptr - s = struct.unpack_from(f'{str_len - 1}s', buffer, cur_ptr)[0] # str_len-1 to skip trailing '\0' - s = s.decode(encoding) - cur_ptr += str_len +def write_string(s, buffer): + if s is not None: + s = s.encode('utf-8') + b'\x00' + str_len = len(s) + buffer += struct.pack('I', str_len) + buffer += struct.pack(f'{str_len}s', s) + else: + buffer += b'\x00' return buffer def write_attr_ptr(attr_ptr, buffer): - index = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - s, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') - attr_ptr = { - 'index': index, - 'str': s - } + buffer += struct.pack('I', attr_ptr['index']) + buffer = write_string(attr_ptr['str'], buffer) return buffer def write_aiballs(aiballs, buffer): - num_ball_types = 4 - for _ in range(num_ball_types): - size = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - for _ in range(size): - first_pos = struct.unpack_from('3f', buffer, cur_ptr)[0] - cur_ptr += 3 * 4 - pos = struct.unpack_from('3f', buffer, cur_ptr)[0] - cur_ptr += 3 * 4 - particle_ptr = struct.unpack_from('p', buffer, cur_ptr)[0] - cur_ptr += 8 - ball_event, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') - - format = 'IffffffffffI' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) - - ball_params = { - 'first_pos': first_pos, - 'pos': pos, - 'particle_ptr': particle_ptr, - 'ball_event': ball_event, - 'ball_owner': data[0], - 'time': data[1], - 'speedV0': data[2], - 'dir_x': data[3], - 'dir_z': data[4], - 'sin_angle': data[5], - 'cos_angle': data[6], - 'height_multiply': data[7], - 'size_multiply': data[8], - 'time_speed_multiply': data[9], - 'max_fire_distance': data[10], - 'cannon_type': data[11] - } - assert (len(data) == 12) + for balls in aiballs['ball_types']: + buffer += struct.pack('I', len(balls)) + for ball in balls: + buffer += struct.pack('3f', ball['first_pos']['x'], ball['first_pos']['y'], ball['first_pos']['z']) + buffer += struct.pack('3f', ball['pos']['x'], ball['pos']['y'], ball['pos']['z']) + buffer += struct.pack('p', ball['particle_ptr']) + buffer = write_string(ball['ball_event'], buffer) + buffer += struct.pack('I', ball['ball_owner']) + buffer += struct.pack('f', ball['time']) + buffer += struct.pack('f', ball['speedV0']) + buffer += struct.pack('f', ball['dir_x']) + buffer += struct.pack('f', ball['dir_z']) + buffer += struct.pack('f', ball['sin_angle']) + buffer += struct.pack('f', ball['cos_angle']) + buffer += struct.pack('f', ball['height_multiply']) + buffer += struct.pack('f', ball['size_multiply']) + buffer += struct.pack('f', ball['time_speed_multiply']) + buffer += struct.pack('f', ball['max_fire_distance']) + buffer += struct.pack('I', ball['cannon_type']) return buffer def write_aicannon(aicannon, buffer): - format = '3f3ffff3fIfIIIII' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) + buffer += struct.pack('3f', aicannon['pos']['x'], aicannon['pos']['y'], aicannon['pos']['z']) + buffer += struct.pack('3f', aicannon['dir']['x'], aicannon['dir']['y'], aicannon['dir']['z']) + buffer += struct.pack('f', aicannon['time2action']) + buffer += struct.pack('f', aicannon['total_time2action']) + buffer += struct.pack('f', aicannon['speedV0']) + buffer += struct.pack('3f', aicannon['enemy_pos']['x'], aicannon['enemy_pos']['y'], aicannon['enemy_pos']['z']) + buffer += struct.pack('I', aicannon['empty']) + buffer += struct.pack('f', aicannon['damaged']) + buffer += struct.pack('I', aicannon['fired']) + buffer += struct.pack('I', aicannon['ready2fire']) + buffer += struct.pack('I', aicannon['recharged']) + buffer += struct.pack('I', aicannon['load']) + buffer += struct.pack('I', aicannon['can_recharge']) - cannon = { - 'pos': data[0], - 'dir': data[1], - 'time2action': data[2], - 'total_time2action': data[3], - 'speedV0': data[4], - 'enemy_pos': data[5], - 'empty': data[6], - 'damaged': data[7], - 'fired': data[8], - 'ready2fire': data[9], - 'recharged': data[10], - 'load': data[11], - 'can_recharge': data[12] - } - assert (len(data) == 13) return buffer def write_aifort(aifort, buffer): - min_cannon_damage_distance = struct.unpack_from('f', buffer, cur_ptr)[0] - cur_ptr += 4 - num_forts = 4 - for _ in range(num_forts): - pos = struct.unpack_from('3f', buffer, cur_ptr)[0] - cur_ptr += 4 - num_cannons = 4 - for _ in range(num_cannons): - cannon, cur_ptr = read_aicannon(buffer, cur_ptr) - num_culevrins = 4 - for _ in range(num_culevrins): - culevrin, cur_ptr = read_aicannon(buffer, cur_ptr) - num_mortars = 4 - for _ in range(num_cannons): - mortar, cur_ptr = read_aicannon(buffer, cur_ptr) - return buffer - - -def write_ship(ship, buffer): - character, cur_ptr = read_attr_ptr(buffer, cur_ptr) - ship, cur_ptr = read_attr_ptr(buffer, cur_ptr) - realize_layer, cur_ptr = read_string(buffer, cur_ptr) - execute_layer, cur_ptr = read_string(buffer, cur_ptr) - ship_name, cur_ptr = read_string(buffer, cur_ptr) - _ = struct.unpack_from('I', buffer, cur_ptr)[0] # skip - cur_ptr += 4 - gravity = struct.unpack_from('f', buffer, cur_ptr)[0] - cur_ptr += 4 - sail_state = struct.unpack_from('f', buffer, cur_ptr)[0] - cur_ptr += 4 - uni_idx = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - use = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - ship_points, cur_ptr = read_buffer(buffer, cur_ptr) - speed_accel = struct.unpack_from('3f', buffer, cur_ptr)[0] - cur_ptr += 3 * 4 - sp, cur_ptr = read_buffer(buffer, cur_ptr) - pos = struct.unpack_from('3f', buffer, cur_ptr)[0] - cur_ptr += 3 * 4 - angle = struct.unpack_from('3f', buffer, cur_ptr)[0] - cur_ptr += 3 * 4 - water_line = struct.unpack_from('f', buffer, cur_ptr)[0] - cur_ptr += 4 - is_dead = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - is_visible = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - dead_dir = struct.unpack_from('3f', buffer, cur_ptr)[0] - cur_ptr += 3 * 4 - cur_dead_dir = struct.unpack_from('3f', buffer, cur_ptr)[0] - cur_ptr += 3 * 4 - keel_contour, cur_ptr = read_buffer(buffer, cur_ptr) - is_ship2strand = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - is_mounted = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - is_keel_contour = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - is_perk_turn_active = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - initial_perk_angle = struct.unpack_from('f', buffer, cur_ptr)[0] - cur_ptr += 4 - result_perk_angle = struct.unpack_from('f', buffer, cur_ptr)[0] - cur_ptr += 4 - strength, cur_ptr = read_buffer(buffer, cur_ptr) - is_set_light_and_fog = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - save_ambient = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - save_fog_color = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - save_light, cur_ptr = read_buffer(buffer, cur_ptr) - state, cur_ptr = read_buffer(buffer, cur_ptr) + buffer += struct.pack('f', aifort['min_cannon_damage_distance']) - num_masts = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - masts = [] - for _ in range(num_masts): - format = '3f3fIIf' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) + for fort in aifort['forts']: + buffer += struct.pack('3f', fort['pos']['x'], fort['pos']['y'], fort['pos']['z']) + for cannon in fort['cannons']: + buffer = write_aicannon(cannon, buffer) + for culevrin in fort['culevrins']: + buffer = write_aicannon(culevrin, buffer) + for mortar in fort['mortars']: + buffer = write_aicannon(mortar, buffer) - mast = { - 'v_src': {'x': data[0], 'y': data[1], 'z': data[2]}, - 'v_dst': {'x': data[3], 'y': data[4], 'z': data[5]}, - 'mast_num': data[6], - 'is_broken': data[7], - 'damage': data[8] - } - assert (len(data) == 9) + return buffer - num_hulls = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - hulls = [] - for _ in range(num_hulls): - format = '3f3fIIf' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) - hull = { - 'v_src': {'x': data[0], 'y': data[1], 'z': data[2]}, - 'v_dst': {'x': data[3], 'y': data[4], 'z': data[5]}, - 'hull_num': data[6], - 'is_broken': data[7], - 'damage': data[8] - } - assert (len(data) == 9) - - num_fireplaces = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - fireplaces = [] - for _ in range(num_fireplaces): - fireplace, cur_ptr = read_fireplace(buffer, cur_ptr) - - x_heel = struct.unpack_from('f', buffer, cur_ptr)[0] - cur_ptr += 4 - z_heel = struct.unpack_from('f', buffer, cur_ptr)[0] - cur_ptr += 4 +def write_ship(ship, buffer): + buffer = write_attr_ptr(ship['character'], buffer) + buffer = write_attr_ptr(ship['ship_attr_ptr'], buffer) + buffer += struct.pack('I', ship['realize_layer']) + buffer += struct.pack('I', ship['execute_layer']) + buffer = write_string(ship['ship_name'], buffer) + # _ = struct.unpack_from('I', buffer, cur_ptr)[0] # skip + buffer += struct.pack('f', ship['gravity']) + buffer += struct.pack('f', ship['sail_state']) + buffer += struct.pack('I', ship['uni_idx']) + buffer += struct.pack('I', ship['use']) + buffer = write_buffer(ship['ship_points'], buffer) + buffer += struct.pack('3f', ship['speed_accel']['x'], ship['speed_accel']['y'], ship['speed_accel']['z']) + buffer = write_buffer(ship['sp'], buffer) + buffer += struct.pack('3f', ship['pos']['x'], ship['pos']['y'], ship['pos']['z']) + buffer += struct.pack('3f', ship['angle']['x'], ship['angle']['y'], ship['angle']['z']) + buffer += struct.pack('f', ship['water_line']) + buffer += struct.pack('I', ship['is_dead']) + buffer += struct.pack('I', ship['is_visible']) + buffer += struct.pack('3f', ship['dead_dir']['x'], ship['dead_dir']['y'], ship['dead_dir']['z']) + buffer += struct.pack('3f', ship['cur_dead_dir']['x'], ship['cur_dead_dir']['y'], ship['cur_dead_dir']['z']) + buffer = write_buffer(ship['keel_contour'], buffer) + buffer += struct.pack('I', ship['is_ship2strand']) + buffer += struct.pack('I', ship['is_mounted']) + buffer += struct.pack('I', ship['is_keel_contour']) + buffer += struct.pack('I', ship['is_perk_turn_active']) + buffer += struct.pack('f', ship['initial_perk_angle']) + buffer += struct.pack('f', ship['result_perk_angle']) + buffer = write_buffer(ship['strength'], buffer) + buffer += struct.pack('I', ship['is_set_light_and_fog']) + buffer += struct.pack('I', ship['save_ambient']) + buffer += struct.pack('I', ship['save_fog_color']) + buffer = write_buffer(ship['save_light'], buffer) + buffer = write_buffer(ship['state'], buffer) + + buffer += struct.pack('I', len(ship['masts'])) + for mast in ship['masts']: + buffer += struct.pack('3f', mast['v_src']['x'], mast['v_src']['y'], mast['v_src']['z']) + buffer += struct.pack('3f', mast['v_dst']['x'], mast['v_dst']['y'], mast['v_dst']['z']) + buffer += struct.pack('I', mast['mast_num']) + buffer += struct.pack('I', mast['is_broken']) + buffer += struct.pack('f', mast['damage']) + + buffer += struct.pack('I', len(ship['hulls'])) + for hull in ship['hulls']: + buffer += struct.pack('3f', hull['v_src']['x'], hull['v_src']['y'], hull['v_src']['z']) + buffer += struct.pack('3f', hull['v_dst']['x'], hull['v_dst']['y'], hull['v_dst']['z']) + buffer += struct.pack('I', hull['hull_num']) + buffer += struct.pack('I', hull['is_broken']) + buffer += struct.pack('f', hull['damage']) + + buffer += struct.pack('I', len(ship['fireplaces'])) + for fireplace in ship['fireplaces']: + buffer = write_fireplace(fireplace, buffer) + + buffer += struct.pack('f', ship['x_heel']) + buffer += struct.pack('f', ship['z_heel']) return buffer def write_aiship(aiship, buffer): - ship, cur_ptr = read_ship(buffer, cur_ptr) + buffer = write_ship(aiship['base_ship'], buffer) - character, cur_ptr = read_attr_ptr(buffer, cur_ptr) - obj_type = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - is_dead = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 + buffer = write_attr_ptr(aiship['character'], buffer) + buffer += struct.pack('I', aiship['obj_type']) + buffer += struct.pack('I', aiship['is_dead']) - cannon_ctrl, cur_ptr = read_aiship_cannon_controller(buffer, cur_ptr) + buffer = write_aiship_cannon_controller(aiship['cannon_ctrl'], buffer) - has_cam_ctrl = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - if has_cam_ctrl: - cam_ctrl, cur_ptr = read_aiship_camera_controller(buffer, cur_ptr) + has_cam_ctrl = 1 if aiship['cam_ctrl'] is not None else 0 + buffer += struct.pack('I', has_cam_ctrl) + if has_cam_ctrl != 0: + buffer = write_aiship_camera_controller(aiship['cam_ctrl'], buffer) - move_ctrl, cur_ptr = read_aiship_move_controller(buffer, cur_ptr) - rotate_ctrl, cur_ptr = read_aiship_rotate_controller(buffer, cur_ptr) - speed_ctrl, cur_ptr = read_aiship_speed_controller(buffer, cur_ptr) - task_ctrl, cur_ptr = read_aiship_task_controller(buffer, cur_ptr) - touch_ctrl, cur_ptr = read_aiship_touch_controller(buffer, cur_ptr) + buffer = write_aiship_move_controller(aiship['move_ctrl'], buffer) + buffer = write_aiship_rotate_controller(aiship['rotate_ctrl'], buffer) + buffer = write_aiship_speed_controller(aiship['speed_ctrl'], buffer) + buffer = write_aiship_task_controller(aiship['task_ctrl'], buffer) + buffer = write_aiship_touch_controller(aiship['touch_ctrl'], buffer) return buffer def write_aiship_camera_controller(controller, buffer): + buffer += struct.pack('I', controller['target']) + buffer += struct.pack('f', controller['delta']) + buffer += struct.pack('I', controller['cam_outside']) + return buffer def write_aiship_cannon_controller(controller, buffer): + buffer += struct.pack('I', controller['reload']) + buffer += struct.pack('I', controller['not_enough_balls']) + buffer += struct.pack('I', controller['temp_flag']) + + buffer += struct.pack('I', len(controller['borts'])) + for bort in controller['borts']: + buffer = write_string(bort['name'], buffer) + buffer += struct.pack('f', bort['fire_zone']) + buffer += struct.pack('f', bort['fire_angle_min']) + buffer += struct.pack('f', bort['fire_angle_max']) + buffer += struct.pack('f', bort['fire_dir']) + buffer += struct.pack('f', bort['fire_heigh']) + buffer += struct.pack('f', bort['charge_percent']) + buffer += struct.pack('f', bort['cos_fire_zone']) + buffer += struct.pack('I', bort['num_cannons']) + buffer += struct.pack('f', bort['speedV0']) + buffer += struct.pack('f', bort['max_fire_distance']) + buffer += struct.pack('3f', bort['direction']['x'], bort['direction']['y'], bort['direction']['z']) + + buffer += struct.pack('I', len(bort['cannons'])) + for cannon in bort['cannons']: + buffer = write_aicannon(cannon, buffer) + return buffer def write_aiship_move_controller(controller, buffer): + buffer += struct.pack('I', controller['stopped']) + buffer += struct.pack('3f', controller['dest_point']['x'], controller['dest_point']['y'], + controller['dest_point']['z']) + buffer += struct.pack('3f', controller['braking_force']['x'], controller['braking_force']['y'], + controller['braking_force']['z']) + buffer += struct.pack('3f', controller['deviation_force']['x'], controller['deviation_force']['y'], + controller['deviation_force']['z']) + buffer += struct.pack('f', controller['move_time']) + buffer += struct.pack('I', controller['cur_point']) + return buffer def write_aiship_rotate_controller(controller, buffer): + buffer += struct.pack('I', controller['rotate_num']) + buffer += struct.pack('f', controller['rotate_mode']) + buffer += struct.pack('f', controller['rotate_time']) + buffer += struct.pack('f', controller['rotate_smooth']) + buffer += struct.pack('f', controller['rotate']) + buffer += struct.pack('f', controller['global_multiply']) + return buffer def write_aiship_speed_controller(controller, buffer): + buffer += struct.pack('I', controller['speed_num']) + buffer += struct.pack('f', controller['speed_smooth']) + buffer += struct.pack('f', controller['speed']) + buffer += struct.pack('f', controller['speed_time']) + buffer += struct.pack('f', controller['top_speed']) + buffer += struct.pack('f', controller['global_multiply']) + return buffer def write_aiship_task_controller(controller, buffer): + buffer += struct.pack('f', controller['extra_distance']) + + primary_task = controller['primary_task'] + buffer += struct.pack('I', primary_task['active']) + buffer += struct.pack('I', primary_task['task_type']) + buffer = write_attr_ptr(primary_task['task_character'], buffer) + buffer += struct.pack('3f', primary_task['task_point']['x'], primary_task['task_point']['y'], + primary_task['task_point']['z']) + + secondary_task = controller['secondary_task'] + buffer += struct.pack('I', secondary_task['active']) + buffer += struct.pack('I', secondary_task['task_type']) + buffer = write_attr_ptr(secondary_task['task_character'], buffer) + buffer += struct.pack('3f', secondary_task['task_point']['x'], secondary_task['task_point']['y'], + secondary_task['task_point']['z']) + return buffer def write_aiship_touch_controller(controller, buffer): + buffer += struct.pack('I', len(controller['rays'])) + for ray in controller['rays']: + buffer += struct.pack('3f', ray['x'], ray['y'], ray['z']) + + buffer += struct.pack('f', controller['left_rays_free']) + buffer += struct.pack('f', controller['right_rays_free']) + buffer += struct.pack('f', controller['ray_size']) + buffer += struct.pack('f', controller['speed_factor']) + buffer += struct.pack('f', controller['rotate_factor']) + buffer += struct.pack('3f', controller['box1']['x'], controller['box1']['y'], + controller['box1']['z']) + buffer += struct.pack('3f', controller['box2']['x'], controller['box2']['y'], + controller['box2']['z']) + buffer += struct.pack('3f', controller['box3']['x'], controller['box3']['y'], + controller['box3']['z']) + buffer += struct.pack('3f', controller['box4']['x'], controller['box4']['y'], + controller['box4']['z']) + buffer += struct.pack('3f', controller['box5']['x'], controller['box5']['y'], + controller['box5']['z']) + return buffer def write_aigroup(aigroup, buffer): - commander, cur_ptr = read_attr_ptr(buffer, cur_ptr) - group_name, cur_ptr = read_string(buffer, cur_ptr) - command, cur_ptr = read_string(buffer, cur_ptr) - command_group, cur_ptr = read_string(buffer, cur_ptr) - location_near_other_group, cur_ptr = read_string(buffer, cur_ptr) - group_type, cur_ptr = read_string(buffer, cur_ptr) - init_group_pos = struct.unpack_from('3f', buffer, cur_ptr)[0] - cur_ptr += 3 * 4 - move_point = struct.unpack_from('3f', buffer, cur_ptr)[0] - cur_ptr += 3 * 4 - first_execute = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - num_ships = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - for _ in range(num_ships): - ship, cur_ptr = read_ship(buffer, cur_ptr) + buffer = write_attr_ptr(aigroup['commander'], buffer) + buffer = write_string(aigroup['group_name'], buffer) + buffer = write_string(aigroup['command'], buffer) + buffer = write_string(aigroup['command_group'], buffer) + buffer = write_string(aigroup['location_near_other_group'], buffer) + buffer = write_string(aigroup['group_type'], buffer) + buffer += struct.pack('3f', aigroup['init_group_pos']['x'], aigroup['init_group_pos']['y'], + aigroup['init_group_pos']['z']) + buffer += struct.pack('3f', aigroup['move_point']['x'], aigroup['move_point']['y'], aigroup['move_point']['z']) + buffer += struct.pack('I', aigroup['first_execute']) + buffer += struct.pack('I', len(aigroup['ships'])) + for ship in aigroup['ships']: + buffer = write_aiship(ship, buffer) return buffer def write_aihelper(aihelper, buffer): - gravity = struct.unpack_from('f', buffer, cur_ptr)[0] - cur_ptr += 4 - num_relations = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - relations, cur_ptr = read_buffer(buffer, cur_ptr) - sea_cameras, cur_ptr = read_attr_ptr(buffer, cur_ptr) + buffer += struct.pack('f', aihelper['gravity']) + buffer += struct.pack('I', aihelper['num_relations']) - num_characters = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - characters = [] - for _ in range(num_characters): - char_ptr, cur_ptr = read_attr_ptr(buffer, cur_ptr) - characters.append(char_ptr) + buffer = write_buffer(aihelper['relations'], buffer) + buffer = write_attr_ptr(aihelper['sea_cameras'], buffer) - num_main_characters = struct.unpack_from('I', buffer, cur_ptr)[0] - cur_ptr += 4 - main_characters = [] - for _ in range(num_main_characters): - char_ptr, cur_ptr = read_attr_ptr(buffer, cur_ptr) - main_characters.append(char_ptr) + buffer += struct.pack('I', len(aihelper['characters'])) + for c in aihelper['characters']: + buffer = write_attr_ptr(c, buffer) + + buffer += struct.pack('I', len(aihelper['main_characters'])) + for c in aihelper['main_characters']: + buffer = write_attr_ptr(c, buffer) - aihelper = { - 'gravity': gravity, - 'num_relations': num_relations, - 'relations': relations, - 'sea_cameras': sea_cameras, - 'characters': characters, - 'main_characters': main_characters - } return buffer -def write_deck_cam(deck_cam, buffer): - vertices, cur_ptr = read_buffer(buffer, cur_ptr) +def write_deck_cam(cam, buffer): + buffer = write_buffer(cam['vertices'], buffer) + + buffer += struct.pack('f', cam['distance_sensitivity']) + buffer += struct.pack('f', cam['height_angle_sensitivity']) + buffer += struct.pack('f', cam['azimuth_angle_sensitivity']) + buffer += struct.pack('f', cam['rocking_x']) + buffer += struct.pack('f', cam['rocking_z']) + buffer += struct.pack('f', cam['men_step_up']) + buffer += struct.pack('f', cam['men_step_min']) + buffer += struct.pack('f', cam['height_max']) + buffer += struct.pack('f', cam['height_min']) + buffer += struct.pack('f', cam['height_step']) + buffer += struct.pack('f', cam['cam_max_x']) + buffer += struct.pack('f', cam['cam_min_x']) + buffer += struct.pack('f', cam['default_height']) + buffer += struct.pack('3f', cam['g_vec1']['x'], cam['g_vec1']['y'], cam['g_vec1']['z']) + buffer += struct.pack('3f', cam['g_vec2']['x'], cam['g_vec2']['y'], cam['g_vec2']['z']) + buffer += struct.pack('3f', cam['g_vec3']['x'], cam['g_vec3']['y'], cam['g_vec3']['z']) + buffer += struct.pack('3f', cam['cam_pos']['x'], cam['cam_pos']['y'], cam['cam_pos']['z']) + buffer += struct.pack('3f', cam['cam_angle']['x'], cam['cam_angle']['y'], cam['cam_angle']['z']) + buffer += struct.pack('f', cam['eye_height']) + + buffer = write_buffer(cam['screen_rect'], buffer) + + buffer += struct.pack('I', cam['lock_x']) + buffer += struct.pack('I', cam['lock_y']) + buffer += struct.pack('I', cam['is_on']) + buffer += struct.pack('I', cam['is_active']) + buffer += struct.pack('f', cam['perspective']) + + buffer = write_attr_ptr(cam['character'], buffer) - format = '13f3f3f3f3f3ff' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) + return buffer - deck_cam = { - 'distance_sensivity': data[0], - 'height_angle_sensivity': data[1], - 'azimuth_angle_sensivity': data[2], - 'rocking_x': data[3], - 'rocking_z': data[4], - 'men_step_up': data[5], - 'men_step_min': data[6], - 'height_max': data[7], - 'height_min': data[8], - 'height_step': data[9], - 'cam_max_x': data[10], - 'cam_min_x': data[11], - 'default_height': data[12], - 'g_vec1': {'x': data[13], 'y': data[14], 'z': data[15]}, - 'g_vec2': {'x': data[16], 'y': data[17], 'z': data[18]}, - 'g_vec3': {'x': data[19], 'y': data[20], 'z': data[21]}, - 'cam_pos': {'x': data[22], 'y': data[23], 'z': data[24]}, - 'cam_angle': {'x': data[25], 'y': data[26], 'z': data[27]}, - 'eye_height': data[28] - } - assert (len(data) == 29) - deck_cam['screen_rect'], cur_ptr = read_buffer(buffer, cur_ptr) +def write_free_cam(cam, buffer): + buffer += struct.pack('3f', cam['pos']['x'], cam['pos']['y'], cam['pos']['z']) + buffer += struct.pack('3f', cam['angle']['x'], cam['angle']['y'], cam['angle']['z']) + buffer += struct.pack('f', cam['fov']) + buffer += struct.pack('I', cam['lock_x']) + buffer += struct.pack('I', cam['lock_y']) + buffer += struct.pack('I', cam['is_onland']) + buffer += struct.pack('f', cam['cam_onland_height']) - format = '4If' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) - deck_cam['lock_x'] = data[0] - deck_cam['lock_y'] = data[1] - deck_cam['is_on'] = data[2] - deck_cam['is_active'] = data[3] - deck_cam['perspective'] = data[4] - assert (len(data) == 5) + return buffer - deck_cam['character'], cur_ptr = read_attr_ptr(buffer, cur_ptr) + +def write_ship_cam(cam, buffer): + buffer += struct.pack('I', cam['lock_x']) + buffer += struct.pack('I', cam['lock_y']) + buffer += struct.pack('f', cam['min_height_on_sea']) + buffer += struct.pack('f', cam['max_height_on_sea']) + buffer += struct.pack('f', cam['distance']) + buffer += struct.pack('f', cam['max_distance']) + buffer += struct.pack('f', cam['min_distance']) + buffer += struct.pack('f', cam['distance_delta']) + buffer += struct.pack('f', cam['distance_inertia']) + buffer += struct.pack('f', cam['min_angle_x']) + buffer += struct.pack('f', cam['max_angle_x']) + buffer += struct.pack('f', cam['angle_x_delta']) + buffer += struct.pack('f', cam['angle_x_inertia']) + buffer += struct.pack('f', cam['angle_y_delta']) + buffer += struct.pack('f', cam['angle_y_inertia']) + buffer += struct.pack('f', cam['distance_sensivity']) + buffer += struct.pack('f', cam['azimuth_angle_sensivity']) + buffer += struct.pack('f', cam['height_angle_sensivity']) + buffer += struct.pack('f', cam['invert_mouse_x']) + buffer += struct.pack('f', cam['invert_mouse_y']) + buffer += struct.pack('3f', cam['center']['x'], cam['center']['y'], cam['center']['z']) + buffer += struct.pack('3f', cam['angle']['x'], cam['angle']['y'], cam['angle']['z']) + buffer += struct.pack('f', cam['model_atan_y']) + buffer += struct.pack('I', cam['ship_code']) + buffer += struct.pack('I', cam['num_islands']) + buffer += struct.pack('I', cam['is_on']) + buffer += struct.pack('I', cam['is_active']) + buffer += struct.pack('f', cam['perspective']) + + buffer = write_attr_ptr(cam['character'], buffer) return buffer -def write_free_cam(free_cam, buffer): - format = '3f3ffIIIf' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) +def write_fireplace(fireplace, buffer): + buffer += struct.pack('3f', fireplace['orig_pos']['x'], fireplace['orig_pos']['y'], fireplace['orig_pos']['z']) + buffer += struct.pack('I', fireplace['active']) + buffer += struct.pack('f', fireplace['run_time']) + buffer += struct.pack('I', fireplace['ball_character_index']) - free_cam = { - 'pos': {'x': data[0], 'y': data[1], 'z': data[2]}, - 'angle': {'x': data[3], 'y': data[4], 'z': data[5]}, - 'fov': data[6], - 'lock_x': data[7], - 'lock_y': data[8], - 'is_onland': data[9], - 'cam_onland_height': data[10] - } - assert (len(data) == 11) + buffer = write_string(fireplace['particle_smoke_name'], buffer) + buffer = write_string(fireplace['particle_fire_name'], buffer) + buffer = write_string(fireplace['sound_name'], buffer) return buffer -def write_ship_cam(ship_cam, buffer): - buffer += struct.pack('I', ship_cam['lock_x']) - buffer += struct.pack('I', ship_cam['lock_y']) - buffer += struct.pack('f', ship_cam['min_height_on_sea']) - buffer += struct.pack('f', ship_cam['max_height_on_sea']) - buffer += struct.pack('f', ship_cam['distance']) - buffer += struct.pack('f', ship_cam['max_distance']) - buffer += struct.pack('f', ship_cam['min_distance']) - buffer += struct.pack('f', ship_cam['distance_delta']) - buffer += struct.pack('f', ship_cam['distance_inertia']) - buffer += struct.pack('f', ship_cam['min_angle_x']) - buffer += struct.pack('f', ship_cam['max_angle_x']) - buffer += struct.pack('f', ship_cam['angle_x_delta']) - buffer += struct.pack('f', ship_cam['angle_x_inertia']) - buffer += struct.pack('f', ship_cam['angle_y_delta']) - buffer += struct.pack('f', ship_cam['angle_y_inertia']) - buffer += struct.pack('f', ship_cam['distance_sensivity']) - buffer += struct.pack('f', ship_cam['azimuth_angle_sensivity']) - buffer += struct.pack('f', ship_cam['height_angle_sensivity']) - buffer += struct.pack('f', ship_cam['invert_mouse_x']) - buffer += struct.pack('f', ship_cam['invert_mouse_y']) - buffer += struct.pack('3f', ship_cam['center']['x'], ship_cam['center']['y'], ship_cam['center']['z']) - buffer += struct.pack('3f', ship_cam['angle']['x'], ship_cam['angle']['y'], ship_cam['angle']['z']) - buffer += struct.pack('f', ship_cam['model_atan_y']) - buffer += struct.pack('I', ship_cam['ship_code']) - buffer += struct.pack('I', ship_cam['num_islands']) - buffer += struct.pack('I', ship_cam['is_on']) - buffer += struct.pack('I', ship_cam['is_active']) - buffer += struct.pack('f', ship_cam['perspective']) +def write_seasave(seasave, buffer): + buffer = write_aihelper(seasave['aihelper'], buffer) - ship_cam['character'], cur_ptr = read_attr_ptr(buffer, cur_ptr) + buffer = write_ship_cam(seasave['ship_cam'], buffer) + buffer = write_free_cam(seasave['free_cam'], buffer) + buffer = write_deck_cam(seasave['deck_cam'], buffer) - return buffer + buffer = write_aiballs(seasave['aiballs'], buffer) + buffer += struct.pack('I', len(seasave['aigroups'])) + for aigroup in seasave['aigroups']: + buffer = write_aigroup(aigroup, buffer) -def write_fireplace(fireplace, buffer): - buffer += struct.pack('I', var['type'].value) - buffer += struct.pack('I', len(var['values'])) + if seasave['aifort'] is not None: + buffer = write_aifort(seasave['aifort'], buffer) return buffer -def write_seasave(seasave, buffer): - buffer = write_aihelper(aihelper, buffer) +# Conversion + +def convert_107_to_173(seasave): + layers = { + 'execute': 0, + 'realize': 1, + 'sea_execute': 2, + 'sea_realize': 3, + 'interface_execute': 4, + 'interface_realize': 5, + 'fader_execute': 6, + 'fader_realize': 7, + 'lighter_execute': 8, + 'lighter_realize': 9, + 'video_execute': 10, + 'video_realize': 11, + 'editor_realize': 12, + 'info_realize': 13, + 'sound_debug_realize': 14, + 'sea_reflection': 15, + 'sea_reflection2': 16, + 'sea_sunroad': 17, + 'sun_trace': 18, + 'sails_trace': 19, + 'hull_trace': 20, + 'mast_island_trace': 21, + 'mast_ship_trace': 22, + 'ship_cannon_trace': 23, + 'fort_cannon_trace': 24, + 'island_trace': 25, + 'shadow': 26, + 'blood': 27, + 'rain_drops': 28 + } - buffer = write_ship_cam(ship_cam, buffer) - buffer = write_free_cam(free_cam, buffer) - buffer = write_deck_cam(deck_cam, buffer) + for aigroup in seasave['aigroups']: + for aiship in aigroup['ships']: + strval = aiship['base_ship']['realize_layer'] + aiship['base_ship']['realize_layer'] = layers[strval] - buffer = write_aiballs(aiballs, buffer) + strval = aiship['base_ship']['execute_layer'] + aiship['base_ship']['execute_layer'] = layers[strval] - return buffer + return seasave \ No newline at end of file From c1a52e4e495f53e2077467acf04761b9b5650692 Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Sun, 5 Sep 2021 21:46:34 +0200 Subject: [PATCH 11/20] Fix setting wrong seasave buffer length during conversion --- tools/gamesave-convert/gamesave_convert.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index 2774fa043..2e298cb00 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -330,10 +330,10 @@ def write_save(save_data, filename): buffer = write_int8_16_32(len(variables), buffer) if 'oSeaSave' in variables: seasave_data = variables['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] - seasave_buf = bytes() - seasave_buf = seasave.write_seasave(seasave_data, seasave_buf).hex() + seasave_buf = bytearray() + seasave_buf = seasave.write_seasave(seasave_data, seasave_buf) seasave_size = f'{len(seasave_buf):08x}' # as hexadecimal string without prefix 8 bytes long - variables['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] = seasave_size + seasave_buf + variables['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] = seasave_size + seasave_buf.hex() for varname in variables: buffer = write_string(varname, buffer, str_encoding) From 0a9298760a67da4b63dc572ce6a3dcc5aa3959c1 Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Sun, 5 Sep 2021 21:47:15 +0200 Subject: [PATCH 12/20] Add more checks if save data contains seasave --- tools/gamesave-convert/gamesave_convert.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index 2e298cb00..2ce40dc7b 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -196,7 +196,7 @@ def read_save(file_name): var, cur_ptr = read_variable(buffer, cur_ptr, s_db, str_encoding, obj_id_format) vars[name] = var - if 'oSeaSave' in vars: + if 'oSeaSave' in vars and 'attributes' in vars['oSeaSave']['values'][0]['attributes']['skip']: seasave_data = vars['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] data = seasave.read_seasave(bytes.fromhex(seasave_data[8:])) vars['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] = data @@ -328,7 +328,7 @@ def write_save(save_data, filename): variables = save_data['vars'] buffer = write_int8_16_32(len(variables), buffer) - if 'oSeaSave' in variables: + if 'oSeaSave' in variables and 'attributes' in variables['oSeaSave']['values'][0]['attributes']['skip']: seasave_data = variables['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] seasave_buf = bytearray() seasave_buf = seasave.write_seasave(seasave_data, seasave_buf) @@ -576,7 +576,7 @@ def convert_107_to_173(save_data, s_db): for val in var['values']: val['id'] = (sum(val['id']),) # one item tuple - if name == 'oSeaSave': + if name == 'oSeaSave' and 'attributes' in var['values'][0]['attributes']['skip']: seasave_data = var['values'][0]['attributes']['skip']['attributes']['save']['value'] seasave_data = seasave.convert_107_to_173(seasave_data) var['values'][0]['attributes']['skip']['attributes']['save']['value'] = seasave_data From c10d9da674163493d2f9023c0dd91694b958452d Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Sun, 5 Sep 2021 21:48:46 +0200 Subject: [PATCH 13/20] Fix serialization of empty seasave strings --- tools/gamesave-convert/seasave.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tools/gamesave-convert/seasave.py b/tools/gamesave-convert/seasave.py index e26f454f7..609cb9abb 100644 --- a/tools/gamesave-convert/seasave.py +++ b/tools/gamesave-convert/seasave.py @@ -12,11 +12,12 @@ def read_buffer(buffer, cur_ptr): def read_string(buffer, cur_ptr, encoding): str_len = struct.unpack_from('I', buffer, cur_ptr)[0] cur_ptr += 4 - if str_len == 0: - return None, cur_ptr - s = struct.unpack_from(f'{str_len-1}s', buffer, cur_ptr)[0] # str_len-1 to skip trailing '\0' - s = s.decode(encoding) - cur_ptr += str_len + if str_len > 0: + s = struct.unpack_from(f'{str_len - 1}s', buffer, cur_ptr)[0] # str_len-1 to skip trailing '\0' + s = s.decode(encoding) + cur_ptr += str_len + else: + s = '' return s, cur_ptr @@ -795,13 +796,13 @@ def write_buffer(buf, buffer): def write_string(s, buffer): - if s is not None: + if s != '': s = s.encode('utf-8') + b'\x00' str_len = len(s) buffer += struct.pack('I', str_len) buffer += struct.pack(f'{str_len}s', s) else: - buffer += b'\x00' + buffer += struct.pack('I', 0) return buffer From b80c8d87e17a9a7b40467f2de51fbb168bd8bc87 Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Sun, 5 Sep 2021 21:52:07 +0200 Subject: [PATCH 14/20] Add missing seasave data fields --- tools/gamesave-convert/seasave.py | 53 +++++++++++++++++-------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/tools/gamesave-convert/seasave.py b/tools/gamesave-convert/seasave.py index 609cb9abb..4b6c2bdc7 100644 --- a/tools/gamesave-convert/seasave.py +++ b/tools/gamesave-convert/seasave.py @@ -228,7 +228,7 @@ def read_ship(buffer, cur_ptr): realize_layer, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') execute_layer, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') ship_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') - _ = struct.unpack_from('I', buffer, cur_ptr)[0] # skip + ship_priority_execute = struct.unpack_from('I', buffer, cur_ptr)[0] cur_ptr += 4 gravity = struct.unpack_from('f', buffer, cur_ptr)[0] cur_ptr += 4 @@ -338,6 +338,7 @@ def read_ship(buffer, cur_ptr): 'realize_layer': realize_layer, 'execute_layer': execute_layer, 'ship_name': ship_name, + 'ship_priority_execute': ship_priority_execute, 'gravity': gravity, 'sail_state': sail_state, 'uni_idx': uni_idx, @@ -697,7 +698,7 @@ def read_ship_cam(buffer, cur_ptr): 'lock_x': data[0], 'lock_y': data[1], 'min_height_on_sea': data[2], - 'max_height_on_sea': data[3], + 'max_height_on_ship': data[3], 'distance': data[4], 'max_distance': data[5], 'min_distance': data[6], @@ -709,17 +710,17 @@ def read_ship_cam(buffer, cur_ptr): 'angle_x_inertia': data[12], 'angle_y_delta': data[13], 'angle_y_inertia': data[14], - 'distance_sensivity': data[15], - 'azimuth_angle_sensivity': data[16], - 'height_angle_sensivity': data[17], - 'height_angle_onship_sensivity': data[18], + 'distance_sensitivity': data[15], + 'azimuth_angle_sensitivity': data[16], + 'height_angle_sensitivity': data[17], + 'height_angle_onship_sensitivity': data[18], 'invert_mouse_x': data[19], 'invert_mouse_y': data[20], 'center': { 'x': data[21], 'y': data[22], 'z': data[23] }, 'angle': { 'x': data[24], 'y': data[25], 'z': data[26] }, 'model_atan_y': data[27], 'ship_code': data[28], - 'num_islands': data[29], + 'islands_init_count': data[29], 'is_on': data[30], 'is_active': data[31], 'perspective': data[32] @@ -736,18 +737,21 @@ def read_fireplace(buffer, cur_ptr): data = struct.unpack_from(format, buffer, cur_ptr) cur_ptr += struct.calcsize(format) + particle_smoke_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + particle_fire_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + sound_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') + fireplace = { 'orig_pos': {'x': data[0], 'y': data[1], 'z': data[2]}, 'active': data[3], 'run_time': data[4], - 'ball_character_index': data[5] + 'ball_character_index': data[5], + 'particle_smoke_name': particle_smoke_name, + 'particle_fire_name': particle_fire_name, + 'sound_name': sound_name } assert (len(data) == 6) - particle_smoke_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') - particle_fire_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') - sound_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') - return fireplace, cur_ptr @@ -875,7 +879,7 @@ def write_ship(ship, buffer): buffer += struct.pack('I', ship['realize_layer']) buffer += struct.pack('I', ship['execute_layer']) buffer = write_string(ship['ship_name'], buffer) - # _ = struct.unpack_from('I', buffer, cur_ptr)[0] # skip + buffer += struct.pack('I', ship['ship_priority_execute']) buffer += struct.pack('f', ship['gravity']) buffer += struct.pack('f', ship['sail_state']) buffer += struct.pack('I', ship['uni_idx']) @@ -924,9 +928,6 @@ def write_ship(ship, buffer): for fireplace in ship['fireplaces']: buffer = write_fireplace(fireplace, buffer) - buffer += struct.pack('f', ship['x_heel']) - buffer += struct.pack('f', ship['z_heel']) - return buffer @@ -1156,7 +1157,7 @@ def write_ship_cam(cam, buffer): buffer += struct.pack('I', cam['lock_x']) buffer += struct.pack('I', cam['lock_y']) buffer += struct.pack('f', cam['min_height_on_sea']) - buffer += struct.pack('f', cam['max_height_on_sea']) + buffer += struct.pack('f', cam['max_height_on_ship']) buffer += struct.pack('f', cam['distance']) buffer += struct.pack('f', cam['max_distance']) buffer += struct.pack('f', cam['min_distance']) @@ -1168,16 +1169,17 @@ def write_ship_cam(cam, buffer): buffer += struct.pack('f', cam['angle_x_inertia']) buffer += struct.pack('f', cam['angle_y_delta']) buffer += struct.pack('f', cam['angle_y_inertia']) - buffer += struct.pack('f', cam['distance_sensivity']) - buffer += struct.pack('f', cam['azimuth_angle_sensivity']) - buffer += struct.pack('f', cam['height_angle_sensivity']) + buffer += struct.pack('f', cam['distance_sensitivity']) + buffer += struct.pack('f', cam['azimuth_angle_sensitivity']) + buffer += struct.pack('f', cam['height_angle_sensitivity']) + buffer += struct.pack('f', cam['height_angle_onship_sensitivity']) buffer += struct.pack('f', cam['invert_mouse_x']) buffer += struct.pack('f', cam['invert_mouse_y']) buffer += struct.pack('3f', cam['center']['x'], cam['center']['y'], cam['center']['z']) buffer += struct.pack('3f', cam['angle']['x'], cam['angle']['y'], cam['angle']['z']) buffer += struct.pack('f', cam['model_atan_y']) - buffer += struct.pack('I', cam['ship_code']) - buffer += struct.pack('I', cam['num_islands']) + # buffer += struct.pack('I', cam['ship_code']) + buffer += struct.pack('I', cam['islands_init_count']) buffer += struct.pack('I', cam['is_on']) buffer += struct.pack('I', cam['is_active']) buffer += struct.pack('f', cam['perspective']) @@ -1262,4 +1264,9 @@ def convert_107_to_173(seasave): strval = aiship['base_ship']['execute_layer'] aiship['base_ship']['execute_layer'] = layers[strval] - return seasave \ No newline at end of file + del aiship['base_ship']['x_heel'] + del aiship['base_ship']['z_heel'] + + del seasave['ship_cam']['ship_code'] + + return seasave From e40f27f5010aebdee14f346cb57a5f536e7f744f Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Sun, 5 Sep 2021 21:52:32 +0200 Subject: [PATCH 15/20] Reformat seasave.py --- tools/gamesave-convert/seasave.py | 48 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tools/gamesave-convert/seasave.py b/tools/gamesave-convert/seasave.py index 4b6c2bdc7..4736e05cd 100644 --- a/tools/gamesave-convert/seasave.py +++ b/tools/gamesave-convert/seasave.py @@ -115,7 +115,7 @@ def read_aifort(buffer, cur_ptr): for _ in range(num_forts): pos_data = struct.unpack_from('3f', buffer, cur_ptr) pos = {'x': pos_data[0], 'y': pos_data[1], 'z': pos_data[2]} - cur_ptr += 3*4 + cur_ptr += 3 * 4 num_cannons = 4 # hardcode cannons = [] @@ -644,14 +644,14 @@ def read_deck_cam(buffer, cur_ptr): 'cam_max_x': data[10], 'cam_min_x': data[11], 'default_height': data[12], - 'g_vec1': { 'x': data[13], 'y': data[14], 'z': data[15] }, - 'g_vec2': { 'x': data[16], 'y': data[17], 'z': data[18] }, - 'g_vec3': { 'x': data[19], 'y': data[20], 'z': data[21] }, - 'cam_pos': { 'x': data[22], 'y': data[23], 'z': data[24] }, - 'cam_angle': { 'x': data[25], 'y': data[26], 'z': data[27] }, + 'g_vec1': {'x': data[13], 'y': data[14], 'z': data[15]}, + 'g_vec2': {'x': data[16], 'y': data[17], 'z': data[18]}, + 'g_vec3': {'x': data[19], 'y': data[20], 'z': data[21]}, + 'cam_pos': {'x': data[22], 'y': data[23], 'z': data[24]}, + 'cam_angle': {'x': data[25], 'y': data[26], 'z': data[27]}, 'eye_height': data[28] } - assert(len(data) == 29) + assert (len(data) == 29) deck_cam['screen_rect'], cur_ptr = read_buffer(buffer, cur_ptr) @@ -663,7 +663,7 @@ def read_deck_cam(buffer, cur_ptr): deck_cam['is_on'] = data[2] deck_cam['is_active'] = data[3] deck_cam['perspective'] = data[4] - assert(len(data) == 5) + assert (len(data) == 5) deck_cam['character'], cur_ptr = read_attr_ptr(buffer, cur_ptr) @@ -671,28 +671,28 @@ def read_deck_cam(buffer, cur_ptr): def read_free_cam(buffer, cur_ptr): - format = '3f3ffIIIf' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) + fmt = '3f3ffIIIf' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) free_cam = { - 'pos': { 'x': data[0], 'y': data[1], 'z': data[2] }, - 'angle': { 'x': data[3], 'y': data[4], 'z': data[5] }, + 'pos': {'x': data[0], 'y': data[1], 'z': data[2]}, + 'angle': {'x': data[3], 'y': data[4], 'z': data[5]}, 'fov': data[6], 'lock_x': data[7], 'lock_y': data[8], 'is_onland': data[9], 'cam_onland_height': data[10] } - assert(len(data) == 11) - + assert (len(data) == 11) + return free_cam, cur_ptr def read_ship_cam(buffer, cur_ptr): - format = 'II19f3f3ff4If' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) + fmt = 'II19f3f3ff4If' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) ship_cam = { 'lock_x': data[0], @@ -716,8 +716,8 @@ def read_ship_cam(buffer, cur_ptr): 'height_angle_onship_sensitivity': data[18], 'invert_mouse_x': data[19], 'invert_mouse_y': data[20], - 'center': { 'x': data[21], 'y': data[22], 'z': data[23] }, - 'angle': { 'x': data[24], 'y': data[25], 'z': data[26] }, + 'center': {'x': data[21], 'y': data[22], 'z': data[23]}, + 'angle': {'x': data[24], 'y': data[25], 'z': data[26]}, 'model_atan_y': data[27], 'ship_code': data[28], 'islands_init_count': data[29], @@ -725,7 +725,7 @@ def read_ship_cam(buffer, cur_ptr): 'is_active': data[31], 'perspective': data[32] } - assert(len(data) == 33) + assert (len(data) == 33) ship_cam['character'], cur_ptr = read_attr_ptr(buffer, cur_ptr) @@ -733,9 +733,9 @@ def read_ship_cam(buffer, cur_ptr): def read_fireplace(buffer, cur_ptr): - format = '3fIfI' - data = struct.unpack_from(format, buffer, cur_ptr) - cur_ptr += struct.calcsize(format) + fmt = '3fIfI' + data = struct.unpack_from(fmt, buffer, cur_ptr) + cur_ptr += struct.calcsize(fmt) particle_smoke_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') particle_fire_name, cur_ptr = read_string(buffer, cur_ptr, 'cp1251') From d18fb14b9946093a7e0e1cdd20986781d9985e4e Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Mon, 6 Sep 2021 21:40:37 +0200 Subject: [PATCH 16/20] Add simple batch conversion script --- tools/gamesave-convert/convert_all.py | 10 ++++++++ tools/gamesave-convert/gamesave_convert.py | 27 ++++++++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 tools/gamesave-convert/convert_all.py diff --git a/tools/gamesave-convert/convert_all.py b/tools/gamesave-convert/convert_all.py new file mode 100644 index 000000000..4a5b22638 --- /dev/null +++ b/tools/gamesave-convert/convert_all.py @@ -0,0 +1,10 @@ +import glob +import os.path + +import gamesave_convert + +if __name__ == '__main__': + files = glob.iglob('./SAVE/**/*', recursive=True) + for filename in files: + if os.path.isfile(filename): + gamesave_convert.process_file(filename) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index 2ce40dc7b..e64f7c807 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -152,6 +152,10 @@ def read_save(file_name): file_info = struct.unpack('32s', f.read(32))[0] file_info = file_info[0:file_info.find(b'\x00')] file_info = file_info.decode('utf-8') + + if file_info != 'ver 1.0.7': + return None, None + save_data['file_info'] = file_info str_encoding = FILEINFO_CONFIG[file_info]['str_encoding'] @@ -632,15 +636,16 @@ def fallback(v): json.dump(copy, f, ensure_ascii=False, indent=4, default=fallback) -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('input_file', help='name of a save file to convert', type=str) - args = parser.parse_args() +def process_file(file): + file = os.path.abspath(file) + logging.info(f'processing file "{file}".') - input_file = os.path.abspath(args.input_file) - save_data, s_db = read_save(input_file) + save_data, s_db = read_save(file) + if save_data is None: + logging.warning(f'could not process file "{file}".') + return - filename, file_extension = os.path.splitext(input_file) + filename, file_extension = os.path.splitext(file) # dump_save(save_data, f'{filename}_read.json') @@ -651,3 +656,11 @@ def fallback(v): output_file = filename + file_extension write_save(save_data, output_file) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('input_file', help='name of a save file to convert', type=str) + args = parser.parse_args() + + process_file(args.input_file) From 9e4dbeee21d533459a6a1a715f531f3b636f8cbb Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Sat, 2 Oct 2021 13:49:53 +0200 Subject: [PATCH 17/20] Don't convert str_db to utf-8 --- tools/gamesave-convert/gamesave_convert.py | 38 +--------------------- tools/gamesave-convert/str_db.py | 7 ++-- 2 files changed, 5 insertions(+), 40 deletions(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index e64f7c807..d101732de 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -323,7 +323,7 @@ def write_save(save_data, filename): strings = save_data['strings'] buffer = write_int8_16_32(len(strings), buffer) for s in strings.keys(): - buffer = write_string(s, buffer, str_encoding) + buffer = write_string(s, buffer, 'cp1251') segments = save_data['segments'] buffer = write_int8_16_32(len(segments), buffer) @@ -585,42 +585,6 @@ def convert_107_to_173(save_data, s_db): seasave_data = seasave.convert_107_to_173(seasave_data) var['values'][0]['attributes']['skip']['attributes']['save']['value'] = seasave_data - # cleanup strings - used_str = {} - - def visit_attribute(hierarchy, name, a): - used_str[name] = used_str[name] + 1 if name in used_str else 1 - if not name.isascii(): - logging.warning(f'non ascii attribute name "{name}" in {".".join(hierarchy)}') - if 'attributes' in a: - for attr_name, attr in a['attributes'].items(): - visit_attribute([*hierarchy, name], attr_name, attr) - - for name, var in save_data['vars'].items(): - if var['type'] == VarType.Object: - for i, val in enumerate(var['values']): - root_attr_name, root_attr = next(iter(val['attributes'].items())) - visit_attribute([f'{name}[{i}]'], root_attr_name, root_attr) - - # s_db = str_db.remove_unused(s_db, used_str, 'cp1251') - s_db = str_db.create_db(used_str.keys(), 'utf-8') - save_data['strings'] = {} - for s in used_str.keys(): - save_data['strings'][s] = str_db.get_int(s_db, s, 'utf-8') - - # assign new name codes to attributes - def fix_attribute(name, a): - a['name_code'] = str_db.get_int(s_db, name, 'utf-8') - if 'attributes' in a: - for attr_name, attr in a['attributes'].items(): - fix_attribute(attr_name, attr) - - for name, var in save_data['vars'].items(): - if var['type'] == VarType.Object: - for val in var['values']: - root_attr_name, root_attr = next(iter(val['attributes'].items())) - fix_attribute(root_attr_name, root_attr) - return save_data, s_db diff --git a/tools/gamesave-convert/str_db.py b/tools/gamesave-convert/str_db.py index fb3d7c985..aa952e49f 100644 --- a/tools/gamesave-convert/str_db.py +++ b/tools/gamesave-convert/str_db.py @@ -36,15 +36,16 @@ def get_str(db, i): entry = db[row_id][elem_id] return entry + def create_db(str_list, str_encoding): db = [] for i in range(HASH_TABLE_SIZE): db.append([]) - for str in str_list: - h = hash(str, str_encoding) + for s in str_list: + h = hash(s, str_encoding) row_id = h % HASH_TABLE_SIZE - db[row_id].append(str) + db[row_id].append(s) return db From f44654c819dad143cb3d59f7dd7c869eb8a2ceef Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Sat, 2 Oct 2021 14:02:18 +0200 Subject: [PATCH 18/20] Ignore string decoding errors when reading --- tools/gamesave-convert/gamesave_convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index d101732de..84e0b456c 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -55,7 +55,7 @@ def read_string(buffer, cur_ptr, encoding): if str_len == 0: return None, cur_ptr s = struct.unpack_from(f'{str_len - 1}s', buffer, cur_ptr)[0] # str_len-1 to skip trailing '\0' - s = s.decode(encoding) + s = s.decode(encoding, 'ignore') # leave the character out in case of a decoding error cur_ptr += str_len return s, cur_ptr From ef5ec345f46ba8dbbae90447f1bc440915f7620d Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Mon, 18 Oct 2021 21:08:31 +0200 Subject: [PATCH 19/20] Don't fail bulk conversion on errors --- tools/gamesave-convert/convert_all.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/gamesave-convert/convert_all.py b/tools/gamesave-convert/convert_all.py index 4a5b22638..4ce02c176 100644 --- a/tools/gamesave-convert/convert_all.py +++ b/tools/gamesave-convert/convert_all.py @@ -7,4 +7,7 @@ files = glob.iglob('./SAVE/**/*', recursive=True) for filename in files: if os.path.isfile(filename): - gamesave_convert.process_file(filename) + try: + gamesave_convert.process_file(filename) + except: + print(f'Error processing file {filename}') From 17228419903de7eb7c1dbe1f9ea90638565c8ca8 Mon Sep 17 00:00:00 2001 From: Nikolai Prigodich Date: Sun, 21 Nov 2021 18:51:45 +0100 Subject: [PATCH 20/20] Fix PEP warnings --- tools/gamesave-convert/gamesave_convert.py | 28 +++++++++++----------- tools/gamesave-convert/seasave.py | 2 +- tools/gamesave-convert/str_db.py | 15 ++++++------ 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/tools/gamesave-convert/gamesave_convert.py b/tools/gamesave-convert/gamesave_convert.py index 84e0b456c..67785b223 100644 --- a/tools/gamesave-convert/gamesave_convert.py +++ b/tools/gamesave-convert/gamesave_convert.py @@ -1,12 +1,6 @@ #!python -# Python 3.7: Dictionary iteration order is guaranteed to be in order of insertion import sys - -MIN_PYTHON = (3, 7) -if sys.version_info < MIN_PYTHON: - sys.exit("Python {'.'.join([str(n) for n in MIN_PYTHON])} or later is required.") - import argparse import os import struct @@ -17,6 +11,11 @@ import str_db import seasave +# Python 3.7: Dictionary iteration order is guaranteed to be in order of insertion +MIN_PYTHON = (3, 7) +if sys.version_info < MIN_PYTHON: + sys.exit("Python {'.'.join([str(n) for n in MIN_PYTHON])} or later is required.") + FILEINFO_CONFIG = { 'ver 1.0.7': {'str_encoding': 'cp1251', 'obj_id_format': 'QQQ'}, 'ver 1.7.3': {'str_encoding': 'utf-8', 'obj_id_format': 'Q'} @@ -91,8 +90,7 @@ def read_value(var_type, buffer, cur_ptr, s_db, str_encoding, obj_id_format): elif var_type == VarType.String: v, cur_ptr = read_string(buffer, cur_ptr, str_encoding) elif var_type == VarType.Object: - v = {} - v['id'] = struct.unpack_from(obj_id_format, buffer, cur_ptr) + v = {'id': struct.unpack_from(obj_id_format, buffer, cur_ptr)} cur_ptr += struct.calcsize(obj_id_format) attributes = {} attr, cur_ptr = read_attributes_data(buffer, cur_ptr, s_db, str_encoding) @@ -178,8 +176,10 @@ def read_save(file_name): for _ in range(num_strings): s, cur_ptr = read_string(buffer, cur_ptr, str_encoding) if s is not None: - #if not s.isascii(): - #logging.warning(f'non ascii string: {s} (cp1251: {s.encode("cp1251").hex(" ")}, utf-8: {s.encode("utf-8").hex(" ")})') + # if not s.isascii(): + # logging.warning(f'non ascii string: {s} + # (cp1251: {s.encode("cp1251").hex(" ")}, + # utf-8: {s.encode("utf-8").hex(" ")})') strings.append(s) s_db = str_db.create_db(strings, str_encoding) @@ -280,8 +280,8 @@ def write_value(var_type, value, buffer, fileinfo_config): elif var_type == VarType.String: buffer = write_string(value, buffer, fileinfo_config['str_encoding']) elif var_type == VarType.Object: - id = value['id'] - buffer += struct.pack(fileinfo_config['obj_id_format'], *id) + obj_id = value['id'] + buffer += struct.pack(fileinfo_config['obj_id_format'], *obj_id) root_attr = next(iter(value['attributes'])) buffer = write_attributes_data(value['attributes'][root_attr], buffer, fileinfo_config['str_encoding']) elif var_type == VarType.Reference: @@ -315,7 +315,6 @@ def write_save(save_data, filename): file_info = save_data['file_info'] fileinfo_config = FILEINFO_CONFIG[file_info] str_encoding = fileinfo_config['str_encoding'] - obj_id_format = fileinfo_config['obj_id_format'] buffer = bytearray() buffer = write_string(save_data['program_dir'], buffer, str_encoding) @@ -337,7 +336,8 @@ def write_save(save_data, filename): seasave_buf = bytearray() seasave_buf = seasave.write_seasave(seasave_data, seasave_buf) seasave_size = f'{len(seasave_buf):08x}' # as hexadecimal string without prefix 8 bytes long - variables['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] = seasave_size + seasave_buf.hex() + variables['oSeaSave']['values'][0]['attributes']['skip']['attributes']['save']['value'] = \ + seasave_size + seasave_buf.hex() for varname in variables: buffer = write_string(varname, buffer, str_encoding) diff --git a/tools/gamesave-convert/seasave.py b/tools/gamesave-convert/seasave.py index 4736e05cd..038e09376 100644 --- a/tools/gamesave-convert/seasave.py +++ b/tools/gamesave-convert/seasave.py @@ -131,7 +131,7 @@ def read_aifort(buffer, cur_ptr): num_mortars = 4 # hardcode mortars = [] - for _ in range(num_cannons): + for _ in range(num_mortars): mortar, cur_ptr = read_aicannon(buffer, cur_ptr) mortars.append(mortar) diff --git a/tools/gamesave-convert/str_db.py b/tools/gamesave-convert/str_db.py index aa952e49f..43c706459 100644 --- a/tools/gamesave-convert/str_db.py +++ b/tools/gamesave-convert/str_db.py @@ -2,11 +2,12 @@ HASH_TABLE_SIZE = 512 -def hash(s, str_encoding): + +def hash_str(s, str_encoding): h = 0 - bin = s.encode(str_encoding) - for b in bin: - if ord('A') <= b and b <= ord('Z'): + str_bytes = s.encode(str_encoding) + for b in str_bytes: + if ord('A') <= b <= ord('Z'): b += ord('a') - ord('A') h = (h << 4) + b g = h & 0xf0000000 @@ -18,7 +19,7 @@ def hash(s, str_encoding): def get_int(db, s, str_encoding): - h = hash(s, str_encoding) + h = hash_str(s, str_encoding) row_id = h % HASH_TABLE_SIZE if row_id >= HASH_TABLE_SIZE or s not in db[row_id]: logging.warning(f"couldn't find string {s} in the string database") @@ -43,7 +44,7 @@ def create_db(str_list, str_encoding): db.append([]) for s in str_list: - h = hash(s, str_encoding) + h = hash_str(s, str_encoding) row_id = h % HASH_TABLE_SIZE db[row_id].append(s) @@ -58,7 +59,7 @@ def remove_unused(db, used_str, str_encoding): unused_str.append(elem) for s in unused_str: - h = hash(s, str_encoding) + h = hash_str(s, str_encoding) row_id = h % HASH_TABLE_SIZE elem_id = db[row_id].index(s) del db[row_id][elem_id]