diff --git a/mbEditorPro2.0/tests/czone_synthetic.py b/mbEditorPro2.0/tests/czone_synthetic.py new file mode 100644 index 0000000..3659efb --- /dev/null +++ b/mbEditorPro2.0/tests/czone_synthetic.py @@ -0,0 +1,424 @@ +# • ▌ ▄ ·. ▄▄▄▄· ▄▄▄ .·▄▄▄▄ ▪ ▄▄▄▄▄ ▄▄▄ ▄▄▄·▄▄▄ +# ·██ ▐███▪▐█ ▀█▪ ▀▄.▀·██▪ ██ ██ •██ ▪ ▀▄ █· ▐█ ▄█▀▄ █·▪ +# ▐█ ▌▐▌▐█·▐█▀▀█▄ ▐▀▀▪▄▐█· ▐█▌▐█· ▐█.▪ ▄█▀▄ ▐▀▀▄ ██▀·▐▀▀▄ ▄█▀▄ +# ██ ██▌▐█▌██▄▪▐█ ▐█▄▄▌██. ██ ▐█▌ ▐█▌·▐█▌.▐▌▐█•█▌ ▐█▪·•▐█•█▌▐█▌.▐▌ +# ▀▀ █▪▀▀▀·▀▀▀▀ ▀▀▀ ▀▀▀▀▀• ▀▀▀ ▀▀▀ ▀█▄▀▪.▀ ▀ .▀ .▀ ▀ ▀█▄▀▪ +# Magicbane Emulator Project © 2013 - 2022 +# www.magicbane.com + +# Hand-built records for CZone structures the shipped CZone.cache never uses. +# Run from the tool directory: python tests/czone_synthetic.py + +import json +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) + +from arcane.util import ResStream +from arcane.util.ArcFileCache import load_cache_file +from arcane.zones import ArcZone +from arcane.zones.ArcMobile import ArcMobile +from arcane.zones.ArcProp import ArcProp +from arcane.zones.ArcTerrainGen import ArcTerrainGen +from arcane.zones.ArcZoneEvent import ArcZoneEvent +from arcane.zones.mobile.InstanceInfo import InstanceInfo + + +def sample_records(): + mobile_bytes = None + prop_bytes = None + for res_id, data in load_cache_file('CZone.cache'): + zone = ArcZone() + zone.load_binary(ResStream(data)) + if mobile_bytes is None: + for mobile in zone.zone_mobile_info: + if mobile.mobile_type == 1: + out = ResStream() + mobile.mobile_data.save_binary(out) + mobile_bytes = out.get_bytes() + break + if prop_bytes is None: + for prop in zone.zone_prop_info: + if prop.prop_type == 5 and not prop.prop_data.prop_is_light_data: + out = ResStream() + prop.prop_data.save_binary(out) + prop_bytes = out.get_bytes() + break + if mobile_bytes and prop_bytes: + return mobile_bytes, prop_bytes + raise RuntimeError('CZone.cache has no plain mobile or prop to build on') + + +def write_longs(s, values): + for value in values: + s.write_long(value) + + +def write_hash_list(s, values): + s.write_dword(len(values)) + for value in values: + s.write_dword(value) + + +def shopkeeper(s): + s.write_dword(1) + s.write_long(1000) + for value in (0.5, 1.5, 0.25, 1.25): + s.write_float(value) + s.write_dword(0) + s.write_dword(1) + s.write_qword(321) + s.write_long(-4) + + +def merchant(s): + s.write_dword(2) + for value in (0.5, 1.5, 0.25, 1.25, 0.75, 1.75): + s.write_float(value) + s.write_dword(1) + s.write_qword(123) + s.write_long(9) + s.write_dword(0) + + +def product(s): + write_longs(s, (100, 60, 2)) + s.write_bool(True) + s.write_bool(False) + write_longs(s, (3, -1, 7)) + s.write_dword(0x12345678) + s.write_dword(0x9ABCDEF0) + s.write_dword(5) + s.write_bool(True) + for value in (0x0000000100000002, 3, 0xFFFFFFFFFFFFFFFF): + s.write_qword(value) + write_longs(s, (2024, 1, 2, 3, 4, -5)) + + +def minion_element(s): + for value in (1, 2, 3): + s.write_qword(value) + write_longs(s, (10, 20, -30)) + s.write_bool(False) + write_longs(s, range(6)) + s.write_string('Minion') + s.write_string('two') + write_longs(s, (4, 5)) + s.write_string('three') + s.write_string('four') + + +def service_element(s): + s.write_qword(77) + write_longs(s, (1, -2)) + s.write_string('desc') + s.write_string('unit') + write_longs(s, (500, 6, 1)) + + +def hireling(s): + s.write_bool(True) + s.write_long(3) + s.write_string('Title') + write_longs(s, (100, 200, 300)) + s.write_bool(True) + s.write_bool(False) + s.write_long(4) + s.write_dword(2) + write_longs(s, (1, -1)) + s.write_dword(1) + s.write_long(6) + s.write_dword(2) + product(s) + product(s) + s.write_dword(1) + product(s) + s.write_dword(1) + minion_element(s) + s.write_dword(1) + service_element(s) + s.write_dword(1) + write_longs(s, (1, 2, 3, -4)) + s.write_dword(1) + s.write_qword(555) + s.write_dword(1) + s.write_long(7) + s.write_dword(1) + s.write_long(8) + for value in (0xDEADBEEF, 11, 12, 13): + write_hash_list(s, [value]) + s.write_dword(1) + write_longs(s, (1, 2, 3)) + s.write_float(0.5) + s.write_long(40) + s.write_bool(True) + s.write_bool(True) + s.write_bool(False) + write_longs(s, (1, 2)) + + +def minion(s): + hireling(s) + s.write_string('extra') + + +def instance(s, full): + s.write_qword(1) + s.write_qword(2) + s.write_tuple((1.0, 2.0, 3.0)) + s.write_tuple((1.0, 1.0, 1.0)) + for value in (0.0, 0.0, 0.0, 1.0): + s.write_float(value) + s.write_qword(3) + write_longs(s, (-1, -1, 7)) + s.write_bool(True) + s.write_string('Override') + s.write_bool(full) + if full: + s.write_float(100.0) + s.write_float(50.0) + s.write_bool(full) + if full: + s.write_qword(9) + write_longs(s, (1000, -5)) + s.write_float(0.25) + s.write_dword(0x80000001) + s.write_long(3) + s.write_dword(2) + s.write_dword(0x1111) + write_longs(s, (1, -2)) + s.write_bool(True) + s.write_qword(42) + s.write_string('a') + s.write_float(1.5) + s.write_dword(0x2222) + write_longs(s, (3, 4)) + s.write_bool(False) + s.write_dword(17) + s.write_string('b') + s.write_float(2.5) + s.write_long(20) + s.write_bool(full) + if full: + for value in (4, 5, 6): + s.write_qword(value) + s.write_long(2) + s.write_bool(full) + if full: + s.write_qword(8) + s.write_long(3) + s.write_bool(full) + if full: + s.write_bool(True) + s.write_float(2.0) + s.write_dword(0xFFFFFFFF) + s.write_long(-3) + s.write_bool(True) + write_longs(s, range(5)) + s.write_bool(False) + s.write_bool(True) + + +def container(s): + write_longs(s, (1, 2, 0, 0, 1, -1)) + s.write_dword(3) + s.write_dword(1) + for value in (10.0, 20.0, 0.5): + s.write_float(value) + s.write_dword(2) + s.write_float(0.5) + s.write_qword(1234) + s.write_dword(3) + s.write_float(0.25) + s.write_long(-7) + + +def city(s): + s.write_dword(0xAAAA) + s.write_dword(0xBBBB) + write_longs(s, (2, -100)) + + +def trigger(s, timed, has_state): + s.write_long(3) + s.write_bool(True) + s.write_bool(timed) + s.write_bool(True) + if timed: + s.write_float(1.0) + s.write_float(2.0) + s.write_bool(has_state) + if has_state: + s.write_long(-1) + write_hash_list(s, [0x11, 0x22]) + write_hash_list(s, [0x33]) + write_hash_list(s, []) + + +def event_base(s): + s.write_long(60) + s.write_float(5.0) + s.write_string('label') + s.write_string('event') + s.write_dword(0xCAFEBABE) + s.write_tuple((1.0, 2.0, 3.0)) + s.write_string('parent') + s.write_dword(0xBEEF) + s.write_bool(True) + s.write_dword(2) + trigger(s, True, True) + trigger(s, False, False) + + +def resource_event(s): + write_longs(s, (5, 10, 300, 2, -1)) + s.write_bool(True) + s.write_bool(False) + s.write_bool(True) + s.write_dword(2) + s.write_qword(1000) + s.write_float(0.5) + s.write_qword(2000) + s.write_float(0.25) + + +def event_state(s, extras): + write_longs(s, (2, 1)) + s.write_bool(True) + s.write_bool(False) + s.write_bool(True) + for value in (0x1, 0x2, 0x3): + s.write_dword(value) + s.write_bool(extras) + if extras: + write_longs(s, (1, 2, 3)) + s.write_bool(extras) + if extras: + s.write_dword(0x4) + s.write_bool(False) + s.write_long(8) + + +def state_event(s, prop_record, mobile_record): + s.write_long(1) + s.write_bool(True) + s.write_dword(2) + event_state(s, True) + event_state(s, False) + s.write_dword(1) + s.write_long(1) + s.write_bytes(prop_record) + s.write_dword(1) + s.write_long(2) + s.write_bytes(mobile_record) + + +def fractal_terrain(s): + for value in (1.0, 2.0, 3.0, 4.0): + s.write_float(value) + s.write_long(5) + s.write_float(0.5) + s.write_float(0.25) + write_longs(s, (7, -1)) + + +def mesh_terrain(s): + for value in (1.0, 2.0, 3.0, 4.0): + s.write_float(value) + s.write_qword(99) + + +def build(type_id, *parts): + s = ResStream() + s.write_dword(type_id) + for part in parts: + if isinstance(part, bytes): + s.write_bytes(part) + else: + part(s) + return s.get_bytes() + + +def mobile_with_contents(mobile_bytes): + contents = ResStream() + instance(contents, True) + instance(contents, False) + contents_bytes = contents.get_bytes() + instances = [] + stream = ResStream(contents_bytes) + for _ in range(2): + item = InstanceInfo() + item.load_binary(stream) + instances.append(item) + assert stream.buffer.tell() == len(contents_bytes), 'InstanceInfo left bytes unread' + mobile = ArcMobile() + mobile.load_binary(ResStream(build(1, mobile_bytes))) + mobile.mobile_data.mobile_has_content = True + mobile.mobile_data.mobile_inventory_contents = instances + out = ResStream() + mobile.save_binary(out) + record = out.get_bytes() + assert contents_bytes in record, 'InstanceInfo did not save back to the same bytes' + return record + + +def prop_with_light(prop_bytes): + prop = ArcProp() + prop.load_binary(ResStream(build(5, prop_bytes))) + prop.prop_data.prop_is_light_data = True + prop.prop_data.prop_light_color = [1.0, 0.5, 0.25, 1.0] + prop.prop_data.prop_light_radius = 12.0 + prop.prop_data.prop_light_flicker = -1 + prop.prop_data.prop_light_cube_map = 2 + out = ResStream() + prop.save_binary(out) + return out.get_bytes() + + +def round_trip(name, record_class, data): + stream = ResStream(data) + record = record_class() + record.load_binary(stream) + unread = len(data) - stream.buffer.tell() + rebuilt = record_class() + rebuilt.load_json(json.loads(json.dumps(record.save_json()))) + out = ResStream() + rebuilt.save_binary(out) + identical = out.get_bytes() == data + passed = unread == 0 and identical + print(f'{name:22s} unread={unread} identical={identical} {"OK" if passed else "FAIL"}') + return passed + + +def main(): + mobile_bytes, prop_bytes = sample_records() + mobile_record = build(1, mobile_bytes) + prop_record = build(5, prop_bytes) + cases = [ + ('mobile BANKER', ArcMobile, build(2, mobile_bytes)), + ('mobile SHOPKEEPER', ArcMobile, build(3, mobile_bytes, shopkeeper)), + ('mobile MERCHANT', ArcMobile, build(8, mobile_bytes, merchant)), + ('mobile HIRELING', ArcMobile, build(9, mobile_bytes, merchant, hireling)), + ('mobile PET', ArcMobile, build(10, mobile_bytes, merchant, hireling)), + ('mobile MINION', ArcMobile, build(11, mobile_bytes, merchant, minion)), + ('mobile inventory', ArcMobile, mobile_with_contents(mobile_bytes)), + ('prop light', ArcProp, prop_with_light(prop_bytes)), + ('prop CITY', ArcProp, build(6, prop_bytes, city)), + ('prop CONTAINER', ArcProp, build(7, prop_bytes, container)), + ('event EVENT', ArcZoneEvent, build(1, event_base)), + ('event RESOURCE', ArcZoneEvent, build(2, event_base, resource_event)), + ('event STATE', ArcZoneEvent, build(3, event_base, lambda s: state_event(s, prop_record, mobile_record))), + ('terrain PEAKED', ArcTerrainGen, build(1, fractal_terrain)), + ('terrain RIDGED', ArcTerrainGen, build(2, fractal_terrain)), + ('terrain ROLLING', ArcTerrainGen, build(3, fractal_terrain)), + ('terrain MESA', ArcTerrainGen, build(5, fractal_terrain)), + ('terrain MESH', ArcTerrainGen, build(6, mesh_terrain)), + ] + results = [round_trip(name, record_class, data) for name, record_class, data in cases] + print(f'{results.count(True)}/{len(results)} passed') + return 0 if all(results) else 1 + + +if __name__ == '__main__': + sys.exit(main())