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
140 lines
4.8 KiB
Python
Executable File
140 lines
4.8 KiB
Python
Executable File
#!/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))
|