Files
mbEditorPro/mbEditorPro2.0/tests/czone_synthetic.py
T
MagicBotandClaude Opus 5.5 47777669de Add synthetic round-trip test for unexercised CZone structures
The shipped CZone.cache only holds plain/trainer mobiles, plain and
city props and planar/Targa terrain gens. tests/czone_synthetic.py
builds records for every other client branch (banker, shopkeeper,
merchant, hireling, pet, minion, inventory contents, lights,
containers, all three zone event types, fractal and mesh terrain gens)
and checks each is read to the end and rebuilds byte-identical
through JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:06:09 -04:00

425 lines
12 KiB
Python

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