more work done

This commit is contained in:
MaddoScientisto 2026-04-09 00:32:12 +02:00
commit d323bb28fc
68 changed files with 714 additions and 19 deletions

View file

@ -0,0 +1,14 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
print(bridge.rename_function_by_address("1288:0fc3", "allocator_phase_finalize_pass"))
print(
bridge.set_decompiler_comment(
"1288:0fc3",
"Live re-anchor for raw 0009:b1c3. Calls PresentationCallbackBroker vtable slot +0x08 twice through the installed 0x4588 broker pointer, then sweeps allocator heads for finalize cleanup.",
)
)
print(bridge.get_function_by_address("1288:0fc3"))

View file

@ -0,0 +1,38 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
updates = [
{
"address": "1250:026c",
"name": "LoadEntryTableFromManifest",
"comment": (
"Constructor-selected cache-backend branch. Fetches a manifest-style buffer through the "
"object callback table, allocates the +0x10/+0x18 entry tables, and copies parsed entry "
"records into backend-owned storage."
),
},
{
"address": "1250:0749",
"name": "InitFixedEntryTable",
"comment": (
"Constructor-selected cache-backend fallback branch. Uses the current entry count and a "
"caller-supplied fixed record size to allocate default entry records and seed the +0x10/+0x18 tables."
),
},
]
for update in updates:
print(f"=== {update['address']} -> {update['name']} ===")
print(
bridge.set_function_class(
function_address=update["address"],
class_path="Remorse::CacheBackendObject",
method_name=update["name"],
)
)
print(bridge.set_decompiler_comment(update["address"], update["comment"]))
print(bridge.get_function_by_address(update["address"]))
print()

View file

@ -0,0 +1,20 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
print(
bridge.set_function_class(
function_address="1250:0910",
class_path="Remorse::CacheBackendObject",
method_name="SetEntryNameAndTag",
)
)
print(
bridge.set_decompiler_comment(
"1250:0910",
"Indexed CacheBackendObject writer. Resolves the logical entry id through the +0x18 remap table, ensures the +0x10 entry buffer exists and is large enough, stores a 4-byte tag/header, and copies the caller-supplied name string at offset +4.",
)
)
print(bridge.get_function_by_address("1250:0910"))

View file

@ -0,0 +1,39 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
updates = [
{
"address": "1430:014c",
"name": "MaterializeChecked",
"comment": (
"Owner-resource wrapper over helper vtable slot +0x0c. "
"Called by InitSlotOwnerBuffers and EnsureSlotChunkLoaded to materialize owner data "
"and assert if the first output byte is 0xff."
),
},
{
"address": "1430:0195",
"name": "QueryMaterializationSize",
"comment": (
"Owner-resource wrapper over helper vtable slot +0x04. "
"Current best read from the 1430:0000 create path is a size-query callback used ahead "
"of owner-data materialization."
),
},
]
for update in updates:
print(f"=== {update['address']} -> {update['name']} ===")
print(
bridge.set_function_class(
function_address=update["address"],
class_path="Remorse::EntityVmOwnerResource",
method_name=update["name"],
)
)
print(bridge.set_decompiler_comment(update["address"], update["comment"]))
print(bridge.get_function_by_address(update["address"]))
print()

View file

@ -0,0 +1,39 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
updates = [
{
"address": "12d0:0513",
"name": "InitOnce",
"comment": (
"Live re-anchor for the 0x4588 presentation-callback broker install path. "
"Guards on 0x4594, snapshots current video/system state into 0x458c/0x4590, installs "
"the nullable broker pointer at 0x4588, and ensures the fallback buffer at 0x45a6 exists."
),
},
{
"address": "12d0:0656",
"name": "TeardownOnce",
"comment": (
"Live re-anchor for the 0x4588 presentation-callback broker teardown path. "
"Guards on 0x4595, clears the installed broker pointer, conditionally emits broker slot +0x0c "
"when 0x4590 != 0x458c, then calls broker slot +0x04 before final video/system cleanup."
),
},
]
for update in updates:
print(f"=== {update['address']} -> {update['name']} ===")
print(
bridge.set_function_class(
function_address=update["address"],
class_path="Remorse::PresentationCallbackBroker",
method_name=update["name"],
)
)
print(bridge.set_decompiler_comment(update["address"], update["comment"]))
print(bridge.get_function_by_address(update["address"]))
print()

View file

@ -0,0 +1,129 @@
from java.util import ArrayList
from ghidra.program.model.data import DWordDataType, VoidDataType, WordDataType
from ghidra.program.model.listing import Function, ParameterImpl, ReturnParameterImpl
from ghidra.program.model.symbol import SourceType
from tools.pyghidra_crusader.common import transaction
def clone_type(data_type):
return data_type.clone(program.getDataTypeManager())
def get_required_data_type(path):
data_type = program.getDataTypeManager().getDataType(path)
if data_type is None:
raise RuntimeError("Missing datatype: %s" % path)
return data_type
def pointer_to(data_type):
return program.getDataTypeManager().getPointer(data_type)
def build_params(specs):
params = ArrayList()
for spec in specs:
if spec.get("keep"):
params.add(ParameterImpl(spec["parameter"], program))
else:
params.add(ParameterImpl(spec["name"], spec["datatype"], program, SourceType.USER_DEFINED))
return params
def update_signature(function_address, return_type, param_specs):
function = helpers["get_function"](program, function_address)
if function is None:
raise RuntimeError("Function not found: %s" % function_address)
params = build_params(param_specs)
return_param = ReturnParameterImpl(return_type, program)
function.updateFunction(
function.getCallingConventionName(),
return_param,
params,
Function.FunctionUpdateType.DYNAMIC_STORAGE_ALL_PARAMS,
True,
SourceType.USER_DEFINED,
)
return function
runtime_struct = get_required_data_type("/Remorse/EntityVmRuntime")
slot_entry_struct = get_required_data_type("/Remorse/EntityVmSlotEntry")
runtime_ptr = pointer_to(runtime_struct)
slot_entry_ptr = pointer_to(slot_entry_struct)
word_type = clone_type(WordDataType.dataType)
dword_type = clone_type(DWordDataType.dataType)
void_type = clone_type(VoidDataType.dataType)
with transaction(program, "Apply VM class type fixes"):
create_or_clear = update_signature(
"1420:2040",
slot_entry_ptr,
[
{"name": "slot_entry", "datatype": slot_entry_ptr},
],
)
acquire_slot = helpers["get_function"](program, "1420:167c")
if acquire_slot is None:
raise RuntimeError("Function not found: 1420:167c")
acquire_slot_params = acquire_slot.getParameters()
acquire_slot = update_signature(
"1420:167c",
slot_entry_ptr,
[
{"name": "this", "datatype": runtime_ptr},
{"name": "slot_index", "datatype": word_type},
{"keep": True, "parameter": acquire_slot_params[2]},
],
)
init_slot_owner_buffers = update_signature(
"1420:1866",
void_type,
[
{"name": "this", "datatype": runtime_ptr},
{"name": "slot_index", "datatype": word_type},
{"name": "slot_entry", "datatype": slot_entry_ptr},
],
)
create_runtime = update_signature(
"1420:1499",
dword_type,
[
{"name": "this", "datatype": runtime_ptr},
{"name": "owner_type", "datatype": word_type},
{"name": "owner_id", "datatype": word_type},
],
)
init_slots = update_signature(
"1420:1536",
void_type,
[
{"name": "this", "datatype": runtime_ptr},
],
)
release_slots = update_signature(
"1420:1575",
void_type,
[
{"name": "this", "datatype": runtime_ptr},
],
)
for function in [
create_or_clear,
acquire_slot,
init_slot_owner_buffers,
create_runtime,
init_slots,
release_slots,
]:
print("UPDATED", function.getEntryPoint(), function.getPrototypeString(True, True))

View file

@ -0,0 +1,33 @@
from pathlib import Path
import json
from tools.poc_crusader_usecode_parser import parse_ir
def dump_variant(label: str, extracted_root: str, class_name: str, slot: int) -> None:
ir = parse_ir(class_name, slot, extracted_root)
print(f"=== {label} {class_name} slot=0x{slot:02X} ===")
print(json.dumps(ir["class"], indent=2))
print(json.dumps(ir["event"], indent=2))
for op in ir["ops"]:
print(
json.dumps(
{
"offset": op["offset"],
"mnemonic": op["mnemonic"],
"operands": op["operands"],
"raw_bytes": op["raw_bytes"],
},
separators=(",", ":"),
)
)
def main() -> None:
root = Path(__file__).resolve().parent
dump_variant("remorse", str(root / "USECODE" / "EUSECODE_extracted"), "CHANGER", 0x07)
dump_variant("regret", str(root / "USECODE" / "REGRET" / "REGRET_USECODE_extracted"), "CHANGER", 0x07)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,21 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
comments = {
"1278:0616": (
"Verified live caller of PresentationCallbackBroker slot +0x0c through the installed 0x4588 broker pointer. "
"When the local vport state takes the fallback path and param_2 == 0, this function emits the broker callback instead of the normal direct graphics path."
),
"1320:1588": (
"Verified live caller of PresentationCallbackBroker slot +0x0c through the installed 0x4588 broker pointer. "
"Dispatch_ModalGump emits the broker callback before and after modal dispatch when the requested state pair differs from the current SuperVGA mode snapshot."
),
}
for address, comment in comments.items():
print(bridge.set_decompiler_comment(address, comment))
print(bridge.get_function_by_address(address))
print()

View file

@ -0,0 +1,22 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
comments = {
"1180:0000": (
"Live re-anchor for raw 0007:ba00 watch_entity_controller_create_global. "
"The current live NE build exposes this family more concretely as Camera_Init / Camera_CreateProcess "
"rather than under the older watch-entity-controller label."
),
"1180:0045": (
"Live re-anchor for raw 0007:ba45 watch_entity_controller_create. "
"This family now reads as the camera-process create path: allocates the process object, stores the global at 0x2bd8, and seeds the process-name row at 0x2be4."
),
}
for address, comment in comments.items():
print(bridge.set_decompiler_comment(address, comment))
print(bridge.get_function_by_address(address))
print()

View file

@ -0,0 +1,24 @@
from ghidra.program.model.data import CategoryPath, DataTypeConflictHandler, Structure, StructureDataType, WordDataType
data_type_manager = program.getDataTypeManager()
category_path = CategoryPath("/Remorse")
slot_entry = data_type_manager.getDataType("/Remorse/EntityVmSlotEntry")
if slot_entry is None:
slot_entry = StructureDataType(category_path, "EntityVmSlotEntry", 0x26)
slot_entry = data_type_manager.addDataType(slot_entry, DataTypeConflictHandler.REPLACE_HANDLER)
if not isinstance(slot_entry, Structure):
raise RuntimeError("/Remorse/EntityVmSlotEntry is not a structure: %s" % slot_entry.getClass().getName())
word_type = WordDataType.dataType.clone(data_type_manager)
slot_entry.replaceAtOffset(0x1e, word_type, 2, "owner_buffer_offset", None)
slot_entry.replaceAtOffset(0x20, word_type, 2, "owner_buffer_segment", None)
slot_entry.replaceAtOffset(0x22, word_type, 2, "chunk_state_offset", None)
slot_entry.replaceAtOffset(0x24, word_type, 2, "chunk_state_segment", None)
print("DATATYPE", slot_entry.getPathName(), slot_entry.getLength())
for component in slot_entry.getDefinedComponents():
print("FIELD", hex(component.getOffset()), component.getFieldName(), component.getDataType().getDisplayName())

View file

@ -0,0 +1,32 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
struct_result = bridge.create_or_update_struct(
name="SpriteNodeBase",
category_path="/Remorse",
size=0x2b,
fields=[
{"offset": 25, "name": "child_or_next_farptr", "datatype": "undefined4", "comment": "Child-chain pointer pair used by traversal and accumulated-position helpers.", "confidence": "high"},
{"offset": 33, "name": "local_x_offset", "datatype": "undefined2", "comment": "Summed by the live SpriteNode offset-accumulation helpers.", "confidence": "high"},
{"offset": 35, "name": "local_y_offset", "datatype": "undefined2", "comment": "Summed by the live SpriteNode offset-accumulation helpers.", "confidence": "high"},
{"offset": 41, "name": "dirty_flags", "datatype": "undefined2", "comment": "Checked by IsDirty and updated by MarkDirty.", "confidence": "high"},
],
)
print("=== struct ===")
print(struct_result)
print()
vtable_result = bridge.create_or_update_struct(
name="SpriteNodeVtable",
category_path="/Remorse",
size=40,
fields=[
{"offset": 20, "name": "event_handler_slot14", "datatype": "undefined4", "comment": "Verified DispatchEvent target for one event class.", "confidence": "high"},
{"offset": 24, "name": "event_handler_slot18", "datatype": "undefined4", "comment": "Verified DispatchEvent target for one event class.", "confidence": "high"},
{"offset": 32, "name": "event_handler_slot20", "datatype": "undefined4", "comment": "Verified DispatchEvent target for one event class.", "confidence": "high"},
{"offset": 36, "name": "event_handler_slot24", "datatype": "undefined4", "comment": "Verified DispatchEvent target for one event class.", "confidence": "high"},
],
)
print("=== vtable ===")
print(vtable_result)

View file

@ -0,0 +1,8 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["11e0:14fb", "11e0:1814", "11e0:1913", "11e0:19e6", "11e0:1a33"]:
print(f"=== {address} ===")
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,9 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["11e0:17a4", "11e0:17dc", "11e0:187e"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,22 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
RANGES = [
("11e0:1ff0", "11e0:2090"),
("11e0:2190", "11e0:2260"),
("11e0:2290", "11e0:22f0"),
("11e0:2390", "11e0:23f0"),
("11e0:24d0", "11e0:2520"),
("11e0:2580", "11e0:25d0"),
]
for start, end in RANGES:
print(f"=== {start} .. {end} ===")
result = bridge.disassemble_region(start, end)
if isinstance(result, list):
for line in result:
print(line)
else:
print(result)
print()

View file

@ -0,0 +1,70 @@
from java.util import ArrayList
from ghidra.program.model.listing import Function, ParameterImpl
from ghidra.program.model.symbol import SourceType
TARGETS = [
"1420:0eec",
"1420:10b6",
"1420:10da",
"1420:1162",
"1420:118f",
"1420:1278",
]
def main():
dtm = program.getDataTypeManager()
this_base = dtm.getDataType("/Remorse/EntityVmContext")
if this_base is None:
print("failed\tdatatype\t/Remorse/EntityVmContext not found")
return
this_type = dtm.getPointer(this_base)
ok_count = 0
failed = []
for entry_text in TARGETS:
func = helpers["get_function"](program, entry_text)
if func is None:
failed.append((entry_text, "function not found"))
print("failed\t{}\tfunction not found".format(entry_text))
continue
before = func.getPrototypeString(True, True)
params = list(func.getParameters())
replacements = ArrayList()
replacements.add(ParameterImpl("this", this_type, program, SourceType.USER_DEFINED))
for param in params[1:]:
replacements.add(ParameterImpl(param, program))
try:
transaction_id = program.startTransaction("Type EntityVmContext this")
commit = False
try:
func.replaceParameters(
replacements,
Function.FunctionUpdateType.DYNAMIC_STORAGE_ALL_PARAMS,
True,
SourceType.USER_DEFINED,
)
commit = True
finally:
program.endTransaction(transaction_id, commit)
ok_count += 1
print(
"ok\t{}\tbefore={}\tafter={}".format(
entry_text,
before,
func.getPrototypeString(True, True),
)
)
except Exception as exc:
failed.append((entry_text, str(exc)))
print("failed\t{}\t{}".format(entry_text, exc))
print("summary\tok={}\tfailed={}".format(ok_count, len(failed)))
main()

View file

@ -0,0 +1,5 @@
import re
from pathlib import Path
xml = Path('exports/CRUSADER.EXE.xml').read_text(encoding='utf-8', errors='ignore')
for m in re.finditer(r'<MEMORY_SECTION NAME="\.text" START_ADDR="([^"]+)" LENGTH="([^"]+)"', xml):
print(m.group(0))

View file

@ -0,0 +1,48 @@
from java.util import ArrayList
from ghidra.program.model.data import DWordDataType, WordDataType
from ghidra.program.model.listing import Function, ParameterImpl, ReturnParameterImpl, VariableStorage
from ghidra.program.model.symbol import SourceType
def _clone(data_type):
return data_type.clone(program.getDataTypeManager())
function = helpers["get_function"](program, "1420:1499")
if function is None:
raise RuntimeError("Function 1420:1499 not found")
dword_type = _clone(DWordDataType.dataType)
word_type = _clone(WordDataType.dataType)
runtime_offset_param = ParameterImpl("this", word_type, VariableStorage(program, 4, 2), program, SourceType.USER_DEFINED)
runtime_segment_param = ParameterImpl("runtime_segment", word_type, VariableStorage(program, 6, 2), program, SourceType.USER_DEFINED)
owner_type_param = ParameterImpl("owner_type", word_type, VariableStorage(program, 8, 2), program, SourceType.USER_DEFINED)
owner_id_param = ParameterImpl("owner_id", word_type, VariableStorage(program, 10, 2), program, SourceType.USER_DEFINED)
ax_reg = program.getRegister("AX")
dx_reg = program.getRegister("DX")
return_param = ReturnParameterImpl(dword_type, VariableStorage(program, ax_reg, dx_reg), program)
params = ArrayList()
params.add(runtime_offset_param)
params.add(runtime_segment_param)
params.add(owner_type_param)
params.add(owner_id_param)
function.updateFunction(
function.getCallingConventionName(),
return_param,
params,
Function.FunctionUpdateType.CUSTOM_STORAGE,
True,
SourceType.USER_DEFINED,
)
function.setName("Create", SourceType.USER_DEFINED)
function.setComment("Factory-style runtime creator. Uses split 16-bit this/segment parameters so Ghidra can represent the incoming far runtime pointer without corrupting decompilation.")
print("updated", function.getEntryPoint(), function.getSignature())
for param in function.getParameters():
print("param", param.getName(), param.getDataType().getDisplayName(), param.getVariableStorage())
print("return", function.getReturnType().getDisplayName(), function.getReturn().getVariableStorage())

View file

@ -0,0 +1,50 @@
from java.util import ArrayList
from ghidra.program.model.data import DWordDataType
from ghidra.program.model.listing import Function, ParameterImpl, ReturnParameterImpl, VariableStorage
from ghidra.program.model.symbol import SourceType
from tools.pyghidra_crusader.common import transaction
function = helpers["get_function"](program, "1420:1499")
if function is None:
raise RuntimeError("Function 1420:1499 not found")
runtime_type = program.getDataTypeManager().getDataType("/Remorse/EntityVmRuntime")
if runtime_type is None:
raise RuntimeError("Missing datatype /Remorse/EntityVmRuntime")
runtime_ptr = program.getDataTypeManager().getPointer(runtime_type)
dword_type = DWordDataType.dataType.clone(program.getDataTypeManager())
word_type = program.getDataTypeManager().getDataType("/undefined2")
if word_type is None:
raise RuntimeError("Missing datatype /undefined2")
this_param = ParameterImpl("this", runtime_ptr, VariableStorage(program, 4, 4), program, SourceType.USER_DEFINED)
owner_type_param = ParameterImpl("owner_type", word_type, VariableStorage(program, 8, 2), program, SourceType.USER_DEFINED)
owner_id_param = ParameterImpl("owner_id", word_type, VariableStorage(program, 10, 2), program, SourceType.USER_DEFINED)
ax_reg = program.getRegister("AX")
dx_reg = program.getRegister("DX")
return_param = ReturnParameterImpl(dword_type, VariableStorage(program, ax_reg, dx_reg), program)
params = ArrayList()
params.add(this_param)
params.add(owner_type_param)
params.add(owner_id_param)
with transaction(program, "Repair EntityVmRuntime Create custom storage"):
function.updateFunction(
function.getCallingConventionName(),
return_param,
params,
Function.FunctionUpdateType.CUSTOM_STORAGE,
True,
SourceType.USER_DEFINED,
)
print("UPDATED", function.getEntryPoint(), function.getSignature())
for param in function.getParameters():
print("PARAM", param.getName(), param.getDataType().getDisplayName(), param.getVariableStorage())
print("RETURN", function.getReturnType().getDisplayName(), function.getReturn().getVariableStorage())

View file

@ -0,0 +1,50 @@
from java.util import ArrayList
from ghidra.program.model.data import DWordDataType, WordDataType
from ghidra.program.model.listing import Function, ParameterImpl, ReturnParameterImpl, VariableStorage
from ghidra.program.model.symbol import SourceType
def _clone(data_type):
return data_type.clone(program.getDataTypeManager())
function = helpers["get_function"](program, "1000:42e2")
if function is None:
raise RuntimeError("Function 1000:42e2 not found")
dword_type = _clone(DWordDataType.dataType)
word_type = _clone(WordDataType.dataType)
param_1 = ParameterImpl("base_ptr", dword_type, VariableStorage(program, 4, 4), program, SourceType.USER_DEFINED)
blocksize = ParameterImpl("blocksize", word_type, VariableStorage(program, 8, 2), program, SourceType.USER_DEFINED)
nblocks = ParameterImpl("nblocks", word_type, VariableStorage(program, 10, 2), program, SourceType.USER_DEFINED)
param_4 = ParameterImpl("param_4", word_type, VariableStorage(program, 12, 2), program, SourceType.USER_DEFINED)
param_5 = ParameterImpl("param_5", word_type, VariableStorage(program, 14, 2), program, SourceType.USER_DEFINED)
transform_func = ParameterImpl("transform_func", dword_type, VariableStorage(program, 16, 4), program, SourceType.USER_DEFINED)
ax_reg = program.getRegister("AX")
dx_reg = program.getRegister("DX")
return_param = ReturnParameterImpl(dword_type, VariableStorage(program, ax_reg, dx_reg), program)
params = ArrayList()
params.add(param_1)
params.add(blocksize)
params.add(nblocks)
params.add(param_4)
params.add(param_5)
params.add(transform_func)
function.updateFunction(
function.getCallingConventionName(),
return_param,
params,
Function.FunctionUpdateType.CUSTOM_STORAGE,
True,
SourceType.USER_DEFINED,
)
print("updated", function.getEntryPoint(), function.getSignature())
for parameter in function.getParameters():
print("param", parameter.getName(), parameter.getDataType().getDisplayName(), parameter.getVariableStorage())
print("return", function.getReturnType().getDisplayName(), function.getReturn().getVariableStorage())

View file

@ -0,0 +1,55 @@
def dump_function(address):
function = helpers["get_function"](program, address)
if function is None:
raise RuntimeError("Function %s not found" % address)
frame = function.getStackFrame()
symbol_table = program.getSymbolTable()
print("FUNCTION", address)
print("SIGNATURE", function.getSignature())
print("CALLING_CONVENTION", function.getCallingConventionName())
print("CUSTOM_STORAGE", function.hasCustomVariableStorage())
print("STACK_PURGE_SIZE", function.getStackPurgeSize())
print("STACK_PURGE_VALID", function.isStackPurgeSizeValid())
print("HAS_NO_RETURN", function.hasNoReturn())
print("ENTRY", function.getEntryPoint())
print("BODY", function.getBody().getMinAddress(), function.getBody().getMaxAddress())
print("PARENT_NAMESPACE", function.getParentNamespace())
print("STACK_GROWS_NEGATIVE", frame.growsNegative())
print("LOCAL_SIZE", frame.getLocalSize())
print("PARAM_OFFSET", frame.getParameterOffset())
print("RETURN_ADDR_OFFSET", frame.getReturnAddressOffset())
print("FRAME_SIZE", frame.getFrameSize())
print("PARAM_SIZE", frame.getParameterSize())
print("SYMBOLS_AT_ENTRY")
for symbol in symbol_table.getSymbols(function.getEntryPoint()):
print(" ", symbol.getName(True), symbol.getSymbolType(), symbol.getSource())
print("PARAMETERS")
for parameter in function.getParameters():
print(" ", parameter.getName(), parameter.getDataType().getDisplayName(), parameter.getVariableStorage())
print("LOCALS")
for variable in function.getLocalVariables():
print(" ", variable.getName(), variable.getDataType().getDisplayName(), variable.getVariableStorage(), type(variable).__name__)
print("ALL_VARS")
for variable in function.getAllVariables():
print(" ", variable.getName(), variable.getDataType().getDisplayName(), variable.getVariableStorage(), type(variable).__name__)
print("STACK_REFERENCES")
listing = program.getListing()
instruction = listing.getInstructionAt(function.getEntryPoint())
while instruction is not None and function.getBody().contains(instruction.getAddress()):
operands = []
for operand_index in range(instruction.getNumOperands()):
references = instruction.getOperandReferences(operand_index)
if references:
operands.append((operand_index, [str(reference) for reference in references]))
print(instruction.getAddress(), instruction, operands)
instruction = instruction.getNext()
print()
dump_function("1420:1499")
dump_function("1430:0000")

View file

@ -0,0 +1,197 @@
const fs = require("fs");
function readU32LE(buffer, offset) {
return buffer.readUInt32LE(offset);
}
function readU16LE(buffer, offset) {
return buffer.readUInt16LE(offset);
}
function parseLsetWdl(data) {
const headerSize = readU32LE(data, 0);
if (headerSize !== 0x34 || headerSize > data.length) {
throw new Error(`unexpected header size ${headerSize}`);
}
const headerWords = [];
for (let offset = 0; offset < headerSize; offset += 4) {
headerWords.push(readU32LE(data, offset));
}
const audioSize = headerWords[1];
const sectionSizes = [];
for (let offset = 0x08; offset < 0x38; offset += 4) {
sectionSizes.push(readU32LE(data, offset));
}
const sections = [];
let cursor = headerSize + audioSize;
for (let index = 0; index < sectionSizes.length; index += 1) {
const size = sectionSizes[index];
if (size <= 0 || cursor + size > data.length) {
break;
}
sections.push({
index,
name: `post_audio_section_${String(index).padStart(2, "0")}`,
offset: cursor,
size
});
cursor += size;
}
return { headerWords, sections };
}
function readSectionBytes(data, section) {
return data.subarray(section.offset, section.offset + section.size);
}
function parseTypedSection8(data, section) {
const bytes = readSectionBytes(data, section);
if (bytes.length < 8) {
return null;
}
const recordCount = readU32LE(bytes, 0);
const payloadBytes = readU32LE(bytes, 4);
const headerOffset = 8 + payloadBytes;
if (recordCount <= 0 || recordCount > 0x400) {
return null;
}
if (payloadBytes < 0 || headerOffset + recordCount * 8 > bytes.length) {
return null;
}
let payloadCursor = 8;
const records = [];
for (let index = 0; index < recordCount; index += 1) {
const descriptorOffset = headerOffset + index * 8;
const blockSize = readU32LE(bytes, descriptorOffset);
const typeId = readU32LE(bytes, descriptorOffset + 4);
if (blockSize < 0 || payloadCursor + blockSize > headerOffset) {
return null;
}
const payload = bytes.subarray(payloadCursor, payloadCursor + blockSize);
const payloadDwords = [];
for (let offset = 0; offset + 4 <= payload.length; offset += 4) {
payloadDwords.push(readU32LE(payload, offset));
}
records.push({ index, typeId, blockSize, payloadDwords });
payloadCursor += blockSize;
}
return { section, recordCount, payloadBytes, records };
}
function parseTypedSection16(data, section) {
const bytes = readSectionBytes(data, section);
if (bytes.length < 8) {
return null;
}
const recordCount = readU32LE(bytes, 0);
const payloadBytes = readU32LE(bytes, 4);
const headerOffset = 8 + payloadBytes;
if (recordCount <= 0 || recordCount > 0x400) {
return null;
}
if (payloadBytes < 0 || headerOffset + recordCount * 16 > bytes.length) {
return null;
}
let payloadCursor = 8;
const records = [];
for (let index = 0; index < recordCount; index += 1) {
const descriptorOffset = headerOffset + index * 16;
const d4Size = readU32LE(bytes, descriptorOffset);
const ccSize = readU32LE(bytes, descriptorOffset + 4);
const d0Size = readU32LE(bytes, descriptorOffset + 8);
const typeId = readU16LE(bytes, descriptorOffset + 12);
const variantTypeId = readU16LE(bytes, descriptorOffset + 14);
const ccPayload = bytes.subarray(payloadCursor, payloadCursor + ccSize);
const d0Payload = bytes.subarray(payloadCursor + ccSize, payloadCursor + ccSize + d0Size);
const d4Payload = bytes.subarray(payloadCursor + ccSize + d0Size, payloadCursor + ccSize + d0Size + d4Size);
if (payloadCursor + ccSize + d0Size + d4Size > headerOffset) {
return null;
}
records.push({
index,
typeId,
variantTypeId,
ccSize,
d0Size,
d4Size,
ccDwords: readDwords(ccPayload),
d0Dwords: readDwords(d0Payload),
d4Dwords: readDwords(d4Payload)
});
payloadCursor += ccSize + d0Size + d4Size;
}
return { section, recordCount, payloadBytes, records };
}
function readDwords(payload) {
const values = [];
for (let offset = 0; offset + 4 <= payload.length; offset += 4) {
values.push(readU32LE(payload, offset));
}
return values;
}
function main() {
const filePath = process.argv[2];
const wantedTypes = new Set(process.argv.slice(3).map((value) => Number.parseInt(value, 16)));
const data = fs.readFileSync(filePath);
const parsed = parseLsetWdl(data);
const section8 = parsed.sections
.map((section) => parseTypedSection8(data, section))
.filter(Boolean)
.sort((left, right) => right.recordCount - left.recordCount || right.payloadBytes - left.payloadBytes)[0];
const section16 = parsed.sections
.map((section) => parseTypedSection16(data, section))
.filter(Boolean)
.sort((left, right) => right.recordCount - left.recordCount || right.payloadBytes - left.payloadBytes)[0];
const summary = {
filePath,
section8: section8 ? {
section: section8.section.name,
offset: `0x${section8.section.offset.toString(16)}`,
size: `0x${section8.section.size.toString(16)}`,
recordCount: section8.recordCount,
wanted: section8.records
.filter((record) => wantedTypes.has(record.typeId))
.map((record) => ({
index: record.index,
typeId: `0x${record.typeId.toString(16)}`,
blockSize: `0x${record.blockSize.toString(16)}`,
payloadDwords: record.payloadDwords.map((value) => `0x${value.toString(16)}`)
}))
} : null,
section16: section16 ? {
section: section16.section.name,
offset: `0x${section16.section.offset.toString(16)}`,
size: `0x${section16.section.size.toString(16)}`,
recordCount: section16.recordCount,
wanted: section16.records
.filter((record) => wantedTypes.has(record.typeId) || wantedTypes.has(record.variantTypeId))
.map((record) => ({
index: record.index,
typeId: `0x${record.typeId.toString(16)}`,
variantTypeId: `0x${record.variantTypeId.toString(16)}`,
ccSize: `0x${record.ccSize.toString(16)}`,
d0Size: `0x${record.d0Size.toString(16)}`,
d4Size: `0x${record.d4Size.toString(16)}`,
ccDwords: record.ccDwords.map((value) => `0x${value.toString(16)}`),
d0Dwords: record.d0Dwords.map((value) => `0x${value.toString(16)}`),
d4Dwords: record.d4Dwords.map((value) => `0x${value.toString(16)}`)
}))
} : null
};
console.log(JSON.stringify(summary, null, 2));
}
main();

View file

@ -0,0 +1,35 @@
const fs = require('fs');
const path = 'k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-248/b27ea0d8d2a1a391/scene.json';
const scene = JSON.parse(fs.readFileSync(path, 'utf8'));
const items = scene.items.filter((item) => item.shapeDefId === 'shape:1232');
function qlo(item) {
return item.quality & 0xff;
}
function dist(a, b) {
return Math.hypot(a.world.x - b.world.x, a.world.y - b.world.y);
}
for (const item of items.filter((entry) => entry.npcPreview?.name === 'Observer')) {
const pairs = items.filter((candidate) => candidate.id !== item.id && candidate.frame !== item.frame && qlo(candidate) === qlo(item) && dist(candidate, item) <= 128);
console.log(JSON.stringify({
id: item.id,
src: item.mapSourceIndex,
frame: item.frame,
mapNum: item.mapNum,
npcNum: item.npcNum,
qlo: qlo(item),
world: item.world,
pairs: pairs.map((candidate) => ({
id: candidate.id,
src: candidate.mapSourceIndex,
frame: candidate.frame,
mapNum: candidate.mapNum,
npcNum: candidate.npcNum,
name: candidate.npcPreview?.name || null,
qlo: qlo(candidate)
}))
}));
}

View file

@ -0,0 +1,166 @@
from __future__ import annotations
import struct
from pathlib import Path
MAGIC_DATA_OFF = 33000
OPCODE_NAMES = {
0x81: "set_unused_field53",
0x82: "clear_unused_field53",
0x84: "set_target_objid",
0x85: "anim_walk",
0x86: "anim_run",
0x87: "anim_retreat",
0x88: "turn_left_90",
0x89: "turn_right_90",
0x8A: "fire_small_if_clear",
0x8B: "fire_large_if_clear",
0x8C: "anim_stand",
0x8D: "pathfind_home",
0x8E: "pathfind_target",
0x8F: "pathfind_midpoint",
0x92: "anim_kneel_and_fire",
0x93: "sleep_scaled",
0x94: "loiter",
0x95: "face_target",
0x96: "set_activity",
0x97: "switch_tactic",
0x98: "teleport_home",
0x99: "terminate",
0x9A: "jump_if_dist_lt_481",
0x9B: "jump_if_dist_gt_160",
0x9C: "jump_if_shot_blocked",
0x9D: "jump_if_shot_clear",
0x9E: "random_jump_nonzero",
0x9F: "loop_begin",
0xA6: "pathfind_marker_frame",
0xA7: "face_north",
0xA8: "face_south",
0xA9: "face_east",
0xAA: "face_west",
0xAB: "face_northeast",
0xAC: "face_southwest",
0xAD: "face_southeast",
0xAE: "face_northwest",
0xAF: "var_set",
0xB0: "var_add",
0xB1: "var_sub",
0xB2: "var_mul",
0xB3: "var_div",
0xB4: "var_store_curdir",
0xB5: "set_dir_raw",
0xB6: "var_store_curdir_again",
0xB7: "anim_kneeling_retreat",
0xB8: "anim_kneeling_advance",
0xB9: "anim_kneeling_slow_retreat",
0xC0: "jump",
0xC1: "loop_end",
0xFF: "flip_to_block1_restart",
}
def format_word(value: int) -> str:
if value >= MAGIC_DATA_OFF:
return f"var[{value - MAGIC_DATA_OFF}]"
return f"0x{value:04x} ({value})"
def decode_block(block: bytes, start_offset: int) -> list[str]:
pc = 0
lines: list[str] = []
def need_word(use_data: bool) -> int:
nonlocal pc
value = struct.unpack_from("<H", block, pc)[0]
pc += 2
return value
while pc < len(block):
opcode_offset = start_offset + pc
opcode = block[pc]
pc += 1
opname = OPCODE_NAMES.get(opcode, f"opcode_{opcode:02x}")
operands: list[str] = []
if opcode in {0x81, 0x84, 0x93, 0x94, 0x96, 0x97, 0x9A, 0x9B, 0x9C, 0x9D, 0xB5, 0xC0, 0x9F}:
operands.append(format_word(need_word(True)))
elif opcode == 0x9E:
operands.append(format_word(need_word(True)))
operands.append(format_word(need_word(True)))
elif opcode == 0xA6:
operands.append(f"frame={format_word(need_word(True))}")
elif opcode in {0xAF}:
operands.append(format_word(need_word(True)))
operands.append(format_word(need_word(False)))
elif opcode in {0xB0, 0xB1, 0xB2, 0xB3}:
operands.append(format_word(need_word(False)))
operands.append(format_word(need_word(True)))
elif opcode in {0xB4, 0xB6}:
operands.append(format_word(need_word(False)))
text = opname
if operands:
text += " " + ", ".join(operands)
lines.append(f"0x{opcode_offset:04x}: {text}")
return lines
def read_u32(data: bytes, offset: int) -> int:
return struct.unpack_from("<I", data, offset)[0]
def read_u16(data: bytes, offset: int) -> int:
return struct.unpack_from("<H", data, offset)[0]
def main() -> None:
path = Path("STATIC/COMBAT.DAT")
data = path.read_bytes()
print(f"file={path} size={len(data)}")
print(f"header_magic_fill={data[:0x56].hex()[:32]}...")
entries = []
for index in range(64):
off = 0x80 + index * 8
rec_off = read_u32(data, off)
rec_len = read_u32(data, off + 4)
if rec_off == 0 and rec_len == 0:
continue
entries.append((index, rec_off, rec_len))
print(f"entry_count={len(entries)}")
for index, rec_off, rec_len in entries:
rec = data[rec_off:rec_off + rec_len]
name = rec[:16].split(b"\0", 1)[0].decode("ascii")
block_offsets = [read_u16(rec, 16 + i * 2) for i in range(4)]
valid_blocks = [value for value in block_offsets if value and value < rec_len]
print(
f"[{index:02d}] off=0x{rec_off:04x} len=0x{rec_len:04x}"
f" name={name:<16} block_offsets={block_offsets}"
f" body_len={rec_len - min(valid_blocks) if valid_blocks else 0}"
)
for block_no, block_off in enumerate(block_offsets[:2]):
if not block_off or block_off >= rec_len:
continue
next_offsets = sorted(value for value in block_offsets[:2] if value > block_off and value < rec_len)
block_end = next_offsets[0] if next_offsets else rec_len
block = rec[block_off:block_end]
print(
f" block{block_no}: start=0x{block_off:04x} end=0x{block_end:04x}"
f" size={block_end - block_off:02d} bytes={block.hex(' ')}"
)
for line in decode_block(block, block_off):
print(f" {line}")
trailing_offsets = block_offsets[2:]
print(f" extra_offsets_unused={trailing_offsets}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,11 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["1288:0fd1", "1278:06b1", "1320:15e9"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,9 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["0009:5600", "0009:5601"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,23 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["1250:0000", "1250:0001"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()
for address in ["1250:026c", "1250:0749"]:
print(f"=== helper {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print(bridge.get_xrefs_to(address))
print()
for address in ["1250:0910"]:
print(f"=== helper {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print(bridge.get_xrefs_to(address))
print()

View file

@ -0,0 +1,9 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["12d8:03b4", "12d8:03d6", "12d8:0451"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,9 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["13a0:0291", "13a0:045c"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,30 @@
from pathlib import Path
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
ADDRESSES = [
"11e0:14fb",
"11e0:1814",
"11e0:1913",
"11e0:19e6",
"11e0:1a33",
"11e0:2000",
"11e0:21a3",
"11e0:21ec",
"11e0:2238",
"11e0:22ab",
"11e0:23af",
"11e0:24ea",
"11e0:251b",
"11e0:25a1",
]
for address in ADDRESSES:
print(f"=== {address} ===")
try:
print(bridge.get_function_containing(address))
except Exception as exc:
print(f"get_function_containing failed: {exc}")
print()

View file

@ -0,0 +1,9 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["11e8:0000", "11e8:01a3"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,9 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["11e8:01fa", "11e8:0246", "11e8:02b9", "11e8:03bd", "11e8:04f8", "11e8:0529", "11e8:05af"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,38 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["1430:0000", "1430:00fd"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()
for address in ["1220:0000", "1478:67b6", "1478:1228"]:
print(f"=== symbol {address} ===")
if hasattr(bridge, "get_symbol_at"):
print(bridge.get_symbol_at(address))
else:
print(bridge.get_function_by_address(address))
print()
for address in ["1220:0000"]:
print(f"=== decompile {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()
for address in ["1420:1866", "1420:19fd"]:
print(f"=== runtime {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()
for address in ["1430:014c", "1430:0195"]:
print(f"=== owner helper {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print(bridge.get_xrefs_to(address))
print()

View file

@ -0,0 +1,132 @@
const fs = require("fs");
function readU32LE(buffer, offset) {
return buffer.readUInt32LE(offset);
}
function readU16LE(buffer, offset) {
return buffer.readUInt16LE(offset);
}
function parseLsetWdl(data) {
const headerSize = readU32LE(data, 0);
if (headerSize !== 0x34 || headerSize > data.length) {
throw new Error(`unexpected header size ${headerSize}`);
}
const headerWords = [];
for (let offset = 0; offset < headerSize; offset += 4) {
headerWords.push(readU32LE(data, offset));
}
const audioSize = headerWords[1];
const sectionSizes = [];
for (let offset = 0x08; offset < 0x38; offset += 4) {
sectionSizes.push(readU32LE(data, offset));
}
const sections = [];
let cursor = headerSize + audioSize;
for (let index = 0; index < sectionSizes.length; index += 1) {
const size = sectionSizes[index];
if (size <= 0 || cursor + size > data.length) {
break;
}
sections.push({
index,
name: `post_audio_section_${String(index).padStart(2, "0")}`,
offset: cursor,
size
});
cursor += size;
}
return { sections };
}
function parseTypedSection16(data, section, startOffset) {
const bytes = data.subarray(section.offset + startOffset, section.offset + section.size);
if (bytes.length < 8) {
return null;
}
const recordCount = readU32LE(bytes, 0);
const payloadBytes = readU32LE(bytes, 4);
const headerOffset = 8 + payloadBytes;
if (recordCount <= 0 || recordCount > 0x400) {
return null;
}
if (payloadBytes < 0 || headerOffset + recordCount * 16 > bytes.length) {
return null;
}
let payloadCursor = 8;
const records = [];
for (let index = 0; index < recordCount; index += 1) {
const descriptorOffset = headerOffset + index * 16;
const d4Size = readU32LE(bytes, descriptorOffset);
const ccSize = readU32LE(bytes, descriptorOffset + 4);
const d0Size = readU32LE(bytes, descriptorOffset + 8);
const typeId = readU16LE(bytes, descriptorOffset + 12);
const variantTypeId = readU16LE(bytes, descriptorOffset + 14);
const endOffset = payloadCursor + ccSize + d0Size + d4Size;
if (endOffset > headerOffset) {
return null;
}
records.push({ index, typeId, variantTypeId, ccSize, d0Size, d4Size, payloadCursor });
payloadCursor = endOffset;
}
return {
sectionName: section.name,
startOffset,
recordCount,
payloadBytes,
headerOffset,
records
};
}
function summarizeCandidate(candidate, wantedTypes) {
return {
sectionName: candidate.sectionName,
startOffset: `0x${candidate.startOffset.toString(16)}`,
recordCount: candidate.recordCount,
payloadBytes: `0x${candidate.payloadBytes.toString(16)}`,
wanted: candidate.records
.filter((record) => wantedTypes.has(record.typeId) || wantedTypes.has(record.variantTypeId))
.map((record) => ({
index: record.index,
typeId: `0x${record.typeId.toString(16)}`,
variantTypeId: `0x${record.variantTypeId.toString(16)}`,
ccSize: `0x${record.ccSize.toString(16)}`,
d0Size: `0x${record.d0Size.toString(16)}`,
d4Size: `0x${record.d4Size.toString(16)}`,
payloadCursor: `0x${record.payloadCursor.toString(16)}`
}))
};
}
function main() {
const filePath = process.argv[2];
const wantedTypes = new Set(process.argv.slice(3).map((value) => Number.parseInt(value, 16)));
const data = fs.readFileSync(filePath);
const parsed = parseLsetWdl(data);
const candidates = [];
for (const section of parsed.sections) {
for (let startOffset = 0; startOffset < Math.min(section.size, 0x800); startOffset += 4) {
const candidate = parseTypedSection16(data, section, startOffset);
if (!candidate) {
continue;
}
const summary = summarizeCandidate(candidate, wantedTypes);
if (summary.wanted.length > 0) {
candidates.push(summary);
}
}
}
console.log(JSON.stringify(candidates, null, 2));
}
main();

View file

@ -0,0 +1,111 @@
const fs = require("fs");
const ALLOWED_U5 = new Set([0x20, 0x22, 0x30]);
function readU32LE(buffer, offset) {
return buffer.readUInt32LE(offset);
}
function readU16LE(buffer, offset) {
return buffer.readUInt16LE(offset);
}
function isStructuredCandidate(record) {
if (record[0] >= 0x200) {
return false;
}
if (record[1] === 0 && record[2] === 0) {
return false;
}
if (record[1] >= 0x4000 || record[2] >= 0x4000) {
return false;
}
if (record[3] > 0x20 || record[4] > 0x04) {
return false;
}
return ALLOWED_U5.has(record[5]);
}
function probeFile(filePath) {
const data = fs.readFileSync(filePath);
const headerSize = readU32LE(data, 0);
const audioSize = readU32LE(data, 4);
const sectionSizes = [];
for (let offset = 8; offset < 0x38; offset += 4) {
sectionSizes.push(readU32LE(data, offset));
}
let cursor = headerSize + audioSize;
const sections = [];
for (let index = 0; index < sectionSizes.length; index += 1) {
const size = sectionSizes[index];
const start = cursor;
const end = start + size;
const bytes = data.subarray(start, end);
cursor = end;
let rowCount = null;
let rootHits = 0;
let bulkHits = 0;
if (bytes.length >= 4) {
rowCount = readU32LE(bytes, 0);
for (let rowIndex = 0; rowIndex < Math.min(rowCount, 5000); rowIndex += 1) {
const base = 4 + rowIndex * 24;
if (base + 24 > bytes.length) {
break;
}
const words = [];
for (let wordIndex = 0; wordIndex < 12; wordIndex += 1) {
words.push(readU16LE(bytes, base + wordIndex * 2));
}
const left = [words[4], words[5], words[0], words[1], words[2], words[3]];
const right = [words[10], words[11], words[6], words[7], words[8], words[9]];
if (isStructuredCandidate(left)) {
rootHits += 1;
}
if (isStructuredCandidate(right)) {
rootHits += 1;
}
}
}
const usableSize = bytes.length - (bytes.length % 24);
for (let offset = 0; offset < usableSize; offset += 24) {
for (const sideOffset of [0, 12]) {
const base = offset + sideOffset;
const record = [
readU16LE(bytes, base),
readU16LE(bytes, base + 2),
readU16LE(bytes, base + 4),
readU16LE(bytes, base + 6),
readU16LE(bytes, base + 8),
readU16LE(bytes, base + 10)
];
if (isStructuredCandidate(record)) {
bulkHits += 1;
}
}
}
sections.push({
index,
start,
size,
rowCount,
rootHits,
bulkHits
});
}
return {
filePath,
headerSize,
audioSize,
sections
};
}
for (const filePath of process.argv.slice(2)) {
console.log(JSON.stringify(probeFile(filePath), null, 2));
}

View file

@ -0,0 +1,9 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["1320:0b18", "1320:0b29", "1320:0b44"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,9 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["1360:0955", "1360:0790", "1360:10d8"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,9 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["1360:0580", "1360:05a6", "1360:0cb2", "1360:12ee"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,9 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["000a:b988", "000b:326e", "000b:3380", "000b:33a6", "000b:3ab2", "000b:40ee"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,9 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["1360:0380", "1360:03af", "1360:0483", "1360:0cd7", "1360:0d79"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,11 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
for address in ["1180:000f", "1180:00a8", "1130:3038", "1130:31cc", "1130:3269", "12d0:0516", "12d0:0668"]:
print(f"=== {address} ===")
print(bridge.get_function_containing(address))
print(bridge.decompile_function_by_address(address))
print()

View file

@ -0,0 +1,314 @@
from __future__ import annotations
import bisect
import json
import struct
import sys
from pathlib import Path
ROOT = Path(r"k:/ghidra/Crusader_Decomp")
BUNDLE_DIR = ROOT / "out/psx_wdl/L0/sprite_bundles/bundle_000A1B04"
FRAME_PATH = BUNDLE_DIR / "frame_000.bin"
BUNDLE_JSON = BUNDLE_DIR / "bundle.json"
GPU_PATH = ROOT / "binary/Crusader - No Remorse (USA) GPU RAM.bin"
L0_WDL_PATH = Path(r"e:/emu/psx/Crusader - No Remorse/LSET1/L0.WDL")
ROW_BYTES = 2048
GPU_ROWS = 512
TOP_N = 10
FRAMEBUFFER_WIDTH = 320
FRAMEBUFFER_HEIGHT = 240
MATCH_TOP_N = 12
sys.path.insert(0, str(ROOT / "tools"))
from psx_extract_wdl import colorize_indexed_pixels, psx_555_to_rgba, write_overview_grid, write_psx_16bpp_png
def find_all(haystack: bytes, needle: bytes):
start = 0
while True:
index = haystack.find(needle, start)
if index < 0:
return
yield index
start = index + 1
def count_row_mismatches(left: bytes, right: bytes) -> int:
return sum(a != b for a, b in zip(left, right))
def is_exact_at(rows: list[bytes], candidate_rows: list[bytes], x: int, y: int, width: int) -> bool:
for dy, src in enumerate(candidate_rows):
if rows[y + dy][x : x + width] != src:
return False
return True
def near_score(
rows: list[bytes],
candidate_rows: list[bytes],
x: int,
y: int,
width: int,
cutoff: int | None,
) -> tuple[int, list[int], bool]:
total = 0
row_mismatches: list[int] = []
for dy, src in enumerate(candidate_rows):
seg = rows[y + dy][x : x + width]
mismatch = 0 if seg == src else count_row_mismatches(seg, src)
total += mismatch
row_mismatches.append(mismatch)
if cutoff is not None and total > cutoff:
return total, row_mismatches, True
return total, row_mismatches, False
def rgba_from_words(words: tuple[int, ...]) -> list[tuple[int, int, int]]:
return [psx_555_to_rgba(word)[:3] for word in words]
def candidate_match_score(
framebuffer_rgb: list[tuple[int, int, int]],
framebuffer_width: int,
framebuffer_height: int,
rgba: bytes,
width: int,
height: int,
guess_x: int,
guess_y: int,
radius: int = 12,
step: int = 2,
) -> tuple[int, int, int]:
best_score: int | None = None
best_x = -1
best_y = -1
x_min = max(0, guess_x - radius)
x_max = min(framebuffer_width - width, guess_x + radius)
y_min = max(0, guess_y - radius)
y_max = min(framebuffer_height - height, guess_y + radius)
for y in range(y_min, y_max + 1):
for x in range(x_min, x_max + 1):
score = 0
samples = 0
for sy in range(0, height, step):
screen_row = (y + sy) * framebuffer_width
sprite_row = sy * width * 4
for sx in range(0, width, step):
src = sprite_row + sx * 4
if rgba[src + 3] == 0:
continue
screen_r, screen_g, screen_b = framebuffer_rgb[screen_row + x + sx]
red = rgba[src]
green = rgba[src + 1]
blue = rgba[src + 2]
score += abs(screen_r - red) + abs(screen_g - green) + abs(screen_b - blue)
samples += 1
if samples == 0:
continue
normalized = score // samples
if best_score is None or normalized < best_score:
best_score = normalized
best_x = x
best_y = y
if best_score is None:
return 1 << 30, -1, -1
return best_score, best_x, best_y
def main() -> None:
bundle = json.loads(BUNDLE_JSON.read_text(encoding="ascii"))
frame_meta = next(frame for frame in bundle["exported_frames"] if frame["index"] == 0)
width = frame_meta["width"]
height = frame_meta["height"]
mode = bundle["mode"]
frame = FRAME_PATH.read_bytes()
expected = width * height
if len(frame) != expected:
raise SystemExit(f"frame byte size mismatch: got {len(frame)}, expected {expected}")
if mode != 1:
raise SystemExit(f"unexpected mode {mode}, expected 1 for 8bpp")
gpu = GPU_PATH.read_bytes()
if len(gpu) != ROW_BYTES * GPU_ROWS:
raise SystemExit(f"unexpected GPU dump size {len(gpu)}")
l0_data = L0_WDL_PATH.read_bytes()
palette_offset = int.from_bytes(l0_data[8:12], "little")
palette_size = int.from_bytes(l0_data[12:16], "little")
if palette_size != 0x1000:
raise SystemExit(f"unexpected palette size 0x{palette_size:X}")
palette_blob = l0_data[palette_offset : palette_offset + palette_size]
palettes_256 = [palette_blob[offset : offset + 0x200] for offset in range(0, len(palette_blob), 0x200)]
rows = [gpu[y * ROW_BYTES : (y + 1) * ROW_BYTES] for y in range(GPU_ROWS)]
frame_rows = [frame[i * width : (i + 1) * width] for i in range(height)]
flip_rows = [row[::-1] for row in frame_rows]
normal_hits: list[tuple[int, int]] = []
flipped_hits: list[tuple[int, int]] = []
for y in range(GPU_ROWS - height + 1):
row = rows[y]
normal_hits.extend((x, y) for x in find_all(row, frame_rows[0]))
flipped_hits.extend((x, y) for x in find_all(row, flip_rows[0]))
exact_normal = [(x, y) for x, y in normal_hits if is_exact_at(rows, frame_rows, x, y, width)]
exact_flipped = [(x, y) for x, y in flipped_hits if is_exact_at(rows, flip_rows, x, y, width)]
print(f"bundle_offset=0x{bundle['offset']:X} mode={mode} frame_count={bundle['frame_count']}")
print(
"frame0 "
f"width={width} height={height} origin=({frame_meta['origin_x']},{frame_meta['origin_y']}) "
f"data_start={frame_meta['data_start']} consumed={frame_meta['consumed']}"
)
print(f"frame_bytes={len(frame)} gpu_dump_bytes={len(gpu)}")
print(f"row0_hits normal={len(normal_hits)} flipped={len(flipped_hits)}")
print(f"exact_full_matches_normal={len(exact_normal)}")
for x, y in exact_normal[:TOP_N]:
print(f" normal x={x} y={y} page=({x // 256},{y // 256}) in_page=({x % 256},{y % 256})")
print(f"exact_full_matches_flipped={len(exact_flipped)}")
for x, y in exact_flipped[:TOP_N]:
print(f" flipped x={x} y={y} page=({x // 256},{y // 256}) in_page=({x % 256},{y % 256})")
live_palette_entries: list[dict[str, object]] = []
live_palette_labels: list[str] = []
for clut_row in range(8):
y = 0xF0 + clut_row
row_words = struct.unpack("<1024H", rows[y])
for column in range(16):
x = column * 16
palette = list(row_words[x : x + 256])
rgba = colorize_indexed_pixels(frame, width, height, mode, palette)
live_palette_entries.append(
{
"width": width,
"height": height,
"rgba": rgba,
}
)
live_palette_labels.append(f"index={clut_row * 16 + column} x={x} y={y}")
atlas_path = BUNDLE_DIR / "live_vram_clut_atlas.png"
labels_path = BUNDLE_DIR / "live_vram_clut_atlas.txt"
write_overview_grid(atlas_path, live_palette_entries, columns=16)
labels_path.write_text("\n".join(live_palette_labels) + "\n", encoding="ascii")
print(f"live_vram_clut_atlas={atlas_path}")
print(f"live_vram_clut_labels={labels_path}")
framebuffer_path = ROOT / "binary/psx_framebuffer_left.png"
framebuffer_crop_path = ROOT / "binary/psx_framebuffer_console_crop.png"
print(f"raw_palette_blocks_256={len(palettes_256)}")
for palette_index, palette in enumerate(palettes_256):
palette_hits: list[tuple[int, int]] = []
for y in range(240, 256):
row = rows[y]
start = 0
while True:
x = row.find(palette, start)
if x < 0:
break
palette_hits.append((x, y))
start = x + 1
print(f" palette_{palette_index}_hits={len(palette_hits)}")
for x, y in palette_hits[:TOP_N]:
print(f" palette_{palette_index} x={x} y={y} row_band={y - 240}")
framebuffer_bytes = bytearray(FRAMEBUFFER_WIDTH * FRAMEBUFFER_HEIGHT * 2)
for y in range(FRAMEBUFFER_HEIGHT):
src_row = rows[y]
start = y * FRAMEBUFFER_WIDTH * 2
framebuffer_bytes[start : start + FRAMEBUFFER_WIDTH * 2] = src_row[: FRAMEBUFFER_WIDTH * 2]
write_psx_16bpp_png(framebuffer_path, bytes(framebuffer_bytes), FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT)
framebuffer_words = struct.unpack(f"<{FRAMEBUFFER_WIDTH * FRAMEBUFFER_HEIGHT}H", bytes(framebuffer_bytes))
framebuffer_rgb = rgba_from_words(framebuffer_words)
crop_x = 70
crop_y = 0
crop_width = 210
crop_height = 110
crop_bytes = bytearray(crop_width * crop_height * 2)
for y in range(crop_height):
src = rows[crop_y + y]
src_start = crop_x * 2
src_end = src_start + crop_width * 2
dst_start = y * crop_width * 2
crop_bytes[dst_start : dst_start + crop_width * 2] = src[src_start:src_end]
write_psx_16bpp_png(framebuffer_crop_path, bytes(crop_bytes), crop_width, crop_height)
print(f"framebuffer_left={framebuffer_path}")
print(f"framebuffer_console_crop={framebuffer_crop_path}")
palette_rankings: list[tuple[int, int, int, int]] = []
for palette_index, entry in enumerate(live_palette_entries):
score, best_x, best_y = candidate_match_score(
framebuffer_rgb,
FRAMEBUFFER_WIDTH,
FRAMEBUFFER_HEIGHT,
entry["rgba"],
width,
height,
guess_x=107,
guess_y=12,
)
palette_rankings.append((score, palette_index, best_x, best_y))
palette_rankings.sort()
ranking_path = BUNDLE_DIR / "live_vram_clut_rank.txt"
top_atlas_path = BUNDLE_DIR / "live_vram_clut_top_matches.png"
best_candidate_path = BUNDLE_DIR / "live_vram_clut_best.png"
ranking_lines = []
print(f"best_live_vram_clut_matches_top_{MATCH_TOP_N}={min(MATCH_TOP_N, len(palette_rankings))}")
top_entries: list[dict[str, object]] = []
for score, palette_index, best_x, best_y in palette_rankings[:MATCH_TOP_N]:
label = live_palette_labels[palette_index]
line = f"score={score} {label} screen=({best_x},{best_y})"
ranking_lines.append(line)
print(f" {line}")
top_entries.append(live_palette_entries[palette_index])
ranking_path.write_text("\n".join(ranking_lines) + "\n", encoding="ascii")
print(f"live_vram_clut_rank={ranking_path}")
write_overview_grid(top_atlas_path, top_entries, columns=4)
print(f"live_vram_clut_top_matches={top_atlas_path}")
if palette_rankings:
best_palette_index = palette_rankings[0][1]
best_entry = live_palette_entries[best_palette_index]
write_overview_grid(best_candidate_path, [best_entry], columns=1)
print(f"live_vram_clut_best={best_candidate_path}")
if exact_normal or exact_flipped:
return
ranked: list[tuple[int, int, int, str, list[int]]] = []
cutoff: int | None = None
for orientation, hits, candidate_rows in (
("normal", normal_hits, frame_rows),
("flipped", flipped_hits, flip_rows),
):
for x, y in hits:
total, row_mismatches, pruned = near_score(rows, candidate_rows, x, y, width, cutoff)
if pruned and len(ranked) >= TOP_N and total > ranked[-1][0]:
continue
entry = (total, y, x, orientation, row_mismatches)
insert_at = bisect.bisect_left(ranked, entry)
ranked.insert(insert_at, entry)
if len(ranked) > TOP_N:
ranked.pop()
if len(ranked) == TOP_N:
cutoff = ranked[-1][0]
print(f"best_near_matches_top_{TOP_N}={len(ranked)}")
for total, y, x, orientation, row_mismatches in ranked:
nonzero_rows = [(index, mismatch) for index, mismatch in enumerate(row_mismatches) if mismatch]
sample = ", ".join(f"r{index}={mismatch}" for index, mismatch in nonzero_rows[:8])
if len(nonzero_rows) > 8:
sample += ", ..."
if not sample:
sample = "all rows exact"
print(
f" {orientation} x={x} y={y} page=({x // 256},{y // 256}) in_page=({x % 256},{y % 256}) "
f"mismatches={total} details=[{sample}]"
)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,134 @@
from __future__ import annotations
import json
import struct
import sys
from pathlib import Path
ROOT = Path(r"k:/ghidra/Crusader_Decomp")
L0_WDL_PATH = Path(r"e:/emu/psx/Crusader - No Remorse/LSET1/L0.WDL")
GPU_PATH = ROOT / "binary/Crusader - No Remorse (USA) GPU RAM.bin"
OUTPUT_DIR = ROOT / "out/psx_wdl/L0/mode1_live_clut_row_f0_x0"
ROW_BYTES = 2048
LIVE_CLUT_Y = 0xF0
LIVE_CLUT_X = 0
sys.path.insert(0, str(ROOT / "tools"))
from psx_extract_wdl import (
colorize_indexed_pixels,
parse_lset_wdl,
scan_sprite_bundles,
write_bundle_atlas,
write_overview_grid,
write_png_rgba,
)
def main() -> None:
l0_data = L0_WDL_PATH.read_bytes()
gpu = GPU_PATH.read_bytes()
summary = parse_lset_wdl(l0_data)
if summary is None:
raise SystemExit("failed to parse L0.WDL")
region = next(region for region in summary["regions"] if region["name"] == "post_audio_region_04")
region_data = l0_data[region["offset"] : region["offset"] + region["size"]]
bundles = scan_sprite_bundles(region_data, max_candidates=160)
row = gpu[LIVE_CLUT_Y * ROW_BYTES : (LIVE_CLUT_Y + 1) * ROW_BYTES]
row_words = struct.unpack("<1024H", row)
palette = list(row_words[LIVE_CLUT_X : LIVE_CLUT_X + 256])
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
entries: list[dict[str, object]] = []
summary_rows: list[dict[str, object]] = []
mode1_count = 0
for bundle in bundles:
if bundle["mode"] != 1 or not bundle["frames"]:
continue
mode1_count += 1
bundle_dir = OUTPUT_DIR / f"bundle_{bundle['offset']:08X}"
bundle_dir.mkdir(parents=True, exist_ok=True)
rendered_frames: list[dict[str, object]] = []
frame_rows: list[dict[str, object]] = []
for frame in bundle["frames"]:
rgba = colorize_indexed_pixels(frame["pixels"], frame["width"], frame["height"], bundle["mode"], palette)
write_png_rgba(bundle_dir / f"frame_{frame['index']:03d}_live_row_f0_x0.png", rgba, frame["width"], frame["height"])
rendered_frames.append(
{
"width": frame["width"],
"height": frame["height"],
"rgba": rgba,
}
)
frame_rows.append(
{
"index": frame["index"],
"width": frame["width"],
"height": frame["height"],
"origin_x": frame["origin_x"],
"origin_y": frame["origin_y"],
"data_start": frame["data_start"],
"consumed": frame["consumed"],
}
)
write_bundle_atlas(bundle_dir / "atlas_live_row_f0_x0.png", rendered_frames)
metadata = {
"offset": bundle["offset"],
"mode": bundle["mode"],
"palette_formula": "live_gpu_row_0xF0_x0_contiguous_256",
"palette_source": {
"gpu_dump": str(GPU_PATH),
"x": LIVE_CLUT_X,
"y": LIVE_CLUT_Y,
},
"frame_count": bundle["frame_count"],
"exported_frames": frame_rows,
}
(bundle_dir / "palette_formula.json").write_text(json.dumps(metadata, indent=2), encoding="ascii")
first_frame = bundle["frames"][0]
first_rgba = rendered_frames[0]["rgba"]
entries.append(
{
"width": first_frame["width"],
"height": first_frame["height"],
"rgba": first_rgba,
"offset": bundle["offset"],
"area": first_frame["width"] * first_frame["height"],
}
)
summary_rows.append(
{
"offset": bundle["offset"],
"width": first_frame["width"],
"height": first_frame["height"],
"frame_count": bundle["frame_count"],
}
)
entries.sort(key=lambda entry: entry["area"], reverse=True)
overview_entries = [{"width": entry["width"], "height": entry["height"], "rgba": entry["rgba"]} for entry in entries]
write_overview_grid(OUTPUT_DIR / "overview_live_row_f0_x0.png", overview_entries, columns=4)
summary_rows.sort(key=lambda row: row["width"] * row["height"], reverse=True)
(OUTPUT_DIR / "summary.json").write_text(
json.dumps(
{
"palette_formula": "live_gpu_row_0xF0_x0_contiguous_256",
"mode1_bundle_count": mode1_count,
"bundles": summary_rows,
},
indent=2,
),
encoding="ascii",
)
print(f"mode1_bundles={mode1_count}")
print(f"overview={OUTPUT_DIR / 'overview_live_row_f0_x0.png'}")
print(f"summary={OUTPUT_DIR / 'summary.json'}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,27 @@
from ghidra.program.model.symbol import SourceType, SymbolType
function = helpers["get_function"](program, "1420:1499")
if function is None:
raise RuntimeError("Function 1420:1499 not found")
symbol_table = program.getSymbolTable()
global_namespace = program.getGlobalNamespace()
remorse_namespace = symbol_table.getNamespace("Remorse", global_namespace)
if remorse_namespace is None:
raise RuntimeError("Namespace Remorse not found")
runtime_namespace = symbol_table.getNamespace("EntityVmRuntime", remorse_namespace)
if runtime_namespace is None:
raise RuntimeError("Namespace Remorse::EntityVmRuntime not found")
for symbol in list(symbol_table.getSymbols(function.getEntryPoint())):
if symbol.getSymbolType() == SymbolType.LABEL and symbol.getName() == "Create" and symbol.getParentNamespace() == runtime_namespace:
symbol.delete()
function.setParentNamespace(runtime_namespace)
function.setName("Create", SourceType.USER_DEFINED)
print("PARENT", function.getParentNamespace())
for symbol in symbol_table.getSymbols(function.getEntryPoint()):
print("SYMBOL", symbol.getName(True), symbol.getSymbolType(), symbol.getSource())

View file

@ -0,0 +1,69 @@
Set-Location 'k:\ghidra\crusader_map_viewer\map_renderer'
function Get-Shape([object]$item) {
if ($null -eq $item -or $null -eq $item.shapeDefId) { return $null }
if ($item.shapeDefId -match '^shape:(\d+)$') { return [int]$Matches[1] }
return $null
}
function Get-Qlo([object]$item) {
if ($null -eq $item -or $null -eq $item.quality) { return $null }
return ([int]$item.quality) -band 0xff
}
function Get-Distance([object]$a, [object]$b) {
$dx = [double]$a.world.x - [double]$b.world.x
$dy = [double]$a.world.y - [double]$b.world.y
return [math]::Sqrt(($dx * $dx) + ($dy * $dy))
}
$pairs = @(
@{ Name = 'BRO_BOOT->SPANEL'; Source = 0x04fe; Target = 0x03aa; Distance = 768 },
@{ Name = 'NPC_ONLY->CMD_LINK'; Source = 0x0366; Target = 0x04b1; Distance = 768 },
@{ Name = 'DEATHBOX->CMD_LINK'; Source = 0x04e7; Target = 0x04b1; Distance = 768 },
@{ Name = 'NPC_ONLY->TRIGGERISH'; Source = 0x0366; Target = 0x0361; Distance = 768 }
)
$sceneFiles = Get-ChildItem '.\site\data\maps' -Recurse -Filter 'scene.json' | Sort-Object FullName
$lines = New-Object System.Collections.Generic.List[string]
foreach ($pair in $pairs) {
$matchedSources = 0
$sourceCount = 0
$linkCount = 0
$examples = New-Object System.Collections.Generic.List[string]
foreach ($file in $sceneFiles) {
$scene = Get-Content $file.FullName -Raw | ConvertFrom-Json
$items = @($scene.items)
$sources = @($items | Where-Object { (Get-Shape $_) -eq $pair.Source })
if ($sources.Count -eq 0) { continue }
$targets = @($items | Where-Object { (Get-Shape $_) -eq $pair.Target })
foreach ($source in $sources) {
$sourceCount += 1
$qlo = Get-Qlo $source
if ($null -eq $qlo) { continue }
$matches = @($targets | Where-Object {
(Get-Qlo $_) -eq $qlo -and (Get-Distance $source $_) -le $pair.Distance
})
if ($matches.Count -gt 0) {
$matchedSources += 1
$linkCount += $matches.Count
if ($examples.Count -lt 6) {
$examples.Add(('{0}/{1}: source={2} qlo={3} targetIds={4}' -f $file.Directory.Parent.Name, $file.Directory.Name, $source.id, $qlo, (($matches | ForEach-Object { $_.id }) -join ',')))
}
}
}
}
$rate = if ($sourceCount -gt 0) { [math]::Round(($matchedSources / $sourceCount) * 100, 1) } else { 0 }
$lines.Add(('PAIR {0}' -f $pair.Name))
$lines.Add(('sources={0} matched={1} rate={2}% links={3}' -f $sourceCount, $matchedSources, $rate, $linkCount))
foreach ($example in $examples) {
$lines.Add($example)
}
$lines.Add('')
}
$lines | Set-Content 'k:\ghidra\Crusader_Decomp\_tmp_scene_link_scan.txt'

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,75 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef WORLD_ACTORS_COMBAT_DAT_H
#define WORLD_ACTORS_COMBAT_DAT_H
#include "common/stream.h"
#include "common/str.h"
namespace Ultima {
namespace Ultima8 {
/**
* A single entry in the Crusader combat.dat flex. The files consist of 3 parts:
* 1. human-readable name (zero-padded 16 bytes)
* 2. offset table (10x2-byte offsets, in practice only the first 2 offsets are ever used)
* 3. tactic blocks starting at the offsets given in the offset (in practice only 2 blocks are used)
*
* The tactic blocks are a sequence of opcodes of things the NPC should
* do - eg, turn towards direction X.
*/
class CombatDat {
public:
CombatDat(Common::SeekableReadStream &rs);
~CombatDat();
const Common::String &getName() const {
return _name;
};
const uint8 *getData() const {
return _data;
}
uint16 getOffset(int block) const {
assert(block < ARRAYSIZE(_offsets));
return _offsets[block];
}
uint16 getDataLen() const {
return _dataLen;
}
private:
Common::String _name;
uint16 _offsets[4];
uint8 *_data;
uint16 _dataLen;
};
} // End of namespace Ultima8
} // End of namespace Ultima
#endif

View file

@ -0,0 +1,35 @@
const fs = require('fs');
function load(path) {
return JSON.parse(fs.readFileSync(path, 'utf8'));
}
function dist(a, b) {
return Math.hypot(a.world.x - b.world.x, a.world.y - b.world.y);
}
function qlo(item) {
return item.quality & 0xff;
}
function isSpawner(item) {
return item.shapeDefId === 'shape:1232';
}
for (const [label, path] of [
['map1', 'k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-1/9ccaa5dabe08947e/scene.json'],
['map248', 'k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-248/b27ea0d8d2a1a391/scene.json']
]) {
const scene = load(path);
const items = scene.items.filter(isSpawner);
const interesting = items.filter((item) => item.npcPreview?.name === 'Observer' || item.npcPreview?.name === 'RoamingSusan');
console.log(`\n### ${label} interesting spawners ${interesting.length}`);
for (const item of interesting) {
const pairs = items.filter((candidate) => candidate.id !== item.id && candidate.frame !== item.frame && qlo(candidate) === qlo(item) && dist(candidate, item) <= 128);
const pairText = pairs.map((candidate) => `${candidate.id} src=${candidate.mapSourceIndex} f=${candidate.frame} npc=${candidate.npcNum} ${candidate.npcPreview?.name || '?'} qlo=${qlo(candidate)} d=${dist(candidate, item).toFixed(1)} map=${candidate.mapNum}`).join(' || ');
console.log(`${item.id} src=${item.mapSourceIndex} f=${item.frame} npc=${item.npcNum} ${item.npcPreview?.name || '?'} qlo=${qlo(item)} map=${item.mapNum} world=${item.world.x},${item.world.y},${item.world.z}`);
if (pairText) {
console.log(` pairs: ${pairText}`);
}
}
}

View file

@ -0,0 +1,61 @@
import fs from "node:fs";
const scenePaths = [
["map1", "k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-1/9ccaa5dabe08947e/scene.json"],
["map248", "k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-248/b27ea0d8d2a1a391/scene.json"]
];
function distance(left, right) {
return Math.hypot(left.world.x - right.world.x, left.world.y - right.world.y);
}
for (const [label, scenePath] of scenePaths) {
const scene = JSON.parse(fs.readFileSync(scenePath, "utf8"));
const spawners = scene.items.filter((item) => item.shapeDefId === "shape:1232");
const rows = [];
for (const item of spawners) {
if (item.frame !== 0) {
continue;
}
const qlo = item.quality & 0xff;
const pairCandidates = spawners
.filter((candidate) => candidate.id !== item.id && candidate.frame === 1 && ((candidate.quality & 0xff) === qlo) && distance(item, candidate) <= 512)
.sort((left, right) => distance(item, left) - distance(item, right));
const pair = pairCandidates[0] ?? null;
if (!pair) {
continue;
}
rows.push({
controllerId: item.id,
pairId: pair.id,
auto: (item.mapNum & 0x08) === 0,
controllerNpc: item.npcPreview?.name ?? null,
controllerNpcNum: item.npcNum,
pairNpc: pair.npcPreview?.name ?? null,
pairNpcNum: pair.npcNum,
qlo,
distance: Math.round(distance(item, pair)),
pairCount: pairCandidates.length
});
}
console.log(`\n=== ${label} ===`);
const autoMismatched = rows.filter((row) => row.auto && row.controllerNpc && row.pairNpc && row.controllerNpc !== row.pairNpc);
const blockedMismatched = rows.filter((row) => !row.auto && row.controllerNpc && row.pairNpc && row.controllerNpc !== row.pairNpc);
const autoControllerMissingPairResolved = rows.filter((row) => row.auto && !row.controllerNpc && row.pairNpc);
console.log(`auto mismatched valid pairs: ${autoMismatched.length}`);
console.log(`blocked mismatched valid pairs: ${blockedMismatched.length}`);
console.log(`auto unresolved-controller / resolved-pair: ${autoControllerMissingPairResolved.length}`);
for (const row of autoMismatched.slice(0, 12)) {
console.log(`AUTO ${row.controllerId} ${row.controllerNpcNum}:${row.controllerNpc} -> ${row.pairId} ${row.pairNpcNum}:${row.pairNpc} qlo=${row.qlo} d=${row.distance} c=${row.pairCount}`);
}
for (const row of autoControllerMissingPairResolved.slice(0, 12)) {
console.log(`AUTO-UNRESOLVED ${row.controllerId} ${row.controllerNpcNum}:${row.controllerNpc} -> ${row.pairId} ${row.pairNpcNum}:${row.pairNpc} qlo=${row.qlo} d=${row.distance} c=${row.pairCount}`);
}
for (const row of blockedMismatched.slice(0, 12)) {
console.log(`BLOCKED ${row.controllerId} ${row.controllerNpcNum}:${row.controllerNpc} -> ${row.pairId} ${row.pairNpcNum}:${row.pairNpc} qlo=${row.qlo} d=${row.distance} c=${row.pairCount}`);
}
}

19
scripts/_tmp_targets.py Normal file
View file

@ -0,0 +1,19 @@
import csv, pathlib
p = pathlib.Path('USECODE/EUSECODE_extracted/class_event_index.tsv')
targets = {189,190,191,272,273,283,285}
rows = [r for r in csv.DictReader(p.open('r', encoding='utf-8'), delimiter='\t') if int(r['entry_index'], 0) in targets]
for eid in sorted(targets):
print(f'ENTRY {eid}')
for r in rows:
if int(r['entry_index'], 0) != eid:
continue
length = (r.get('derived_body_length') or '').strip()
if not length:
continue
try:
n = int(length, 0)
except Exception:
continue
if n == 0:
continue
print(f" slot {r['slot']} {r['event_name_hint']}: start={r['derived_body_start']} end={r['derived_body_end']} len={r['derived_body_length']} raw={r['raw_event_entry_word']} template={r.get('repeated_template_status','')}")

View file

@ -0,0 +1,15 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
comment = (
"Current best live read: compact shared SpriteNode constructor. "
"The body allocates 0x34 bytes, stamps the 0x501a node vtable, and initializes the core link/offset lanes; "
"current direct callers are overwhelmingly GumpCreate_* wrappers, which supports treating this as the base node "
"constructor used by higher-level UI/gump objects rather than a one-off derived leaf."
)
print(bridge.set_decompiler_comment("1360:036a", comment))
print(bridge.get_function_by_address("1360:036a"))

View file

@ -0,0 +1,15 @@
import sys
sys.path.insert(0, r"k:\mcp\GhidraMCP")
import bridge_mcp_ghidra as bridge
comment = (
"Old 000b:3ab2 by preserved offset delta from live 1360:046e. "
"DispatchEvent maps event codes 1/2/4/8/0x10/0x20/0x40/0x100 onto vtable slots "
"+0x04/+0x08/+0x0c/+0x10/+0x14/+0x18/+0x1c/+0x24; the 0x40 path also walks child nodes "
"and dispatches through child slot +0x34 before optional self-slot +0x1c handling."
)
print(bridge.set_decompiler_comment("1360:0cb2", comment))
print(bridge.get_function_by_address("1360:0cb2"))

View file

@ -0,0 +1,227 @@
import json
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(r"e:\disasm\Crusader-Map-Viewer\map_renderer\.cache")
SCENE_ROOT = ROOT / "scene-cache"
REF_ROOT = ROOT / "reference-data"
TARGET_SHAPE = "shape:251"
MAX_DISTANCE = 1600
MAX_NEIGHBORS_PER_ITEM = 8
INTERESTING_SHAPES = {
"shape:251": "VALUEBOX",
"shape:258": "MONITNS",
"shape:357": "MONITEW",
"shape:871": "WALLMNS",
"shape:1086": "WALLMEW",
"shape:1019": "SECURNS",
"shape:1085": "SECUREW",
"shape:1214": "WATCHNS",
"shape:1246": "WATCHEW",
"shape:2573": "KEYPAD",
"shape:2574": "KEYPAD?",
}
def load_shape_names(game: str) -> dict[str, str]:
ref_path = REF_ROOT / game / "reference-data.json"
with ref_path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
shape_names = {}
for entry in data.get("shapeDefinitions", []):
shape_id = entry.get("id")
if not shape_id:
continue
name = (
entry.get("catalog", {}).get("label")
or entry.get("displayName")
or shape_id
)
shape_names[shape_id] = name
return shape_names
def squared_distance(a: dict, b: dict) -> int:
a_world = a.get("world") or {}
b_world = b.get("world") or {}
dx = int(a_world.get("x", 0)) - int(b_world.get("x", 0))
dy = int(a_world.get("y", 0)) - int(b_world.get("y", 0))
dz = int(a_world.get("z", 0)) - int(b_world.get("z", 0))
return dx * dx + dy * dy + dz * dz
def main() -> None:
shape_names_by_game = {
"remorse": load_shape_names("remorse"),
"regret": load_shape_names("regret"),
}
summary = []
per_game_shape_counts = defaultdict(Counter)
per_game_qlo = defaultdict(Counter)
per_game_qhi = defaultdict(Counter)
interesting_links = defaultdict(Counter)
examples = defaultdict(list)
nonzero_examples = defaultdict(list)
for game_dir in sorted(SCENE_ROOT.iterdir()):
if not game_dir.is_dir():
continue
game_name = game_dir.name
base_game = "regret" if game_name.startswith("regret") else "remorse"
shape_names = shape_names_by_game[base_game]
for map_dir in sorted(game_dir.iterdir()):
if not map_dir.is_dir() or not map_dir.name.startswith("map-"):
continue
map_name = map_dir.name
for hash_dir in sorted(map_dir.iterdir()):
scene_path = hash_dir / "scene.json"
if not scene_path.exists():
continue
with scene_path.open("r", encoding="utf-8") as handle:
scene = json.load(handle)
items = scene.get("items", [])
valueboxes = [item for item in items if item.get("shapeDefId") == TARGET_SHAPE and item.get("frame") == 0]
if not valueboxes:
continue
for item in valueboxes:
quality = int(item.get("quality", 0))
qlo = quality & 0xFF
qhi = (quality >> 8) & 0xFF
per_game_qlo[game_name][qlo] += 1
per_game_qhi[game_name][qhi] += 1
nearby = []
for other in items:
if other is item:
continue
dist2 = squared_distance(item, other)
if dist2 <= MAX_DISTANCE * MAX_DISTANCE:
nearby.append((dist2, other))
nearby.sort(key=lambda pair: pair[0])
for _, other in nearby[:MAX_NEIGHBORS_PER_ITEM]:
per_game_shape_counts[game_name][other.get("shapeDefId", "<none>")] += 1
for dist2, other in nearby:
shape_id = other.get("shapeDefId", "<none>")
if shape_id in INTERESTING_SHAPES:
interesting_links[game_name][shape_id] += 1
if len(examples[game_name]) < 12:
world = item.get("world") or {}
example_row = {
"map": map_name,
"id": item.get("id"),
"coords": [world.get("x"), world.get("y"), world.get("z")],
"quality": quality,
"qlo": qlo,
"qhi": qhi,
"mapNum": item.get("mapNum"),
"npcNum": item.get("npcNum"),
"nextItem": item.get("nextItem"),
"nearby": [
{
"shape": other.get("shapeDefId"),
"name": shape_names.get(other.get("shapeDefId", ""), other.get("shapeDefId", "")),
"frame": other.get("frame"),
"quality": other.get("quality"),
"mapNum": other.get("mapNum"),
"npcNum": other.get("npcNum"),
"nextItem": other.get("nextItem"),
"coords": [
(other.get("world") or {}).get("x"),
(other.get("world") or {}).get("y"),
(other.get("world") or {}).get("z"),
],
"dist2": dist2,
}
for dist2, other in nearby
if other.get("shapeDefId") in INTERESTING_SHAPES
][:MAX_NEIGHBORS_PER_ITEM]
or [
{
"shape": other.get("shapeDefId"),
"name": shape_names.get(other.get("shapeDefId", ""), other.get("shapeDefId", "")),
"frame": other.get("frame"),
"quality": other.get("quality"),
"mapNum": other.get("mapNum"),
"npcNum": other.get("npcNum"),
"nextItem": other.get("nextItem"),
"coords": [
(other.get("world") or {}).get("x"),
(other.get("world") or {}).get("y"),
(other.get("world") or {}).get("z"),
],
"dist2": dist2,
}
for dist2, other in nearby[:MAX_NEIGHBORS_PER_ITEM]
],
}
examples[game_name].append(example_row)
if (qlo or qhi or item.get("mapNum") or item.get("npcNum")) and len(nonzero_examples[game_name]) < 12:
nonzero_examples[game_name].append(example_row)
elif (qlo or qhi or item.get("mapNum") or item.get("npcNum")) and len(nonzero_examples[game_name]) < 12:
world = item.get("world") or {}
nonzero_examples[game_name].append(
{
"map": map_name,
"id": item.get("id"),
"coords": [world.get("x"), world.get("y"), world.get("z")],
"quality": quality,
"qlo": qlo,
"qhi": qhi,
"mapNum": item.get("mapNum"),
"npcNum": item.get("npcNum"),
"nextItem": item.get("nextItem"),
}
)
summary.append({"game": game_name, "map": map_name, "count": len(valueboxes)})
print("VALUEBOX frame-0 counts by map")
for row in sorted(summary, key=lambda entry: (-entry["count"], entry["game"], entry["map"]))[:40]:
print(f"{row['game']:12} {row['map']:8} count={row['count']}")
print("\nTop nearby shapes per game")
for game_name, counter in sorted(per_game_shape_counts.items()):
print(f"\n[{game_name}]")
for shape_id, count in counter.most_common(20):
base_game = "regret" if game_name.startswith("regret") else "remorse"
shape_name = shape_names_by_game[base_game].get(shape_id, shape_id)
print(f"{shape_id:10} {count:5} {shape_name}")
print("\nInteresting nearby controller families")
for game_name, counter in sorted(interesting_links.items()):
print(f"\n[{game_name}]")
for shape_id, count in counter.most_common():
print(f"{shape_id:10} {count:5} {INTERESTING_SHAPES[shape_id]}")
print("\nTop QLo values per game")
for game_name, counter in sorted(per_game_qlo.items()):
print(f"\n[{game_name}]")
for value, count in counter.most_common(20):
print(f"QLo {value:3} -> {count}")
print("\nTop QHi values per game")
for game_name, counter in sorted(per_game_qhi.items()):
print(f"\n[{game_name}]")
for value, count in counter.most_common(20):
print(f"QHi {value:3} -> {count}")
print("\nRepresentative examples")
for game_name, rows in sorted(examples.items()):
print(f"\n[{game_name}]")
for row in rows:
print(json.dumps(row, sort_keys=True))
print("\nNonzero payload examples")
for game_name, rows in sorted(nonzero_examples.items()):
print(f"\n[{game_name}]")
for row in rows:
print(json.dumps(row, sort_keys=True))
if __name__ == "__main__":
main()

View file

@ -0,0 +1,329 @@
VALUEBOX frame-0 counts by map
remorse map-19 count=57
remorse-101 map-19 count=57
remorse-jp map-19 count=57
remorse map-29 count=32
remorse-101 map-29 count=32
remorse-jp map-29 count=32
remorse map-62 count=28
remorse-101 map-62 count=28
remorse-jp map-62 count=28
regret map-15 count=27
remorse map-47 count=27
remorse-101 map-47 count=27
remorse-jp map-47 count=27
remorse map-25 count=25
remorse map-69 count=25
remorse-101 map-25 count=25
remorse-101 map-69 count=25
remorse-jp map-25 count=25
remorse-jp map-69 count=25
remorse map-141 count=24
remorse-101 map-141 count=24
remorse-jp map-141 count=24
remorse map-10 count=22
remorse map-26 count=22
remorse-101 map-10 count=22
remorse-101 map-26 count=22
remorse-jp map-10 count=22
remorse-jp map-26 count=22
regret map-16 count=16
regret map-13 count=14
regret map-14 count=14
regret map-8 count=11
regret map-28 count=10
regret map-5 count=10
regret map-7 count=10
regret map-200 count=9
regret map-201 count=9
remorse map-15 count=9
remorse-101 map-15 count=9
remorse-jp map-15 count=9
Top nearby shapes per game
[regret]
shape:248 226 shape_00f8
shape:249 76 shape_00f9
shape:1232 71 shape_04d0
shape:253 49 shape_00fd
shape:573 47 shape_023d
shape:16 42 shape_0010
shape:1061 42 shape_0425
shape:534 38 shape_0216
shape:1365 28 shape_0555
shape:246 22 shape_00f6
shape:1278 21 shape_04fe
shape:567 20 shape_0237
shape:1201 19 CMD_LINK
shape:1142 19 shape_0476
shape:1364 19 shape_0554
shape:247 18 shape_00f7
shape:1363 16 shape_0553
shape:1369 15 shape_0559
shape:1347 15 shape_0543
shape:1366 14 shape_0556
[regret-demo]
shape:248 8 shape_00f8
shape:99 6 shape_0063
shape:249 6 shape_00f9
shape:574 5 shape_023e
shape:1232 5 shape_04d0
shape:431 5 shape_01af
shape:250 4 shape_00fa
shape:1142 3 shape_0476
shape:16 3 shape_0010
shape:246 2 shape_00f6
shape:252 2 shape_00fc
shape:534 1 shape_0216
shape:949 1 shape_03b5
shape:593 1 shape_0251
shape:1377 1 shape_0561
shape:1593 1 Roof_Regret_Level1
shape:253 1 shape_00fd
shape:1201 1 CMD_LINK
[remorse]
shape:248 444 shape_00f8
shape:249 132 shape_00f9
shape:569 77 shape_0239
shape:534 62 shape_0216
shape:16 60 shape_0010
shape:253 58 shape_00fd
shape:563 49 shape_0233
shape:564 49 shape_0234
shape:1253 49 shape_04e5
shape:572 46 shape_023c
shape:486 42 shape_01e6
shape:250 40 shape_00fa
shape:43 40 shape_002b
shape:573 39 shape_023d
shape:716 36 shape_02cc
shape:252 35 shape_00fc
shape:631 35 shape_0277
shape:246 33 shape_00f6
shape:15 33 shape_000f
shape:547 30 shape_0223
[remorse-101]
shape:248 444 shape_00f8
shape:249 132 shape_00f9
shape:569 77 shape_0239
shape:534 62 shape_0216
shape:16 60 shape_0010
shape:253 58 shape_00fd
shape:563 49 shape_0233
shape:564 49 shape_0234
shape:1253 49 shape_04e5
shape:572 46 shape_023c
shape:486 42 shape_01e6
shape:250 40 shape_00fa
shape:43 40 shape_002b
shape:573 39 shape_023d
shape:716 36 shape_02cc
shape:252 35 shape_00fc
shape:631 35 shape_0277
shape:246 33 shape_00f6
shape:15 33 shape_000f
shape:547 30 shape_0223
[remorse-jp]
shape:248 444 shape_00f8
shape:249 132 shape_00f9
shape:569 77 shape_0239
shape:534 62 shape_0216
shape:16 60 shape_0010
shape:253 58 shape_00fd
shape:563 49 shape_0233
shape:564 49 shape_0234
shape:1253 49 shape_04e5
shape:572 46 shape_023c
shape:486 42 shape_01e6
shape:250 40 shape_00fa
shape:43 40 shape_002b
shape:573 39 shape_023d
shape:716 36 shape_02cc
shape:252 35 shape_00fc
shape:631 35 shape_0277
shape:246 33 shape_00f6
shape:15 33 shape_000f
shape:547 30 shape_0223
Interesting nearby controller families
[regret]
shape:251 226 VALUEBOX
shape:357 32 MONITEW
shape:258 21 MONITNS
shape:1246 20 WATCHEW
shape:1019 11 SECURNS
shape:1085 6 SECUREW
shape:871 6 WALLMNS
shape:1086 1 WALLMEW
[regret-demo]
shape:251 6 VALUEBOX
shape:258 2 MONITNS
shape:1246 2 WATCHEW
[remorse]
shape:251 456 VALUEBOX
shape:357 90 MONITEW
shape:258 65 MONITNS
shape:1246 18 WATCHEW
shape:1019 1 SECURNS
[remorse-101]
shape:251 456 VALUEBOX
shape:357 90 MONITEW
shape:258 65 MONITNS
shape:1246 18 WATCHEW
shape:1019 1 SECURNS
[remorse-jp]
shape:251 456 VALUEBOX
shape:357 90 MONITEW
shape:258 65 MONITNS
shape:1246 18 WATCHEW
shape:1019 1 SECURNS
Top QLo values per game
[regret]
QLo 0 -> 170
QLo 23 -> 1
[regret-demo]
QLo 0 -> 7
[remorse]
QLo 0 -> 299
QLo 60 -> 3
[remorse-101]
QLo 0 -> 299
QLo 60 -> 3
[remorse-jp]
QLo 0 -> 299
QLo 60 -> 3
Top QHi values per game
[regret]
QHi 0 -> 171
[regret-demo]
QHi 0 -> 7
[remorse]
QHi 0 -> 299
QHi 3 -> 3
[remorse-101]
QHi 0 -> 299
QHi 3 -> 3
[remorse-jp]
QHi 0 -> 299
QHi 3 -> 3
Representative examples
[regret]
{"coords": [57662, 5790, 0], "id": "item:1425:fixed:251:0:57662:5790:0", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [58302, 5790, 0], "dist2": 409600, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 4501, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [58590, 7070, 0], "dist2": 2499584, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 4229, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 4523, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [58302, 5790, 0], "id": "item:1430:fixed:251:0:58302:5790:0", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [57662, 5790, 0], "dist2": 409600, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 4523, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [58590, 7070, 0], "dist2": 1721344, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 4229, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [58590, 7358, 0], "dist2": 2541568, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 4080, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 4501, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [58590, 7070, 0], "id": "item:1621:fixed:251:0:58590:7070:0", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [58590, 7358, 0], "dist2": 82944, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 4080, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [58302, 5790, 0], "dist2": 1721344, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 4501, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [57662, 5790, 0], "dist2": 2499584, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 4523, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 4229, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [58590, 7358, 0], "id": "item:1710:fixed:251:0:58590:7358:0", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [58590, 7070, 0], "dist2": 82944, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 4229, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [58302, 5790, 0], "dist2": 2541568, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 4501, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 4080, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [59358, 11486, 0], "id": "item:1991:fixed:251:0:59358:11486:0", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [59230, 11486, 0], "dist2": 16384, "frame": 0, "mapNum": 0, "name": "shape_00fd", "nextItem": 5364, "npcNum": 0, "quality": 0, "shape": "shape:253"}, {"coords": [59390, 11262, 0], "dist2": 51200, "frame": 4, "mapNum": 0, "name": "shape_023d", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:573"}, {"coords": [59390, 11262, 0], "dist2": 51200, "frame": 0, "mapNum": 0, "name": "shape_0010", "nextItem": 3701, "npcNum": 0, "quality": 2651, "shape": "shape:16"}, {"coords": [59390, 11262, 0], "dist2": 51200, "frame": 2, "mapNum": 0, "name": "shape_0237", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:567"}, {"coords": [59390, 11262, 0], "dist2": 51200, "frame": 2, "mapNum": 0, "name": "shape_0239", "nextItem": 3719, "npcNum": 0, "quality": 0, "shape": "shape:569"}, {"coords": [59422, 11262, 0], "dist2": 54272, "frame": 1, "mapNum": 0, "name": "shape_0239", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:569"}, {"coords": [59486, 11262, 0], "dist2": 66560, "frame": 1, "mapNum": 0, "name": "shape_023c", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:572"}, {"coords": [59390, 11774, 0], "dist2": 83968, "frame": 4, "mapNum": 0, "name": "shape_023d", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:573"}], "nextItem": 5365, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [58334, 9214, 96], "id": "item:5998:fixed:251:0:58334:9214:96", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [57758, 9022, 96], "dist2": 368640, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 3949, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [57758, 9630, 96], "dist2": 504832, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 3819, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [57758, 10110, 96], "dist2": 1134592, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 3957, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [57758, 9022, 96], "id": "item:6001:fixed:251:0:57758:9022:96", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [58334, 9214, 96], "dist2": 368640, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 3957, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [57758, 9630, 96], "dist2": 369664, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 3819, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [57758, 10110, 96], "dist2": 1183744, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 3949, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [57758, 10110, 96], "id": "item:6046:fixed:251:0:57758:10110:96", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [57758, 9630, 96], "dist2": 230400, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 3819, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [58334, 9214, 96], "dist2": 1134592, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 3957, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [57758, 9022, 96], "dist2": 1183744, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 3949, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [57758, 9630, 96], "id": "item:6051:fixed:251:0:57758:9630:96", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [57758, 10110, 96], "dist2": 230400, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [57758, 9022, 96], "dist2": 369664, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 3949, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [58334, 9214, 96], "dist2": 504832, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 3957, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 3819, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [37406, 28222, 96], "id": "item:6414:fixed:251:0:37406:28222:96", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [36958, 28222, 96], "dist2": 200704, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2040, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [37144, 29232, 128], "dist2": 1089768, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 1983, "npcNum": 0, "quality": 18972, "shape": "shape:357"}, {"coords": [35902, 28478, 128], "dist2": 2328576, "frame": 0, "mapNum": 22, "name": "shape_043d", "nextItem": 2071, "npcNum": 164, "quality": 18, "shape": "shape:1085"}], "nextItem": 2043, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [36958, 28222, 96], "id": "item:6416:fixed:251:0:36958:28222:96", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [37406, 28222, 96], "dist2": 200704, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2043, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [37144, 29232, 128], "dist2": 1055720, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 1983, "npcNum": 0, "quality": 18972, "shape": "shape:357"}, {"coords": [35902, 28478, 128], "dist2": 1181696, "frame": 0, "mapNum": 22, "name": "shape_043d", "nextItem": 2071, "npcNum": 164, "quality": 18, "shape": "shape:1085"}], "nextItem": 2040, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [32414, 33310, 96], "id": "item:6666:fixed:251:0:32414:33310:96", "map": "map-13", "mapNum": 0, "nearby": [{"coords": [33694, 33310, 96], "dist2": 1638400, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1605, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 1660, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
[regret-demo]
{"coords": [48158, 54750, 8], "id": "item:2223:fixed:251:0:48158:54750:8", "map": "map-2", "mapNum": 0, "nearby": [{"coords": [48158, 55486, 8], "dist2": 541696, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 475, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 642, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [50206, 54750, 8], "id": "item:2250:fixed:251:0:50206:54750:8", "map": "map-2", "mapNum": 0, "nearby": [{"coords": [50652, 54868, 56], "dist2": 215144, "frame": 0, "mapNum": 167, "name": "MONITNS", "nextItem": 564, "npcNum": 139, "quality": 43, "shape": "shape:258"}, {"coords": [50206, 55486, 8], "dist2": 541696, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 442, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 638, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [54430, 55230, 8], "id": "item:2280:fixed:251:0:54430:55230:8", "map": "map-2", "mapNum": 0, "nearby": [{"coords": [53982, 55070, 8], "dist2": 226304, "frame": 0, "mapNum": 102, "name": "WATCHEW", "nextItem": 492, "npcNum": 24, "quality": 282, "shape": "shape:1246"}, {"coords": [54366, 54750, 8], "dist2": 234496, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 3670, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 534, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [54366, 54750, 8], "id": "item:2285:fixed:251:0:54366:54750:8", "map": "map-2", "mapNum": 0, "nearby": [{"coords": [54430, 55230, 8], "dist2": 234496, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 534, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [53982, 55070, 8], "dist2": 249856, "frame": 0, "mapNum": 102, "name": "WATCHEW", "nextItem": 492, "npcNum": 24, "quality": 282, "shape": "shape:1246"}], "nextItem": 3670, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [48158, 55486, 8], "id": "item:2295:fixed:251:0:48158:55486:8", "map": "map-2", "mapNum": 0, "nearby": [{"coords": [48158, 54750, 8], "dist2": 541696, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 642, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 475, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [50206, 55486, 8], "id": "item:2306:fixed:251:0:50206:55486:8", "map": "map-2", "mapNum": 0, "nearby": [{"coords": [50206, 54750, 8], "dist2": 541696, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 638, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [50652, 54868, 56], "dist2": 583144, "frame": 0, "mapNum": 167, "name": "MONITNS", "nextItem": 564, "npcNum": 139, "quality": 43, "shape": "shape:258"}], "nextItem": 442, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [26526, 28670, 96], "id": "item:3506:fixed:251:0:26526:28670:96", "map": "map-2", "mapNum": 0, "nearby": [{"coords": [26590, 28670, 96], "dist2": 4096, "frame": 0, "mapNum": 0, "name": "shape_00f9", "nextItem": 4941, "npcNum": 0, "quality": 0, "shape": "shape:249"}, {"coords": [26622, 28670, 96], "dist2": 9216, "frame": 2, "mapNum": 0, "name": "shape_0063", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:99"}, {"coords": [26622, 28670, 96], "dist2": 9216, "frame": 0, "mapNum": 0, "name": "shape_0010", "nextItem": 4933, "npcNum": 0, "quality": 347, "shape": "shape:16"}, {"coords": [26622, 28670, 96], "dist2": 9216, "frame": 1, "mapNum": 0, "name": "shape_00f8", "nextItem": 4940, "npcNum": 0, "quality": 0, "shape": "shape:248"}, {"coords": [26398, 28670, 96], "dist2": 16384, "frame": 0, "mapNum": 0, "name": "shape_00fd", "nextItem": 4943, "npcNum": 0, "quality": 0, "shape": "shape:253"}, {"coords": [26494, 28894, 136], "dist2": 52800, "frame": 0, "mapNum": 200, "name": "shape_0476", "nextItem": 4827, "npcNum": 128, "quality": 28425, "shape": "shape:1142"}, {"coords": [26558, 28958, 96], "dist2": 83968, "frame": 0, "mapNum": 239, "name": "shape_0476", "nextItem": 4824, "npcNum": 64, "quality": 9, "shape": "shape:1142"}, {"coords": [26462, 28382, 96], "dist2": 87040, "frame": 8, "mapNum": 111, "name": "CMD_LINK", "nextItem": 4935, "npcNum": 97, "quality": 3334, "shape": "shape:1201"}], "nextItem": 4942, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
[remorse]
{"coords": [22782, 11326, 0], "id": "item:2327:fixed:251:0:22782:11326:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [23262, 10302, 112], "dist2": 1291520, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2988, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [24062, 11518, 40], "dist2": 1676864, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 2726, "npcNum": 0, "quality": 50, "shape": "shape:357"}], "nextItem": 2839, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [25534, 12254, 0], "id": "item:2408:fixed:251:0:25534:12254:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [25534, 11710, 0], "dist2": 295936, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2753, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [26462, 11262, 112], "dist2": 1857792, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2883, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 2754, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [25534, 11710, 0], "id": "item:2411:fixed:251:0:25534:11710:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [25534, 12254, 0], "dist2": 295936, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2754, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [26462, 11262, 112], "dist2": 1074432, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2883, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [25534, 10302, 112], "dist2": 1995008, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2898, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [25790, 10270, 112], "dist2": 2151680, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2800, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [24062, 11518, 40], "dist2": 2205248, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 2726, "npcNum": 0, "quality": 50, "shape": "shape:357"}], "nextItem": 2753, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [22398, 13374, 0], "id": "item:2550:fixed:251:0:22398:13374:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [21950, 13374, 0], "dist2": 200704, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2542, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 2545, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [21950, 13374, 0], "id": "item:2554:fixed:251:0:21950:13374:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [22398, 13374, 0], "dist2": 200704, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2545, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 2542, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [20670, 15422, 0], "id": "item:2768:fixed:251:0:20670:15422:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [20350, 15294, 24], "dist2": 119360, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 2530, "npcNum": 0, "quality": 14906, "shape": "shape:357"}, {"coords": [19998, 14558, 72], "dist2": 1203264, "frame": 2, "mapNum": 0, "name": "MONITEW", "nextItem": 2531, "npcNum": 0, "quality": 0, "shape": "shape:357"}], "nextItem": 2424, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [30910, 17502, 0], "id": "item:2988:fixed:251:0:30910:17502:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [31070, 16734, 40], "dist2": 617024, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 2224, "npcNum": 0, "quality": 0, "shape": "shape:357"}], "nextItem": 2049, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [38462, 19518, 0], "id": "item:3216:fixed:251:0:38462:19518:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [38078, 19518, 0], "dist2": 147456, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1738, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [38878, 19358, 136], "dist2": 217152, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 1924, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38878, 19198, 136], "dist2": 293952, "frame": 1, "mapNum": 0, "name": "MONITNS", "nextItem": 1925, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38014, 19166, 136], "dist2": 343104, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 1923, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38704, 18592, 152], "dist2": 939144, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38846, 18494, 112], "dist2": 1208576, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1879, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [39742, 20382, 24], "dist2": 2385472, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 3339, "npcNum": 0, "quality": 0, "shape": "shape:258"}], "nextItem": 1745, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [38078, 19518, 0], "id": "item:3225:fixed:251:0:38078:19518:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [38014, 19166, 136], "dist2": 146496, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 1923, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38462, 19518, 0], "dist2": 147456, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1745, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [38878, 19358, 136], "dist2": 684096, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 1924, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38878, 19198, 136], "dist2": 760896, "frame": 1, "mapNum": 0, "name": "MONITNS", "nextItem": 1925, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38704, 18592, 152], "dist2": 1272456, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38846, 18494, 112], "dist2": 1650944, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1879, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 1738, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [32414, 20542, 0], "id": "item:3354:fixed:251:0:32414:20542:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [32926, 19998, 0], "dist2": 558080, "frame": 0, "mapNum": 0, "name": "shape_04de", "nextItem": 1767, "npcNum": 0, "quality": 14362, "shape": "shape:1246"}], "nextItem": 3361, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [36062, 23614, 0], "id": "item:3782:fixed:251:0:36062:23614:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [36478, 23710, 32], "dist2": 183296, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 1177, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [35966, 24030, 32], "dist2": 183296, "frame": 2, "mapNum": 0, "name": "MONITEW", "nextItem": 1176, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [36926, 23710, 16], "dist2": 755968, "frame": 2, "mapNum": 0, "name": "MONITNS", "nextItem": 1099, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [36030, 25054, 0], "dist2": 2074624, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 962, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 1130, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [36030, 25054, 0], "id": "item:3932:fixed:251:0:36030:25054:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [35966, 24030, 32], "dist2": 1053696, "frame": 2, "mapNum": 0, "name": "MONITEW", "nextItem": 1176, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [36478, 23710, 32], "dist2": 2008064, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 1177, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [36062, 23614, 0], "dist2": 2074624, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1130, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 962, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
[remorse-101]
{"coords": [22782, 11326, 0], "id": "item:2326:fixed:251:0:22782:11326:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [23262, 10302, 112], "dist2": 1291520, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 533, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [24062, 11518, 40], "dist2": 1676864, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 752, "npcNum": 0, "quality": 50, "shape": "shape:357"}], "nextItem": 707, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [25534, 11710, 0], "id": "item:2402:fixed:251:0:25534:11710:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [25534, 12254, 0], "dist2": 295936, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 782, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [26462, 11262, 112], "dist2": 1074432, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 664, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [25534, 10302, 112], "dist2": 1995008, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 622, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [25790, 10270, 112], "dist2": 2151680, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 639, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [24062, 11518, 40], "dist2": 2205248, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 752, "npcNum": 0, "quality": 50, "shape": "shape:357"}], "nextItem": 783, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [25534, 12254, 0], "id": "item:2410:fixed:251:0:25534:12254:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [25534, 11710, 0], "dist2": 295936, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 783, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [26462, 11262, 112], "dist2": 1857792, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 664, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 782, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [22398, 13374, 0], "id": "item:2536:fixed:251:0:22398:13374:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [21950, 13374, 0], "dist2": 200704, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 940, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 937, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [21950, 13374, 0], "id": "item:2545:fixed:251:0:21950:13374:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [22398, 13374, 0], "dist2": 200704, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 937, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 940, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [20670, 15422, 0], "id": "item:2753:fixed:251:0:20670:15422:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [20350, 15294, 24], "dist2": 119360, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 1039, "npcNum": 0, "quality": 14906, "shape": "shape:357"}, {"coords": [19998, 14558, 72], "dist2": 1203264, "frame": 2, "mapNum": 0, "name": "MONITEW", "nextItem": 1038, "npcNum": 0, "quality": 0, "shape": "shape:357"}], "nextItem": 1217, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [30910, 17502, 0], "id": "item:2982:fixed:251:0:30910:17502:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [31070, 16734, 40], "dist2": 617024, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 1396, "npcNum": 0, "quality": 0, "shape": "shape:357"}], "nextItem": 1566, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [38462, 19518, 0], "id": "item:3225:fixed:251:0:38462:19518:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [38078, 19518, 0], "dist2": 147456, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1987, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [38878, 19358, 136], "dist2": 217152, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 1810, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38878, 19198, 136], "dist2": 293952, "frame": 1, "mapNum": 0, "name": "MONITNS", "nextItem": 1809, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38014, 19166, 136], "dist2": 343104, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 1811, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38638, 18602, 152], "dist2": 893136, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 1759, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38846, 18494, 112], "dist2": 1208576, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1778, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [39742, 20382, 24], "dist2": 2385472, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 2104, "npcNum": 0, "quality": 0, "shape": "shape:258"}], "nextItem": 1980, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [38078, 19518, 0], "id": "item:3229:fixed:251:0:38078:19518:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [38014, 19166, 136], "dist2": 146496, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 1811, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38462, 19518, 0], "dist2": 147456, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1980, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [38878, 19358, 136], "dist2": 684096, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 1810, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38878, 19198, 136], "dist2": 760896, "frame": 1, "mapNum": 0, "name": "MONITNS", "nextItem": 1809, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38638, 18602, 152], "dist2": 1175760, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 1759, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38846, 18494, 112], "dist2": 1650944, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1778, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 1987, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [32414, 20542, 0], "id": "item:3347:fixed:251:0:32414:20542:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [32926, 19998, 0], "dist2": 558080, "frame": 0, "mapNum": 0, "name": "shape_04de", "nextItem": 1924, "npcNum": 0, "quality": 14362, "shape": "shape:1246"}], "nextItem": 2182, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [36062, 23614, 0], "id": "item:3771:fixed:251:0:36062:23614:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [35966, 24030, 32], "dist2": 183296, "frame": 2, "mapNum": 0, "name": "MONITEW", "nextItem": 2765, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [36478, 23710, 32], "dist2": 183296, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 2764, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [36926, 23710, 16], "dist2": 755968, "frame": 2, "mapNum": 0, "name": "MONITNS", "nextItem": 2844, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [36030, 25054, 0], "dist2": 2074624, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2960, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 2748, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [36030, 25054, 0], "id": "item:3950:fixed:251:0:36030:25054:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [35966, 24030, 32], "dist2": 1053696, "frame": 2, "mapNum": 0, "name": "MONITEW", "nextItem": 2765, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [36478, 23710, 32], "dist2": 2008064, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 2764, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [36062, 23614, 0], "dist2": 2074624, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2748, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 2960, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
[remorse-jp]
{"coords": [22782, 11326, 0], "id": "item:2327:fixed:251:0:22782:11326:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [23262, 10302, 112], "dist2": 1291520, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2988, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [24062, 11518, 40], "dist2": 1676864, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 2726, "npcNum": 0, "quality": 50, "shape": "shape:357"}], "nextItem": 2839, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [25534, 12254, 0], "id": "item:2408:fixed:251:0:25534:12254:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [25534, 11710, 0], "dist2": 295936, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2753, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [26462, 11262, 112], "dist2": 1857792, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2883, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 2754, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [25534, 11710, 0], "id": "item:2411:fixed:251:0:25534:11710:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [25534, 12254, 0], "dist2": 295936, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2754, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [26462, 11262, 112], "dist2": 1074432, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2883, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [25534, 10302, 112], "dist2": 1995008, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2898, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [25790, 10270, 112], "dist2": 2151680, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2800, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [24062, 11518, 40], "dist2": 2205248, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 2726, "npcNum": 0, "quality": 50, "shape": "shape:357"}], "nextItem": 2753, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [22398, 13374, 0], "id": "item:2550:fixed:251:0:22398:13374:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [21950, 13374, 0], "dist2": 200704, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2542, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 2545, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [21950, 13374, 0], "id": "item:2554:fixed:251:0:21950:13374:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [22398, 13374, 0], "dist2": 200704, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 2545, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 2542, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [20670, 15422, 0], "id": "item:2768:fixed:251:0:20670:15422:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [20350, 15294, 24], "dist2": 119360, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 2530, "npcNum": 0, "quality": 14906, "shape": "shape:357"}, {"coords": [19998, 14558, 72], "dist2": 1203264, "frame": 2, "mapNum": 0, "name": "MONITEW", "nextItem": 2531, "npcNum": 0, "quality": 0, "shape": "shape:357"}], "nextItem": 2424, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [30910, 17502, 0], "id": "item:2988:fixed:251:0:30910:17502:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [31070, 16734, 40], "dist2": 617024, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 2224, "npcNum": 0, "quality": 0, "shape": "shape:357"}], "nextItem": 2049, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [38462, 19518, 0], "id": "item:3216:fixed:251:0:38462:19518:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [38078, 19518, 0], "dist2": 147456, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1738, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [38878, 19358, 136], "dist2": 217152, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 1924, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38878, 19198, 136], "dist2": 293952, "frame": 1, "mapNum": 0, "name": "MONITNS", "nextItem": 1925, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38014, 19166, 136], "dist2": 343104, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 1923, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38704, 18592, 152], "dist2": 939144, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38846, 18494, 112], "dist2": 1208576, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1879, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [39742, 20382, 24], "dist2": 2385472, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 3339, "npcNum": 0, "quality": 0, "shape": "shape:258"}], "nextItem": 1745, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [38078, 19518, 0], "id": "item:3225:fixed:251:0:38078:19518:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [38014, 19166, 136], "dist2": 146496, "frame": 0, "mapNum": 0, "name": "MONITEW", "nextItem": 1923, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38462, 19518, 0], "dist2": 147456, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1745, "npcNum": 0, "quality": 0, "shape": "shape:251"}, {"coords": [38878, 19358, 136], "dist2": 684096, "frame": 1, "mapNum": 0, "name": "MONITEW", "nextItem": 1924, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [38878, 19198, 136], "dist2": 760896, "frame": 1, "mapNum": 0, "name": "MONITNS", "nextItem": 1925, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38704, 18592, 152], "dist2": 1272456, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 0, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [38846, 18494, 112], "dist2": 1650944, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1879, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 1738, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [32414, 20542, 0], "id": "item:3354:fixed:251:0:32414:20542:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [32926, 19998, 0], "dist2": 558080, "frame": 0, "mapNum": 0, "name": "shape_04de", "nextItem": 1767, "npcNum": 0, "quality": 14362, "shape": "shape:1246"}], "nextItem": 3361, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [36062, 23614, 0], "id": "item:3782:fixed:251:0:36062:23614:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [36478, 23710, 32], "dist2": 183296, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 1177, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [35966, 24030, 32], "dist2": 183296, "frame": 2, "mapNum": 0, "name": "MONITEW", "nextItem": 1176, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [36926, 23710, 16], "dist2": 755968, "frame": 2, "mapNum": 0, "name": "MONITNS", "nextItem": 1099, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [36030, 25054, 0], "dist2": 2074624, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 962, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 1130, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [36030, 25054, 0], "id": "item:3932:fixed:251:0:36030:25054:0", "map": "map-10", "mapNum": 0, "nearby": [{"coords": [35966, 24030, 32], "dist2": 1053696, "frame": 2, "mapNum": 0, "name": "MONITEW", "nextItem": 1176, "npcNum": 0, "quality": 0, "shape": "shape:357"}, {"coords": [36478, 23710, 32], "dist2": 2008064, "frame": 0, "mapNum": 0, "name": "MONITNS", "nextItem": 1177, "npcNum": 0, "quality": 0, "shape": "shape:258"}, {"coords": [36062, 23614, 0], "dist2": 2074624, "frame": 0, "mapNum": 0, "name": "shape_00fb", "nextItem": 1130, "npcNum": 0, "quality": 0, "shape": "shape:251"}], "nextItem": 962, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
Nonzero payload examples
[regret]
{"coords": [4926, 14302, 96], "id": "item:5149:fixed:251:0:4926:14302:96", "map": "map-29", "mapNum": 0, "nextItem": 1596, "npcNum": 0, "qhi": 0, "qlo": 23, "quality": 23}
[remorse]
{"coords": [53118, 30558, 96], "id": "item:10711:fixed:251:0:53118:30558:96", "map": "map-141", "mapNum": 0, "nextItem": 799, "npcNum": 2, "qhi": 3, "qlo": 60, "quality": 828}
{"coords": [64702, 34814, 96], "id": "item:12590:glob:251:0:64702:34814:96", "map": "map-141", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [65438, 34814, 96], "id": "item:12594:glob:251:0:65438:34814:96", "map": "map-141", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [53118, 30558, 96], "id": "item:11657:fixed:251:0:53118:30558:96", "map": "map-25", "mapNum": 0, "nextItem": 1078, "npcNum": 2, "qhi": 3, "qlo": 60, "quality": 828}
{"coords": [64702, 34814, 96], "id": "item:13546:glob:251:0:64702:34814:96", "map": "map-25", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [65438, 34814, 96], "id": "item:13550:glob:251:0:65438:34814:96", "map": "map-25", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [53118, 30558, 96], "id": "item:10397:fixed:251:0:53118:30558:96", "map": "map-26", "mapNum": 0, "nextItem": 279, "npcNum": 2, "qhi": 3, "qlo": 60, "quality": 828}
[remorse-101]
{"coords": [53118, 30558, 96], "id": "item:10711:fixed:251:0:53118:30558:96", "map": "map-141", "mapNum": 0, "nextItem": 799, "npcNum": 2, "qhi": 3, "qlo": 60, "quality": 828}
{"coords": [64702, 34814, 96], "id": "item:12590:glob:251:0:64702:34814:96", "map": "map-141", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [65438, 34814, 96], "id": "item:12594:glob:251:0:65438:34814:96", "map": "map-141", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [53118, 30558, 96], "id": "item:11664:fixed:251:0:53118:30558:96", "map": "map-25", "mapNum": 0, "nextItem": 1561, "npcNum": 2, "qhi": 3, "qlo": 60, "quality": 828}
{"coords": [64702, 34814, 96], "id": "item:13546:glob:251:0:64702:34814:96", "map": "map-25", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [65438, 34814, 96], "id": "item:13550:glob:251:0:65438:34814:96", "map": "map-25", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [53118, 30558, 96], "id": "item:10397:fixed:251:0:53118:30558:96", "map": "map-26", "mapNum": 0, "nextItem": 279, "npcNum": 2, "qhi": 3, "qlo": 60, "quality": 828}
[remorse-jp]
{"coords": [53118, 30558, 96], "id": "item:10711:fixed:251:0:53118:30558:96", "map": "map-141", "mapNum": 0, "nextItem": 799, "npcNum": 2, "qhi": 3, "qlo": 60, "quality": 828}
{"coords": [64702, 34814, 96], "id": "item:12590:glob:251:0:64702:34814:96", "map": "map-141", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [65438, 34814, 96], "id": "item:12594:glob:251:0:65438:34814:96", "map": "map-141", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [53118, 30558, 96], "id": "item:11657:fixed:251:0:53118:30558:96", "map": "map-25", "mapNum": 0, "nextItem": 1078, "npcNum": 2, "qhi": 3, "qlo": 60, "quality": 828}
{"coords": [64702, 34814, 96], "id": "item:13546:glob:251:0:64702:34814:96", "map": "map-25", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [65438, 34814, 96], "id": "item:13550:glob:251:0:65438:34814:96", "map": "map-25", "mapNum": 8, "nextItem": 0, "npcNum": 0, "qhi": 0, "qlo": 0, "quality": 0}
{"coords": [53118, 30558, 96], "id": "item:10397:fixed:251:0:53118:30558:96", "map": "map-26", "mapNum": 0, "nextItem": 279, "npcNum": 2, "qhi": 3, "qlo": 60, "quality": 828}