logbook: an All code bar (game + engine + platform, by bytes, with function counts) next to the game-code bar, so engine work shows too

This commit is contained in:
KeelrLeek committed 2026-10-07 16:43:38 -05:00
1 parent 59dac57bc8
commit 71e6d6cfda
2 files changed
+1964 -1960

No files matched your search

+325 -321
View File
@@ -1,321 +1,325 @@
"""The Logbook's progress map, in the style of decomp.dev: one unit per original source file, its functions and how """The Logbook's progress map, in the style of decomp.dev: one unit per original source file, its functions and how
far each has come, plus the headline share of game code decompiled and the Code and Data bars. far each has come, plus the headline share of game code decompiled and the Code and Data bars.
GET /api/logbook/progress-map answers with one compact JSON document. It is computed once and kept until one of its GET /api/logbook/progress-map answers with one compact JSON document. It is computed once and kept until one of its
inputs changes (main's HEAD, the Logbook's symbol table and statistics, the match, emulator and library results, inputs changes (main's HEAD, the Logbook's symbol table and statistics, the match, emulator and library results,
facts, names, source files, globals, or a deferral note); `?have=<version>` answers {"version", "same": true} when facts, names, source files, globals, or a deferral note); `?have=<version>` answers {"version", "same": true} when
the caller already holds the current one, so a page that polls does not download or re-lay-out anything. the caller already holds the current one, so a page that polls does not download or re-lay-out anything.
Definitions follow the Logbook exactly (open-seas tools/progress/progress.py, which writes stats.json): Definitions follow the Logbook exactly (open-seas tools/progress/progress.py, which writes stats.json):
- the symbol table is <[logbook] stats_repo>/re/symbols/pc102.csv: its category and status per function; - the symbol table is <[logbook] stats_repo>/re/symbols/pc102.csv: its category and status per function;
- counted ("game code") = category game and status not superseded or wontfix; - counted ("game code") = category game and status not superseded or wontfix;
- decompiled = status decompiled, reimplemented or verified, so percent = decompiled game bytes / game bytes, - decompiled = status decompiled, reimplemented or verified, so percent = decompiled game bytes / game bytes,
the Logbook's game_bytes_decompiled_percent. the Logbook's game_bytes_decompiled_percent.
Decompiled functions are split further, best first: exact (byte match in the [reverse] match_results), verified Decompiled functions are split further, best first: exact (byte match in the [reverse] match_results), verified
(behaves the same in the emulator, diffrun_results, not 'weak'), readable. The rest: deferred (a live DEFERRED note), (behaves the same in the emulator, diffrun_results, not 'weak'), readable. The rest: deferred (a live DEFERRED note),
raw, and, not counted, unsorted, engine (Gamebryo, named only) and library (C runtime, D3DX9, platform wrappers). raw, and, not counted, unsorted, engine (Gamebryo, named only) and library (C runtime, D3DX9, platform wrappers).
"results" says which commit the exact and verified tiers are from (harbor.results_refresh keeps them current). "results" says which commit the exact and verified tiers are from (harbor.results_refresh keeps them current).
Without a stats_repo the categories come from names.csv / facts and readable from the src headers (the queue's view). Without a stats_repo the categories come from names.csv / facts and readable from the src headers (the queue's view).
Units, first that applies: the source file from the Xbox build's assert sites ([reverse] source_files); the D3DX9 block Units, first that applies: the source file from the Xbox build's assert sites ([reverse] source_files); the D3DX9 block
(platform/d3dx9) or the import-thunk stubs from library_ranges; the unit facts gives (engine/<class>, (platform/d3dx9) or the import-thunk stubs from library_ranges; the unit facts gives (engine/<class>,
platform/<library>); otherwise platform/<library>); otherwise
<category> plus its 64 KB address block, labelled "(no source file)".""" <category> plus its 64 KB address block, labelled "(no source file)"."""
import csv import csv
import hashlib import hashlib
import json import json
import subprocess import subprocess
import threading import threading
import time import time
from pathlib import Path from pathlib import Path
from .queue import FunctionQueue from .queue import FunctionQueue
STATUSES = ("exact", "verified", "readable", "deferred", "raw", "unsorted", "engine", "library") STATUSES = ("exact", "verified", "readable", "deferred", "raw", "unsorted", "engine", "library")
DECOMPILED = ("decompiled", "reimplemented", "verified") DECOMPILED = ("decompiled", "reimplemented", "verified")
EXCLUDED = ("superseded", "wontfix") EXCLUDED = ("superseded", "wontfix")
UNIT_FIELDS = ("name", "kind", "lo", "hi", "bytes", "counted", "exact", "verified", "readable", "functions") UNIT_FIELDS = ("name", "kind", "lo", "hi", "bytes", "counted", "exact", "verified", "readable", "functions")
FUNCTION_FIELDS = ("unit", "address", "size", "status", "name") FUNCTION_FIELDS = ("unit", "address", "size", "status", "name")
KEY_TTL = 15 # seconds a computed input key is trusted before the files and HEAD are looked at again KEY_TTL = 15 # seconds a computed input key is trusted before the files and HEAD are looked at again
def _int(text): def _int(text):
return int(str(text), 16) if str(text).lower().startswith("0x") else int(text) return int(str(text), 16) if str(text).lower().startswith("0x") else int(text)
def _mtime(path): def _mtime(path):
try: try:
return Path(path).stat().st_mtime_ns if path else None return Path(path).stat().st_mtime_ns if path else None
except OSError: except OSError:
return None return None
def _git(repo, *args): def _git(repo, *args):
try: try:
out = subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True, timeout=20, out = subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True, timeout=20,
encoding="utf-8", errors="replace") encoding="utf-8", errors="replace")
except (OSError, subprocess.TimeoutExpired): except (OSError, subprocess.TimeoutExpired):
return "" return ""
return out.stdout.strip() if out.returncode == 0 else "" return out.stdout.strip() if out.returncode == 0 else ""
def _utc(iso): def _utc(iso):
import datetime import datetime
try: try:
return datetime.datetime.fromisoformat(iso).astimezone(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") return datetime.datetime.fromisoformat(iso).astimezone(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
except ValueError: except ValueError:
return None return None
class ProgressMap: class ProgressMap:
def __init__(self, project, queue_factory): def __init__(self, project, queue_factory):
self.project = project self.project = project
self.queue_factory = queue_factory # -> FunctionQueue on a connection this thread may use self.queue_factory = queue_factory # -> FunctionQueue on a connection this thread may use
self.lock = threading.Lock() self.lock = threading.Lock()
self.value = None # (key, document) self.value = None # (key, document)
self.checked = (0.0, None) # (monotonic time, key) self.checked = (0.0, None) # (monotonic time, key)
self.computations = 0 self.computations = 0
# ------------------------------------------------------------------ inputs # ------------------------------------------------------------------ inputs
def _config(self): def _config(self):
return self.project.reverse or {}, getattr(self.project, "logbook", None) or {} return self.project.reverse or {}, getattr(self.project, "logbook", None) or {}
def _symbols_path(self): def _symbols_path(self):
_, lb = self._config() _, lb = self._config()
return Path(lb["stats_repo"]) / "re" / "symbols" / "pc102.csv" if lb.get("stats_repo") else None return Path(lb["stats_repo"]) / "re" / "symbols" / "pc102.csv" if lb.get("stats_repo") else None
def _decomp(self): def _decomp(self):
rev, lb = self._config() rev, lb = self._config()
if lb.get("decomp"): if lb.get("decomp"):
return Path(lb["decomp"]) return Path(lb["decomp"])
return Path(rev["annotations"]).parent if rev.get("annotations") else None return Path(rev["annotations"]).parent if rev.get("annotations") else None
def _logbook_dir(self): def _logbook_dir(self):
from . import logbook from . import logbook
return logbook.folder(self.project) return logbook.folder(self.project)
def key(self, q): def key(self, q):
rev, _ = self._config() rev, _ = self._config()
decomp = self._decomp() decomp = self._decomp()
head = _git(decomp, "rev-parse", "HEAD") if decomp and (decomp / ".git").exists() else "" head = _git(decomp, "rev-parse", "HEAD") if decomp and (decomp / ".git").exists() else ""
paths = [self._symbols_path(), self._logbook_dir() / "stats.json", self._logbook_dir() / "history.json", paths = [self._symbols_path(), self._logbook_dir() / "stats.json", self._logbook_dir() / "history.json",
self._logbook_dir() / "results.json"] self._logbook_dir() / "results.json"]
paths += [rev.get(k) for k in ("facts", "match_results", "diffrun_results", "library_results", paths += [rev.get(k) for k in ("facts", "match_results", "diffrun_results", "library_results",
"library_ranges", "source_files")] "library_ranges", "source_files")]
if rev.get("annotations"): if rev.get("annotations"):
paths += [Path(rev["annotations"]) / "names.csv", Path(rev["annotations"]) / "globals.csv"] paths += [Path(rev["annotations"]) / "names.csv", Path(rev["annotations"]) / "globals.csv"]
try: try:
note = q.h.conn.execute("SELECT MAX(id) FROM notes").fetchone()[0] note = q.h.conn.execute("SELECT MAX(id) FROM notes").fetchone()[0]
except Exception: except Exception:
note = None note = None
raw = json.dumps([head, note, [str(p) for p in paths], [_mtime(p) for p in paths]]) raw = json.dumps([head, note, [str(p) for p in paths], [_mtime(p) for p in paths]])
return hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16], head return hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16], head
# ------------------------------------------------------------------ answer # ------------------------------------------------------------------ answer
def get(self, have=None): def get(self, have=None):
with self.lock: with self.lock:
q = self.queue_factory() q = self.queue_factory()
now = time.monotonic() now = time.monotonic()
if self.value and now - self.checked[0] < KEY_TTL: if self.value and now - self.checked[0] < KEY_TTL:
key = self.checked[1] key = self.checked[1]
else: else:
key, head = self.key(q) key, head = self.key(q)
self.checked = (now, key) self.checked = (now, key)
if not self.value or self.value[0] != key: if not self.value or self.value[0] != key:
document = self.compute(q, head) document = self.compute(q, head)
if document is None and self.value: # the symbol table is being rewritten: keep the last if document is None and self.value: # the symbol table is being rewritten: keep the last
document = self.value[1] document = self.value[1]
elif document is None: elif document is None:
document = self.compute(q, head, use_symbols=False) document = self.compute(q, head, use_symbols=False)
document["version"] = key document["version"] = key
self.value = (key, document) self.value = (key, document)
self.computations += 1 self.computations += 1
if have and have == self.value[0]: if have and have == self.value[0]:
return {"version": have, "same": True} return {"version": have, "same": True}
return self.value[1] return self.value[1]
# ------------------------------------------------------------------ the computation # ------------------------------------------------------------------ the computation
def _symbols(self, expected): def _symbols(self, expected):
"""pc102.csv as {address: (category, status, name)}, or None while it is missing or half written.""" """pc102.csv as {address: (category, status, name)}, or None while it is missing or half written."""
path = self._symbols_path() path = self._symbols_path()
if not path or not path.exists(): if not path or not path.exists():
return None return None
try: try:
with path.open(encoding="utf-8", newline="") as stream: with path.open(encoding="utf-8", newline="") as stream:
rows = {_int(r["address"]): (r["category"], r["status"], r.get("name") or "") rows = {_int(r["address"]): (r["category"], r["status"], r.get("name") or "")
for r in csv.DictReader(stream)} for r in csv.DictReader(stream)}
except (OSError, ValueError, KeyError): except (OSError, ValueError, KeyError):
return None return None
return rows if len(rows) >= 0.98 * expected else None return rows if len(rows) >= 0.98 * expected else None
def _source_files(self, path): def _source_files(self, path):
out = {} out = {}
if path and Path(path).exists(): if path and Path(path).exists():
with open(path, encoding="utf-8", newline="") as stream: with open(path, encoding="utf-8", newline="") as stream:
for row in csv.DictReader(stream): for row in csv.DictReader(stream):
if row.get("source_file"): if row.get("source_file"):
out[_int(row["pc_address"])] = row["source_file"] out[_int(row["pc_address"])] = row["source_file"]
return out return out
def _all_ranges(self, path): def _all_ranges(self, path):
"""[(lo, hi, label)] of the library_ranges csv (the D3DX9 block and the compiler's stubs).""" """[(lo, hi, label)] of the library_ranges csv (the D3DX9 block and the compiler's stubs)."""
out = [] out = []
if path and Path(path).exists(): if path and Path(path).exists():
with open(path, encoding="utf-8", newline="") as stream: with open(path, encoding="utf-8", newline="") as stream:
for r in csv.DictReader(stream): for r in csv.DictReader(stream):
verdict = r.get("verdict", "") verdict = r.get("verdict", "")
label = "platform/d3dx9" if verdict.startswith("D3DX") else \ label = "platform/d3dx9" if verdict.startswith("D3DX") else \
"Import thunks and unwind funclets" if "stubs" in verdict else None "Import thunks and unwind funclets" if "stubs" in verdict else None
if label: if label:
out.append((_int(r["start"]), _int(r["end"]), label)) out.append((_int(r["start"]), _int(r["end"]), label))
return out return out
def _data(self, facts, rev): def _data(self, facts, rev):
"""Named globals against the distinct .data/.rdata addresses the code touches (needs sections.json).""" """Named globals against the distinct .data/.rdata addresses the code touches (needs sections.json)."""
facts_path = Path(rev["facts"]) if rev.get("facts") else None facts_path = Path(rev["facts"]) if rev.get("facts") else None
sections_path = facts_path.parent / "sections.json" if facts_path else None sections_path = facts_path.parent / "sections.json" if facts_path else None
globals_path = Path(rev["annotations"]) / "globals.csv" if rev.get("annotations") else None globals_path = Path(rev["annotations"]) / "globals.csv" if rev.get("annotations") else None
if not sections_path or not sections_path.exists() or not globals_path or not globals_path.exists(): if not sections_path or not sections_path.exists() or not globals_path or not globals_path.exists():
return None return None
spans = [(s["start"], s["end"]) for s in json.loads(sections_path.read_text(encoding="utf-8")) spans = [(s["start"], s["end"]) for s in json.loads(sections_path.read_text(encoding="utf-8"))
if s.get("name") in (".data", ".rdata")] if s.get("name") in (".data", ".rdata")]
touched = set() touched = set()
for d in facts.values(): for d in facts.values():
for ref in d.get("data", []): for ref in d.get("data", []):
a = _int(ref) a = _int(ref)
if any(lo <= a < hi for lo, hi in spans): if any(lo <= a < hi for lo, hi in spans):
touched.add(a) touched.add(a)
with globals_path.open(encoding="utf-8", newline="") as stream: with globals_path.open(encoding="utf-8", newline="") as stream:
named = {_int(r["address"]) for r in csv.DictReader(line for line in stream if not line.startswith("#")) named = {_int(r["address"]) for r in csv.DictReader(line for line in stream if not line.startswith("#"))
if r.get("address") and r.get("name")} if r.get("address") and r.get("name")}
if not touched: if not touched:
return None return None
return {"addresses": len(touched), "named": len(touched & named), "globals": len(named), return {"addresses": len(touched), "named": len(touched & named), "globals": len(named),
"bytes": sum(hi - lo for lo, hi in spans)} "bytes": sum(hi - lo for lo, hi in spans)}
def compute(self, q, head, use_symbols=True): def compute(self, q, head, use_symbols=True):
rev, _ = self._config() rev, _ = self._config()
facts = q.r._facts() facts = q.r._facts()
symbols = self._symbols(len(facts)) if use_symbols else None symbols = self._symbols(len(facts)) if use_symbols else None
if use_symbols and self._symbols_path() and symbols is None and self._symbols_path().exists(): if use_symbols and self._symbols_path() and symbols is None and self._symbols_path().exists():
return None return None
names = q.r._csv("names.csv") names = q.r._csv("names.csv")
statuses = None if symbols else q.statuses() statuses = None if symbols else q.statuses()
exact = {a for a, row in q._results("match_results").items() exact = {a for a, row in q._results("match_results").items()
if str(row.get("status")) in ("match", "exact", "exact-relaxed")} if str(row.get("status")) in ("match", "exact", "exact-relaxed")}
verified = {a for a, row in q._results("diffrun_results").items() verified = {a for a, row in q._results("diffrun_results").items()
if str(row.get("verdict")) == "same" and not row.get("weak")} if str(row.get("verdict")) == "same" and not row.get("weak")}
library = set(q._results("library_results")) library = set(q._results("library_results"))
ranges = self._all_ranges(rev.get("library_ranges")) ranges = self._all_ranges(rev.get("library_ranges"))
lib_ranges = [(lo, hi) for lo, hi, _ in ranges] lib_ranges = [(lo, hi) for lo, hi, _ in ranges]
deferred = q.deferrals() deferred = q.deferrals()
sources = self._source_files(rev.get("source_files")) sources = self._source_files(rev.get("source_files"))
units, unit_index, functions = [], {}, [] units, unit_index, functions = [], {}, []
game_functions = game_done = 0 game_functions = game_done = 0
for addr in sorted(facts): for addr in sorted(facts):
d = facts[addr] d = facts[addr]
size = int(d.get("size") or 0) size = int(d.get("size") or 0)
if symbols and addr in symbols: if symbols and addr in symbols:
category, status, sym_name = symbols[addr] category, status, sym_name = symbols[addr]
else: else:
category = q._category(addr) category = q._category(addr)
status = "decompiled" if (statuses or {}).get(addr, ("raw",))[0] == "readable" else "classified" status = "decompiled" if (statuses or {}).get(addr, ("raw",))[0] == "readable" else "classified"
sym_name = "" sym_name = ""
row = names.get(addr) row = names.get(addr)
name = (row[1] if row and len(row) > 1 and row[1] else "") or sym_name or d.get("name") or f"FUN_{addr:06X}" name = (row[1] if row and len(row) > 1 and row[1] else "") or sym_name or d.get("name") or f"FUN_{addr:06X}"
counted = category == "game" and status not in EXCLUDED counted = category == "game" and status not in EXCLUDED
done = status in DECOMPILED done = status in DECOMPILED
is_lib = q.is_library(addr, library, lib_ranges) or category in ("library", "platform", "crt") is_lib = q.is_library(addr, library, lib_ranges) or category in ("library", "platform", "crt")
if counted or (done and not is_lib) or (category == "unsorted" and not is_lib): if counted or (done and not is_lib) or (category == "unsorted" and not is_lib):
tier = "exact" if done and addr in exact else "verified" if done and addr in verified else \ tier = "exact" if done and addr in exact else "verified" if done and addr in verified else \
"readable" if done else "deferred" if addr in deferred else \ "readable" if done else "deferred" if addr in deferred else \
"raw" if counted else "unsorted" "raw" if counted else "unsorted"
elif is_lib: elif is_lib:
tier = "library" tier = "library"
else: else:
tier = "engine" tier = "engine"
source = sources.get(addr) source = sources.get(addr)
in_range = next((label for lo, hi, label in ranges if lo <= addr < hi), None) in_range = next((label for lo, hi, label in ranges if lo <= addr < hi), None)
if source: if source:
unit = source unit = source
elif in_range: elif in_range:
unit = in_range unit = in_range
elif d.get("unit"): elif d.get("unit"):
unit = d["unit"] unit = d["unit"]
else: else:
unit = f"{category} · 0x{addr & ~0xFFFF:06X} block (no source file)" unit = f"{category} · 0x{addr & ~0xFFFF:06X} block (no source file)"
if unit not in unit_index: if unit not in unit_index:
unit_index[unit] = len(units) unit_index[unit] = len(units)
units.append({"name": unit, "lo": addr, "hi": addr + size, "bytes": 0, "counted": 0, "exact": 0, units.append({"name": unit, "lo": addr, "hi": addr + size, "bytes": 0, "counted": 0, "exact": 0,
"verified": 0, "readable": 0, "functions": 0, "kinds": {}}) "verified": 0, "readable": 0, "functions": 0, "kinds": {}})
u = units[unit_index[unit]] u = units[unit_index[unit]]
u["hi"] = max(u["hi"], addr + size) u["hi"] = max(u["hi"], addr + size)
u["bytes"] += size u["bytes"] += size
u["functions"] += 1 u["functions"] += 1
kind = "game" if counted else "library" if tier == "library" else "engine" if category == "engine" \ kind = "game" if counted else "library" if tier == "library" else "engine" if category == "engine" \
else "unsorted" if category == "unsorted" else "library" else "unsorted" if category == "unsorted" else "library"
u["kinds"][kind] = u["kinds"].get(kind, 0) + size u["kinds"][kind] = u["kinds"].get(kind, 0) + size
if counted: if counted:
game_functions += 1 game_functions += 1
game_done += done game_done += done
u["counted"] += size u["counted"] += size
if tier in ("exact", "verified", "readable"): if tier in ("exact", "verified", "readable"):
u[tier] += size u[tier] += size
functions.append([unit_index[unit], addr, size, STATUSES.index(tier), name]) functions.append([unit_index[unit], addr, size, STATUSES.index(tier), name])
for u in units: for u in units:
kinds = u.pop("kinds") kinds = u.pop("kinds")
u["kind"] = max(kinds, key=kinds.get) if kinds else "unsorted" # what most of its bytes are u["kind"] = max(kinds, key=kinds.get) if kinds else "unsorted" # what most of its bytes are
code = {k: sum(u[k] for u in units) for k in ("exact", "verified", "readable")} code = {k: sum(u[k] for u in units) for k in ("exact", "verified", "readable")}
code["bytes"] = sum(u["counted"] for u in units) code["bytes"] = sum(u["counted"] for u in units)
code["functions"], code["decompiled_functions"] = game_functions, game_done code["functions"], code["decompiled_functions"] = game_functions, game_done
percent = round(100 * (code["exact"] + code["verified"] + code["readable"]) / code["bytes"], 2) \ percent = round(100 * (code["exact"] + code["verified"] + code["readable"]) / code["bytes"], 2) \
if code["bytes"] else 0.0 if code["bytes"] else 0.0
from . import logbook from . import logbook
stats = logbook._read(self.project, "stats.json", None) or {} stats = logbook._read(self.project, "stats.json", None) or {}
history = sorted(logbook._read(self.project, "history.json", []), key=lambda p: p.get("at", "")) history = sorted(logbook._read(self.project, "history.json", []), key=lambda p: p.get("at", ""))
decomp = self._decomp() decomp = self._decomp()
commit = {"sha": stats.get("commit"), "at": None} commit = {"sha": stats.get("commit"), "at": None}
if commit["sha"] and decomp: if commit["sha"] and decomp:
commit["at"] = _utc(_git(decomp, "log", "-1", "--format=%cI", commit["sha"])) or None commit["at"] = _utc(_git(decomp, "log", "-1", "--format=%cI", commit["sha"])) or None
main = {"sha": head[:7] if head else None, main = {"sha": head[:7] if head else None,
"at": _utc(_git(decomp, "log", "-1", "--format=%cI", head)) if head else None} "at": _utc(_git(decomp, "log", "-1", "--format=%cI", head)) if head else None}
from . import results_refresh from . import results_refresh
return { return {
"project": self.project.title or self.project.name, "project": self.project.title or self.project.name,
"results": results_refresh.freshness(self.project), "results": results_refresh.freshness(self.project),
"source": "logbook symbol table" if symbols else "src headers and names.csv", "source": "logbook symbol table" if symbols else "src headers and names.csv",
"commit": commit, "main": main, "updated_at": stats.get("updated_at"), "commit": commit, "main": main, "updated_at": stats.get("updated_at"),
"percent": percent, "logbook_percent": stats.get("game_bytes_decompiled_percent"), "percent": percent, "logbook_percent": stats.get("game_bytes_decompiled_percent"),
"code": code, "data": self._data(facts, rev), "code": code, "data": self._data(facts, rev),
"history": [[p.get("at"), p.get("decompiled"), p.get("commit"), p.get("game_bytes")] # every function in the executable (game, engine, platform, CRT), so engine work shows too
for p in history if p.get("decompiled") is not None], "all_code": {"bytes": stats.get("bytes"), "functions": stats.get("functions"),
"statuses": list(STATUSES), "decompiled_bytes": ((stats.get("by_status") or {}).get("decompiled") or {}).get("bytes"),
"unit_fields": list(UNIT_FIELDS), "decompiled_functions": ((stats.get("by_status") or {}).get("decompiled") or {}).get("functions")},
"units": [[u[k] for k in UNIT_FIELDS] for u in units], "history": [[p.get("at"), p.get("decompiled"), p.get("commit"), p.get("game_bytes")]
"function_fields": list(FUNCTION_FIELDS), for p in history if p.get("decompiled") is not None],
"functions": functions, "statuses": list(STATUSES),
} "unit_fields": list(UNIT_FIELDS),
"units": [[u[k] for k in UNIT_FIELDS] for u in units],
"function_fields": list(FUNCTION_FIELDS),
_MAPS = {} "functions": functions,
_LOCK = threading.Lock() }
def map_for(project): _MAPS = {}
with _LOCK: _LOCK = threading.Lock()
if project.name not in _MAPS:
from . import decomp_api
api = decomp_api.api_for(project) def map_for(project):
_MAPS[project.name] = ProgressMap(project, lambda: FunctionQueue(api.h)) with _LOCK:
return _MAPS[project.name] if project.name not in _MAPS:
from . import decomp_api
api = decomp_api.api_for(project)
def handle(project, query): _MAPS[project.name] = ProgressMap(project, lambda: FunctionQueue(api.h))
if not (project.reverse or {}).get("facts"): return _MAPS[project.name]
return 404, {"error": "this project has no [reverse] table"}
return 200, map_for(project).get(query.get("have"))
def handle(project, query):
if not (project.reverse or {}).get("facts"):
return 404, {"error": "this project has no [reverse] table"}
return 200, map_for(project).get(query.get("have"))
+1639 -1639
View File
File diff suppressed because it is too large. Load diff