# • ▌ ▄ ·. ▄▄▄▄· ▄▄▄ .·▄▄▄▄ ▪ ▄▄▄▄▄ ▄▄▄ ▄▄▄·▄▄▄ # ·██ ▐███▪▐█ ▀█▪ ▀▄.▀·██▪ ██ ██ •██ ▪ ▀▄ █· ▐█ ▄█▀▄ █·▪ # ▐█ ▌▐▌▐█·▐█▀▀█▄ ▐▀▀▪▄▐█· ▐█▌▐█· ▐█.▪ ▄█▀▄ ▐▀▀▄ ██▀·▐▀▀▄ ▄█▀▄ # ██ ██▌▐█▌██▄▪▐█ ▐█▄▄▌██. ██ ▐█▌ ▐█▌·▐█▌.▐▌▐█•█▌ ▐█▪·•▐█•█▌▐█▌.▐▌ # ▀▀ █▪▀▀▀·▀▀▀▀ ▀▀▀ ▀▀▀▀▀• ▀▀▀ ▀▀▀ ▀█▄▀▪.▀ ▀ .▀ .▀ ▀ ▀█▄▀▪ # 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())