Skip to content

Instantly share code, notes, and snippets.

@fluffeliger
Created October 26, 2024 14:12
Show Gist options
  • Save fluffeliger/5fce8b3ddaea8bffa2ab1f419315b069 to your computer and use it in GitHub Desktop.
Save fluffeliger/5fce8b3ddaea8bffa2ab1f419315b069 to your computer and use it in GitHub Desktop.
A script to read logic world save files
# 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()
@fluffeliger
Copy link
Author

Sorry for bad code lol

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment