Created
October 26, 2024 14:12
-
-
Save fluffeliger/5fce8b3ddaea8bffa2ab1f419315b069 to your computer and use it in GitHub Desktop.
A script to read logic world save files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Programmed with <3 by fluffy | |
from typing import Any | |
from dataclasses import dataclass | |
import io, struct | |
class save: pass | |
def parse_bool(data:bytes) -> bool: | |
return data == 1 | |
def parse_int(data:bytes) -> int: | |
return int.from_bytes(data, 'little') | |
def parse_float(data:bytes) -> float: | |
return struct.unpack('<f', data)[0] | |
def parse_version(data:bytes) -> float: | |
a = int.from_bytes(data[0:4], 'little') | |
b = int.from_bytes(data[4:8], 'little') | |
c = int.from_bytes(data[8:12], 'little') | |
d = int.from_bytes(data[12:16], 'little') | |
return f'{a}.{b}.{c}.{d}' | |
def parse_component_address(data:bytes) -> str: | |
return bin(parse_int(data))[2:] | |
def parse_peg_address(data:bytes) -> dict: | |
print(data) | |
return { | |
'type': parse_int(data[0:1]), | |
'address': parse_component_address(data[1:5]), | |
'index': parse_int(data[5:9]) | |
} | |
def list_to_string(data:dict, depth:int=0) -> None: | |
s = '' | |
depth_str = '\t'*depth | |
for item in data: | |
if type(item) is dict: | |
s += f'{depth_str}-\n{dict_to_string(item, depth+1)}\n' | |
elif type(item) is list: | |
s += f'{depth_str}-{list_to_string(item, depth+1)}\n' | |
else: s += f'{depth_str}-{item}\n' | |
return s.rstrip() | |
def dict_to_string(data:dict, depth:int=0) -> None: | |
s = '' | |
depth_str = '\t'*depth | |
for key, value in data.items(): | |
if type(value) is dict: | |
s += f'{depth_str}{key}:\n{dict_to_string(value, depth+1)}\n' | |
elif type(value) is list: | |
s += f'{depth_str}{key}:\n{list_to_string(value, depth+1)}\n' | |
else: s += f'{depth_str}{key}: {value}\n' | |
return s.rstrip() | |
class Size: | |
BOOL = (1, parse_bool) | |
BYTE = (1, lambda data: data) | |
INT = (4, parse_int) | |
FLOAT = (4, parse_float) | |
UINT2 = (2, parse_int) | |
VERSION = (16, parse_version) | |
HEADERFOOTER = (16, lambda data: data) | |
COMPONENT_ADDRESS = (4, parse_component_address) | |
PEG_ADDRESS = (9, parse_peg_address) | |
@dataclass | |
class ByteData: | |
template:dict[str, Any] | |
def __post_init__(self) -> None: | |
self.__last_exception:str|None = None | |
self.__saved:dict[str, Any] = {} | |
def get_last_exception(self) -> str|None: | |
return self.__last_exception | |
def __parse_dynamic(self, data:Any) -> Any: | |
if type(data) is int: return data | |
elif type(data) is str: return self.__saved[data] | |
elif type(data) is tuple: | |
return data[1](self.__stream.read(data[0])) | |
elif type(data) is dict: | |
data_type = data['type'] | |
if data_type is list: | |
length = self.__parse_dynamic(data['length']) | |
parsed_data:list = [] | |
for _ in range(length): | |
parsed_data.append(self.__parse_dynamic(data['data'])) | |
return parsed_data | |
elif data_type is dict: | |
parsed_data:dict = {} | |
for name, value in data['fields'].items(): | |
parsed_data[name] = self.__parse_dynamic(value) | |
return parsed_data | |
elif data_type is str: | |
string_size = int.from_bytes(self.__stream.read(4), 'little') | |
return self.__stream.read(string_size) | |
elif data_type is save: | |
result = self.__parse_dynamic(data['data']) | |
self.__saved[data['name']] = result | |
return result | |
def parse(self, data:bytes) -> bool: | |
self.__stream = io.BytesIO(data) | |
self.__data = {} | |
for field, size in self.template.items(): | |
self.__data[field] = self.__parse_dynamic(size) | |
return True | |
def get_result(self) -> dict: | |
return self.__data | |
SAVE_FILE_TEMPLATE = { | |
'header': Size.HEADERFOOTER, | |
'format_version': Size.BYTE, | |
'game_version': Size.VERSION, | |
'save_type': Size.BYTE, | |
'component_count': { | |
'type': save, | |
'data': Size.INT, | |
'name': 'component_count' | |
}, | |
'wire_count': { | |
'type': save, | |
'data': Size.INT, | |
'name': 'wire_count' | |
}, | |
'mod_count': { | |
'type': save, | |
'data': Size.INT, | |
'name': 'mod_count' | |
}, | |
'mods': { | |
'type': list, | |
'length': 'mod_count', | |
'data': { | |
'type': dict, | |
'fields': { | |
'id': { | |
'type': str | |
}, | |
'version': Size.VERSION | |
} | |
} | |
}, | |
'component_id_count': { | |
'type': save, | |
'data': Size.INT, | |
'name': 'component_id_count' | |
}, | |
'component_ids': { | |
'type': list, | |
'length': 'component_id_count', | |
'data': { | |
'type': dict, | |
'fields': { | |
'numID': Size.UINT2, | |
'textID': { | |
'type': str | |
} | |
} | |
} | |
}, | |
'component_data': { | |
'type': list, | |
'length': 'component_count', | |
'data': { | |
'type': dict, | |
'fields': { | |
'address': Size.COMPONENT_ADDRESS, | |
'parentAddress': Size.COMPONENT_ADDRESS, | |
'numID': Size.UINT2, | |
'position': { | |
'type': dict, | |
'fields': { | |
'x': Size.INT, | |
'y': Size.INT, | |
'z': Size.INT | |
} | |
}, | |
'rotation': { | |
'type': dict, | |
'fields': { | |
'x': Size.FLOAT, | |
'y': Size.FLOAT, | |
'z': Size.FLOAT, | |
'w': Size.FLOAT | |
} | |
}, | |
'inputs': { | |
'type': dict, | |
'fields': { | |
'numInputs': { | |
'type': save, | |
'data': Size.INT, | |
'name': 'numInputs' | |
}, | |
'data': { | |
'type': list, | |
'length': 'numInputs', | |
'data': Size.INT | |
} | |
} | |
}, | |
'outputs': { | |
'type': dict, | |
'fields': { | |
'numOutputs': { | |
'type': save, | |
'data': Size.INT, | |
'name': 'numOutputs' | |
}, | |
'data': { | |
'type': list, | |
'length': 'numOutputs', | |
'data': Size.INT | |
} | |
} | |
}, | |
'customData': { | |
'type': dict, | |
'fields': { | |
'customDataLength': { | |
'type': save, | |
'data': Size.INT, | |
'name': 'customDataLength' | |
}, | |
'data': { | |
'type': list, | |
'length': 'customDataLength', | |
'data': Size.BYTE | |
} | |
} | |
} | |
} | |
} | |
}, | |
'wires_data': { | |
'type': list, | |
'length': 'wire_count', | |
'data': { | |
'type': dict, | |
'fields': { | |
'peg0': Size.PEG_ADDRESS, | |
'peg1': Size.PEG_ADDRESS, | |
'stateID': Size.INT, | |
'rotation': Size.FLOAT | |
} | |
} | |
} | |
} | |
def main(): | |
################################################# | |
# EDIT THIS | |
logic_world_path = 'path/to/Logic World' | |
################################################# | |
# REMOVE THIS | |
print('SETUP LOGIC WORLD PATH VARIABLE IN THE PYTHON SCRIPT DIRECTLY') | |
return 0 | |
################################################# | |
save_name = input('Save Name: ') | |
path = f'{logic_world_path}/saves/{save_name}/data.logicworld' | |
data = open(path, 'rb').read() | |
byte_data = ByteData(SAVE_FILE_TEMPLATE) | |
result = byte_data.parse(data) | |
if not result: | |
print(f'Failed: {byte_data.get_last_exception()}') | |
return -1 | |
print('-'*100, 'Result', '-'*100) | |
print(dict_to_string(byte_data.get_result())) | |
print('-'*100, '------', '-'*100) | |
return 0 | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sorry for bad code lol