diff --git a/harbor/logbook_auto.py b/harbor/logbook_auto.py
index 8b7c58e..1589d83 100644
--- a/harbor/logbook_auto.py
+++ b/harbor/logbook_auto.py
@@ -73,8 +73,8 @@ def log_commits(project, repo, since, head):
for sha, when, subject, body in commits:
files = [f for f in _git(repo, "show", "--name-only", "--format=", sha).splitlines() if f.strip()]
lines = [l for l in body.strip().splitlines() if l.strip() and not l.lower().startswith("co-authored-by")]
- detail = "\n".join(lines[:8]) if lines else subject
- detail += f"\n(main {sha[:8]}, {len(files)} file{'s' if len(files) != 1 else ''})"
+ detail = "\n".join(lines[:8]) + "\n" if lines else "" # the board shows the title already
+ detail += f"(main {sha[:8]}, {len(files)} file{'s' if len(files) != 1 else ''})"
added.append(logbook.add_entry(project, subject, detail, kind=_kind(files), at=_utc(when)))
return added
diff --git a/harbor/progress_map.py b/harbor/progress_map.py
index 41a58f7..67c1088 100644
--- a/harbor/progress_map.py
+++ b/harbor/progress_map.py
@@ -213,6 +213,7 @@ class ProgressMap:
units, unit_index, functions = [], {}, []
game_functions = game_done = 0
+ tiers = {t: {"functions": 0, "bytes": 0, "game_functions": 0, "game_bytes": 0} for t in STATUSES}
for addr in sorted(facts):
d = facts[addr]
size = int(d.get("size") or 0)
@@ -257,6 +258,11 @@ class ProgressMap:
kind = "game" if counted else "library" if tier == "library" else "engine" if category == "engine" \
else "unsorted" if category == "unsorted" else "library"
u["kinds"][kind] = u["kinds"].get(kind, 0) + size
+ tiers[tier]["functions"] += 1
+ tiers[tier]["bytes"] += size
+ if counted: # the Code bar's game functions, split by tier
+ tiers[tier]["game_functions"] += 1
+ tiers[tier]["game_bytes"] += size
if counted:
game_functions += 1
game_done += done
@@ -291,7 +297,7 @@ class ProgressMap:
"source": "logbook symbol table" if symbols else "src headers and names.csv",
"commit": commit, "main": main, "updated_at": stats.get("updated_at"),
"percent": percent, "logbook_percent": stats.get("game_bytes_decompiled_percent"),
- "code": code, "data": self._data(facts, rev),
+ "code": code, "data": self._data(facts, rev), "tiers": tiers,
# every function in the executable (game, engine, platform, CRT), so engine work shows too
"all_code": {"bytes": stats.get("bytes"), "functions": stats.get("functions"),
"decompiled_bytes": ((stats.get("by_status") or {}).get("decompiled") or {}).get("bytes"),
diff --git a/harbor/results_refresh.py b/harbor/results_refresh.py
index 7c037dc..05bbb9b 100644
--- a/harbor/results_refresh.py
+++ b/harbor/results_refresh.py
@@ -205,12 +205,12 @@ def _paths(project, job):
return paths
-def _argv(project, job, out):
+def _argv(project, job, out, decomp=None):
need = configured(project, job)
if job == "match":
script = Path(need["script"])
return [sys.executable, "-B", str(script), "check", "--build", "--decomp",
- str(Path(project.logbook["decomp"])), "--json", str(out)], script.parent
+ str(Path(decomp or project.logbook["decomp"])), "--json", str(out)], script.parent
command = [str(c) for c in need["command"]]
return command + ["--readable", "--jobs", "4", "--json", str(out)], Path(command[-1]).parent
@@ -260,7 +260,12 @@ def _complete(project, state, run, now, returncode=None):
def run_job(project, state, job, head, now, reason=""):
paths = _paths(project, job)
- argv, cwd = _argv(project, job, paths["partial"])
+ decomp = None
+ if job == "match" and project.logbook.get("results_snapshot") and head:
+ # a clean checkout of exactly `head`: the live checkout is the lander's and changes under a running build
+ from .logbook_auto import snapshot_at
+ decomp = snapshot_at({"snapshot": project.logbook["results_snapshot"]}, project.logbook["decomp"], head)
+ argv, cwd = _argv(project, job, paths["partial"], decomp)
for stale in [paths["partial"]] + ([paths["text_partial"]] if job == "behaviour" else []):
if stale.exists():
stale.write_bytes(b"") # a leftover of an interrupted run is never taken for this one
diff --git a/harbor/web/app.html b/harbor/web/app.html
index 9fd61de..22313cf 100644
--- a/harbor/web/app.html
+++ b/harbor/web/app.html
@@ -699,7 +699,7 @@ button.chip:hover { color: var(--accent-ink); background: var(--accent-soft); }
Loading…
-
The project is – decompiled
+
The project: game code – decompiled
@@ -849,7 +849,7 @@ const ic=(n,c)=>'
';
const INIT={Claude:'CL',Codex:'CX',DeepSeek:'DS',OpenCode:'OC',Kimi:'KM',User:'You'};
const ago=m=>m==null?'never':m<1?'just now':m<120?m+' min ago':m<2880?Math.round(m/60)+' h ago':Math.round(m/1440)+' days ago';
-const central=s=>{const d=new Date(s);return isNaN(d)?s:d.toLocaleString('en-US',{timeZone:'America/Chicago',month:'short',day:'numeric',hour:'numeric',minute:'2-digit',timeZoneName:'short'})};
+const central=s=>{const d=new Date(s);return isNaN(d)?s:d.toLocaleString('en-US',{timeZone:'America/Chicago',month:'short',day:'numeric',hour:'numeric',minute:'2-digit',hour12:true})+' CST'};
const STATE={submitted:'blue',in_review:'violet',changes_requested:'amber',approved:'green',held:'',landed:'green',withdrawn:'',rejected:'red'};
let data=null,openThread=null,current=null,doctorRan=false,unseen=0;
const HUMAN=()=>data&&data.project?data.project.human:'User';
@@ -1132,17 +1132,21 @@ el('menuBtn').onclick=()=>el('shell').classList.add('open');
el('scrim').onclick=()=>el('shell').classList.remove('open');
window.addEventListener('hashchange',()=>{const v=location.hash.slice(1);if(v&&v!==current)show(v)});
const TIERS=[['exact','Exact match','byte-identical with the game’s own compiler'],['verified','Verified','behaves the same as the game in the emulator'],
- ['readable','Readable','readable C that passed review'],['deferred','Deferred','set aside with a reason after failed attempts'],['raw','Raw','not decompiled yet'],
- ['library','Library','C runtime, D3DX9 and other linked libraries: never decompiled'],['engine','Engine','Gamebryo engine code: names only']];
+ ['readable','Readable','readable C that passed review'],['deferred','Deferred','set aside with a reason after failed attempts'],['raw','Raw','game code not decompiled yet'],['unsorted','Unsorted','not classified yet, outside the known library ranges (the few inside them count as Library)'],
+ ['library','Library','C runtime, D3DX9 and other linked libraries: never decompiled'],['engine','Engine','Gamebryo engine code not decompiled: names only']];
const fmt=n=>Number(n||0).toLocaleString('en-US');
function renderDecomp(){renderProgress();renderNextUp();renderDeferred()}
-async function renderProgress(){let p;try{p=await get('api/decomp/progress')}catch(e){put('progress','
'+esc(e.message)+'
');return}
- const t=p.tiers,own=['exact','verified','readable','deferred','raw'].reduce((a,k)=>a+t[k].functions,0),done=t.exact.functions+t.verified.functions+t.readable.functions;
- el('progressSum').textContent=fmt(done)+' of '+fmt(own)+' game functions decompiled';
+async function renderProgress(){let p;try{await pmLoad();p=PM.doc&&PM.doc.tiers?PM.doc:await get('api/decomp/progress')}catch(e){put('progress','
'+esc(e.message)+'
');return}
+ const t=tz(p.tiers),g=k=>t[k].game_functions!=null?t[k].game_functions:t[k].functions,
+ own=['exact','verified','readable','deferred','raw'].reduce((a,k)=>a+g(k),0),done=g('exact')+g('verified')+g('readable');
+ el('progressSum').textContent=fmt(done)+' of '+fmt(own)+' game functions decompiled · the bar shows every function';
put('progress',tierBar(t)+'
'}
async function renderNextUp(){if(!el('nextUp')._html)put('nextUp',empty('clock','Ranking the bands…','This takes a few seconds the first time.'));let q;
try{q=await get('api/decomp/queues')}catch(e){put('nextUp','
'
@@ -1178,7 +1182,7 @@ const LB_CATS=[['game','Game code','Firaxis game logic: decompiled to readable C
['platform','Platform','Miles, Bink, D3D9 and input wrappers: replaced in the port'],['crt','C runtime','MSVC 7.1 runtime: replaced by Rust’s standard library'],['unsorted','Unsorted','not classified yet; each lands in one of the four above']];
let LB=null;
const pct=n=>Number(n||0).toFixed(Number(n)>=10?1:2)+'%';
-const kb=b=>b>=1048576?(b/1048576).toFixed(2)+' MB':Math.round((b||0)/1024)+' KB';
+const kb=b=>b>=1e6?(b/1e6).toFixed(2)+' MB':Math.round((b||0)/1e3)+' kB';
const cssVar=n=>getComputedStyle(el('view-logbook')).getPropertyValue(n).trim();
function renderLbFigs(){const s=(LB&&LB.stats)||{},cat=k=>(s.by_category||{})[k]||{functions:0,bytes:0};
/* by_status.decompiled counts every category (engine and platform too); the game-only count comes from the progress map */
@@ -1190,15 +1194,15 @@ function renderLbFigs(){const s=(LB&&LB.stats)||{},cat=k=>(s.by_category||{})[k]
.map(([v,l,n])=>'
'+esc(v)+'
'+esc(l)+'
'+esc(n)+'
').join(''))}
async function renderLogbook(){try{LB=await get('api/logbook')}catch(e){put('lbStamp',''+esc(e.message)+'');return}
const s=LB.stats||{},cat=k=>(s.by_category||{})[k]||{functions:0,bytes:0};
- const cur=LB.milestones.find(m=>/progress/i.test(m.state||''));el('lbMilestone').textContent=cur?'Milestone '+cur.code+' · '+cur.title:'Milestones';
+ const cur=LB.milestones.filter(m=>/progress/i.test(m.state||'')).pop();el('lbMilestone').textContent=cur?'Milestone '+cur.code+' · '+cur.title:'Milestones';
const au=LB.auto||{};put('lbStamp',(s.updated_at?'Statistics updated '+esc(central(s.updated_at))+' main at '+esc(s.commit||'–')+'':'No statistics yet')+(au.last_refresh?' Keeps itself current: last checked '+esc(central(au.last_refresh))+(au.last_error?' · '+esc(String(au.last_error).slice(0,120))+'':''):''));
renderLbFigs();
put('lbLegend',LB_CATS.map(([k,n])=>''+esc(n)+'').join('')+'decompiled share under each column');
- drawLbMap();pmLoad().then(()=>{if(PM.doc){pmLayout();renderLbFigs()}});renderLbWhere(s);renderLbTrend(LB.history);renderLbMilestones(LB.milestones);
+ drawLbMap();pmLoad().then(()=>{if(PM.doc){pmLayout();renderLbFigs();renderLbWhere(s)}});renderLbWhere(s);renderLbTrend(LB.history);renderLbMilestones(LB.milestones);
el('lbLogCount').textContent=LB.log_total?LB.log_total+' entries':'';
- put('lbLog',LB.log.length?LB.log.map(e=>'
')}
async function renderLbWhere(s){const total=s.bytes||0,cat=k=>(s.by_category||{})[k]||{functions:0,bytes:0},uns=cat('unsorted'),sorted=total?100*(total-uns.bytes)/total:0;
- let tiers='';try{const p=await get('api/decomp/progress');tiers='
Game functions by how far they have come
'+tierBar(p.tiers)+tierTable(p.tiers)}catch(e){}
+ let tiers='';try{const p=PM.doc&&PM.doc.tiers?PM.doc:await get('api/decomp/progress');tiers='
Every function by how far it has come
Live from main. The game column is the one the headline counts; engine and platform functions that were decompiled anyway sit in the same tiers.