fix: v3.0.0 -- thresholds on the free-fraction scale the assembly actually uses
Disassembling Cosmoteer.dll (tools/ildump.py) shows PartThresholdToggle compares plain floats with no percent logic, and that a NetworkResourceInput's Value is the free fraction (0..1) of the connected network. v2.1.0 compared that against 2000000, so it always vented. Thresholds are now 5% / 10% free as local constants. New guard test_threshold_amounts_match_value_scale fails on the old values. README corrects the earlier wrong diagnosis of v2.0.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011oLWY1G6S8U35bi2FxVWni
This commit is contained in:
Executable
+139
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
# ABOUT ME
|
||||
# Disassembles methods of named classes in Cosmoteer.dll to readable CIL, with
|
||||
# call/field/string operands resolved to names. Exists because the wiki
|
||||
# documents none of the component internals: this is how the mod learned that
|
||||
# PartThresholdToggle compares plain floats (no percent logic) and that a
|
||||
# NetworkResourceInput's Value is the free fraction of the connected network.
|
||||
#
|
||||
# Usage:
|
||||
# python3 tools/ildump.py <Cosmoteer.dll> <Type[,Type...]> [method-substring[,...]]
|
||||
#
|
||||
# python3 tools/ildump.py Bin/Cosmoteer.dll PartThresholdToggle OnAmountChanged
|
||||
# python3 tools/ildump.py Bin/Cosmoteer.dll PartNetworkResourceInput get_Value
|
||||
# python3 tools/ildump.py Bin/Cosmoteer.dll BaseResourceStorageRules .ctor # field defaults
|
||||
#
|
||||
# Needs the pure-Python packages dnfile (0.18.0) and dncil (1.0.2), plus pefile.
|
||||
# Set ILDUMP_PKGS to a directory holding unpacked wheels if they are not
|
||||
# installed system-wide. tools/dotnet_enum.py answers the cheaper question
|
||||
# "which fields does this Rules class have" without any dependency.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if os.environ.get("ILDUMP_PKGS"):
|
||||
sys.path.insert(0, os.environ["ILDUMP_PKGS"])
|
||||
|
||||
import dnfile # noqa: E402
|
||||
from dncil.cil.body import CilMethodBody # noqa: E402
|
||||
from dncil.cil.body.reader import CilMethodBodyReaderBase # noqa: E402
|
||||
|
||||
TABLES = {
|
||||
0x01: "TypeRef",
|
||||
0x02: "TypeDef",
|
||||
0x04: "Field",
|
||||
0x06: "MethodDef",
|
||||
0x0A: "MemberRef",
|
||||
0x1B: "TypeSpec",
|
||||
0x2B: "MethodSpec",
|
||||
}
|
||||
|
||||
|
||||
class _Reader(CilMethodBodyReaderBase):
|
||||
def __init__(self, pe, row):
|
||||
self.pe = pe
|
||||
self.off = pe.get_offset_from_rva(row.Rva)
|
||||
|
||||
def read(self, n):
|
||||
data = self.pe.get_data(self.pe.get_rva_from_offset(self.off), n)
|
||||
self.off += n
|
||||
return data
|
||||
|
||||
def tell(self):
|
||||
return self.off
|
||||
|
||||
def seek(self, offset):
|
||||
self.off = offset
|
||||
return offset
|
||||
|
||||
|
||||
class Dumper:
|
||||
def __init__(self, dll_path):
|
||||
self.pe = dnfile.dnPE(dll_path)
|
||||
self.method_owner = {}
|
||||
for tdef in self.pe.net.mdtables.TypeDef.rows:
|
||||
for method in tdef.MethodList:
|
||||
self.method_owner[id(method.row)] = str(tdef.TypeName)
|
||||
|
||||
def resolve(self, token):
|
||||
value = token.value if hasattr(token, "value") else int(token)
|
||||
table, rid = value >> 24, value & 0xFFFFFF
|
||||
if table == 0x70:
|
||||
try:
|
||||
return repr(self.pe.net.user_strings.get(rid).value)
|
||||
except Exception:
|
||||
return f"str#{rid}"
|
||||
name = TABLES.get(table)
|
||||
if name is None:
|
||||
return f"tok{value:08x}"
|
||||
try:
|
||||
row = getattr(self.pe.net.mdtables, name).rows[rid - 1]
|
||||
except Exception:
|
||||
return f"{name}#{rid}"
|
||||
if name == "MethodDef":
|
||||
return f"{self.method_owner.get(id(row), '?')}::{row.Name}"
|
||||
if name == "MemberRef":
|
||||
return f"{getattr(row.Class.row, 'TypeName', '?')}::{row.Name}"
|
||||
if name == "Field":
|
||||
return f"fld {row.Name}"
|
||||
if name in ("TypeDef", "TypeRef"):
|
||||
return f"type {row.TypeName}"
|
||||
return name
|
||||
|
||||
def find_type(self, type_name):
|
||||
for tdef in self.pe.net.mdtables.TypeDef.rows:
|
||||
if str(tdef.TypeName) == type_name:
|
||||
return tdef
|
||||
return None
|
||||
|
||||
def dump(self, type_name, only=None, out=sys.stdout):
|
||||
tdef = self.find_type(type_name)
|
||||
if tdef is None:
|
||||
print(f"no type {type_name}", file=out)
|
||||
return
|
||||
parent = getattr(tdef.Extends.row, "TypeName", "?")
|
||||
print(f"=== {type_name} extends {parent}", file=out)
|
||||
print("fields:", [str(f.row.Name) for f in tdef.FieldList], file=out)
|
||||
for method in (m.row for m in tdef.MethodList):
|
||||
name = str(method.Name)
|
||||
if only and not any(o in name for o in only):
|
||||
continue
|
||||
print(f"--- {name}", file=out)
|
||||
if not method.Rva:
|
||||
continue
|
||||
try:
|
||||
body = CilMethodBody(_Reader(self.pe, method))
|
||||
except Exception as error:
|
||||
print(f" (no body: {error})", file=out)
|
||||
continue
|
||||
for ins in body.instructions:
|
||||
operand = ins.operand
|
||||
if type(operand).__name__.endswith("Token"):
|
||||
operand = self.resolve(operand)
|
||||
shown = "" if operand is None else operand
|
||||
print(f" {ins.offset:04x} {ins.opcode.name} {shown}", file=out)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 3:
|
||||
print(__doc__ or "usage: ildump.py <Cosmoteer.dll> <Type[,Type]> [method[,method]]")
|
||||
return 2
|
||||
dumper = Dumper(argv[1])
|
||||
only = argv[3].split(",") if len(argv) > 3 else None
|
||||
for type_name in argv[2].split(","):
|
||||
dumper.dump(type_name, only)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in New Issue
Block a user