forked from Keelrleek/Harbor
Logbook clarity: game-code headline, CST times, one size unit, tier table from the progress map with a game column, Decomp view counts game functions, ship's log without repeated or cut titles, newest milestone in the banner; whole-program check builds a clean snapshot of main
This commit is contained in:
1 parent
783c46da56
commit
b8dab67871
4 files changed
+39
-23
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
+22
-17
@@ -699,7 +699,7 @@ button.chip:hover { color: var(--accent-ink); background: var(--accent-soft); }
|
||||
<div class="lb-stamp" id="lbStamp">Loading…</div></div></section>
|
||||
<section class="card" aria-labelledby="h-pm" id="pmCard">
|
||||
<div class="body">
|
||||
<h2 class="pm-title" id="h-pm"><span id="pmProject">The project</span> is <b id="pmPct">–</b> decompiled</h2>
|
||||
<h2 class="pm-title" id="h-pm"><span id="pmProject">The project</span>: game code <b id="pmPct">–</b> decompiled</h2>
|
||||
<div class="pm-bars" id="pmBars"></div>
|
||||
<div class="note pm-fresh" id="pmFresh"></div>
|
||||
<div class="pm-commit"><span id="pmCommit"></span><span class="grow"></span>
|
||||
@@ -849,7 +849,7 @@ const ic=(n,c)=>'<svg class="i'+(c?' '+c:'')+'" aria-hidden="true"><use href="st
|
||||
const empty=(icon,title,text)=>'<div class="empty">'+ic(icon)+'<strong>'+esc(title)+'</strong><span>'+(text||'')+'</span></div>';
|
||||
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','<div class="empty err">'+esc(e.message)+'</div>');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','<div class="empty err">'+esc(e.message)+'</div>');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)+'<div class="lb-legend">'+TIERS.map(([k,l])=>'<span><i class="tier-key tier-'+k+'"></i>'+esc(l)+' '+fmt(t[k].functions)+'</span>').join('')+'</div><div class="note" style="margin-top:10px">Bytes, categories, the trend and the ship’s log: <a href="#logbook" data-go="logbook">open the Logbook</a>.</div>');}
|
||||
function tierTable(t){return '<table style="margin-top:14px"><thead><tr><th>Tier</th><th style="text-align:right">Functions</th><th style="text-align:right">Bytes</th><th>Meaning</th></tr></thead><tbody>'
|
||||
+TIERS.map(([k,l,d])=>'<tr><td><span class="tier-key tier-'+k+'"></span>'+esc(l)+'</td><td class="num">'+fmt(t[k].functions)+'</td><td class="num">'+fmt(t[k].bytes)+'</td><td class="note">'+esc(d)+'</td></tr>').join('')+'</tbody></table>'}
|
||||
function tierBar(t){return '<div class="tierbar" role="img" aria-label="'+esc(TIERS.map(([k,l])=>l+' '+t[k].functions).join(', '))+'">'+TIERS.map(([k,l])=>t[k].functions?'<span class="tier-'+k+'" style="flex:'+t[k].functions+'" title="'+esc(l)+': '+fmt(t[k].functions)+' functions"></span>':'').join('')+'</div>'}
|
||||
const tz=t=>Object.fromEntries(TIERS.map(([k])=>[k,t[k]||{functions:0,bytes:0}]));
|
||||
function tierTable(t){t=tz(t);const g=k=>t[k].game_functions,has=g('raw')!=null,sum=f=>TIERS.reduce((a,[k])=>a+(f(k)||0),0);
|
||||
return '<table style="margin-top:14px"><thead><tr><th>Tier</th>'+(has?'<th style="text-align:right">Game functions</th>':'')+'<th style="text-align:right">All functions</th><th style="text-align:right">Bytes (all)</th><th>Meaning</th></tr></thead><tbody>'
|
||||
+TIERS.map(([k,l,d])=>'<tr><td><span class="tier-key tier-'+k+'"></span>'+esc(l)+'</td>'+(has?'<td class="num">'+fmt(g(k))+'</td>':'')+'<td class="num">'+fmt(t[k].functions)+'</td><td class="num">'+fmt(t[k].bytes)+'</td><td class="note">'+esc(d)+'</td></tr>').join('')
|
||||
+'<tr><td><b>Total</b></td>'+(has?'<td class="num"><b>'+fmt(sum(g))+'</b></td>':'')+'<td class="num"><b>'+fmt(sum(k=>t[k].functions))+'</b></td><td class="num"><b>'+fmt(sum(k=>t[k].bytes))+'</b></td><td class="note">exact + verified + readable = decompiled</td></tr></tbody></table>'}
|
||||
function tierBar(t){t=tz(t);return '<div class="tierbar" role="img" aria-label="'+esc(TIERS.map(([k,l])=>l+' '+t[k].functions).join(', '))+'">'+TIERS.map(([k,l])=>t[k].functions?'<span class="tier-'+k+'" style="flex:'+t[k].functions+'" title="'+esc(l)+': '+fmt(t[k].functions)+' functions"></span>':'').join('')+'</div>'}
|
||||
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','<div class="empty err">'+esc(e.message)+'</div>');return}
|
||||
put('nextUp',q.length?q.map(a=>'<div class="band"><div class="cell-who">'+avatar(a.agent,'xs')+'<b>'+esc(a.agent)+'</b><span class="note mono">'+esc(a.bands.map(b=>b.join('–')).join(', '))+'</span></div>'
|
||||
@@ -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])=>'<div class="lb-fig"><div class="v">'+esc(v)+'</div><div class="l">'+esc(l)+'</div><div class="n">'+esc(n)+'</div></div>').join(''))}
|
||||
async function renderLogbook(){try{LB=await get('api/logbook')}catch(e){put('lbStamp','<span class="err">'+esc(e.message)+'</span>');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 <b>'+esc(central(s.updated_at))+'</b><br>main at <b>'+esc(s.commit||'–')+'</b>':'No statistics yet')+(au.last_refresh?'<br>Keeps itself current: last checked '+esc(central(au.last_refresh))+(au.last_error?' · <span class="err">'+esc(String(au.last_error).slice(0,120))+'</span>':''):''));
|
||||
renderLbFigs();
|
||||
put('lbLegend',LB_CATS.map(([k,n])=>'<span><i class="lb-sw'+(k==='unsorted'?' lb-hatch':'')+'" style="background:var(--lb-'+k+')"></i>'+esc(n)+'</span>').join('')+'<span><i class="lb-sw" style="background:var(--lb-done)"></i>decompiled share under each column</span>');
|
||||
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=>'<li><time>'+esc(central(e.at))+'</time><div><div class="t"><span class="kind">'+esc(e.kind)+'</span>'+esc(e.title)+'</div><div class="d">'+esc(e.detail)+'</div></div></li>').join(''):'<li class="note">No entries yet.</li>')}
|
||||
put('lbLog',LB.log.length?LB.log.map(e=>'<li><time>'+esc(central(e.at))+'</time><div><div class="t"><span class="kind">'+esc(e.kind)+'</span>'+esc(e.title)+'</div><div class="d">'+esc(String(e.detail||'').startsWith(e.title)?String(e.detail).slice(e.title.length).replace(/^\n/,''):e.detail)+'</div></div></li>').join(''):'<li class="note">No entries yet.</li>')}
|
||||
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='<h3 class="sub-h" style="margin-top:18px">Game functions by how far they have come</h3>'+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='<h3 class="sub-h" style="margin-top:18px">Every function by how far it has come</h3><div class="note">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.</div>'+tierBar(p.tiers)+tierTable(p.tiers)}catch(e){}
|
||||
put('lbWhere','<div><span class="lb-big">'+pct(sorted)+'</span> <span class="note">of the executable’s code is sorted into a category · '+fmt(uns.functions)+' functions ('+kb(uns.bytes)+') still unsorted</span></div>'
|
||||
+'<div class="lb-stack" role="img" aria-label="Code bytes by category">'+LB_CATS.map(([k,n])=>{const sh=total?100*cat(k).bytes/total:0;return '<i class="'+(k==='unsorted'?'lb-hatch':'')+'" style="flex:0 0 '+sh.toFixed(3)+'%;background:var(--lb-'+k+')" title="'+esc(n)+': '+sh.toFixed(1)+'% of bytes"></i>'}).join('')+'</div>'
|
||||
+'<div class="lb-axis"><span>0</span><span>sorted '+pct(sorted)+' · unsorted '+pct(100-sorted)+'</span><span>'+kb(total)+'</span></div>'
|
||||
@@ -1230,7 +1234,8 @@ const kB=b=>b>=1e6?(b/1e6).toFixed(2)+' MB':(b/1e3).toFixed(2)+' kB';
|
||||
const hx=a=>'0x'+Number(a).toString(16).toUpperCase();
|
||||
const pc=(b,t)=>(t?100*b/t:0).toFixed(2);
|
||||
function rel(iso){const t=Date.parse(iso||'');if(!t)return 'at an unknown time';const s=(Date.now()-t)/1000;if(s<90)return 'just now';const m=s/60;if(m<90)return Math.round(m)+' minutes ago';const h=m/60;if(h<36)return Math.round(h)+' hours ago';return Math.round(h/24)+' days ago'}
|
||||
async function pmLoad(){if(PM.busy)return;PM.busy=true;try{
|
||||
function pmLoad(){if(!PM.pending)PM.pending=pmLoadOnce().finally(()=>{PM.pending=null});return PM.pending}
|
||||
async function pmLoadOnce(){if(PM.busy)return;PM.busy=true;try{
|
||||
const d=await get('api/logbook/progress-map'+(PM.version?'?have='+encodeURIComponent(PM.version):''));if(d.same)return;
|
||||
const U=d.unit_fields;d.units=d.units.map((u,i)=>{const o={i,fns:[]};U.forEach((k,j)=>o[k]=u[j]);o.pct=o.counted?100*(o.exact+o.verified+o.readable)/o.counted:null;o.lname=o.name.toLowerCase();return o});
|
||||
d.functions.forEach(f=>{const o={unit:f[0],address:f[1],size:f[2],status:d.statuses[f[3]],name:f[4]};o.lname=o.name.toLowerCase();d.units[o.unit].fns.push(o)});delete d.functions;
|
||||
@@ -1363,7 +1368,7 @@ const fmt=s=>esc(s).replace(/`([^`]+)`/g,'<code>$1</code>');
|
||||
const DAY_FMT=new Intl.DateTimeFormat('en-US',{timeZone:CENTRAL,year:'numeric',month:'numeric',day:'numeric'});
|
||||
const TIME_FMT=new Intl.DateTimeFormat('en-US',{timeZone:CENTRAL,hour:'numeric',minute:'2-digit',timeZoneName:'short'});
|
||||
const centralDay=d=>DAY_FMT.format(d);
|
||||
const centralTime=d=>TIME_FMT.format(d);
|
||||
const centralTime=d=>TIME_FMT.format(d).replace(/ C[DS]T$/,'')+' CST';
|
||||
const since=s=>{if(!s)return 'never';const m=Math.round((Date.now()-new Date(s))/60000);return m<1?'just now':m<120?m+' min ago':m<2880?Math.round(m/60)+' h ago':Math.round(m/1440)+' days ago'};
|
||||
let people=[],byName={},human='User',messages=[],lastId=0,filter='All',thread=null,sending=false,receipts={},overviewData=null,loaded=false;
|
||||
async function getJSON(path){const r=await fetch(path,{cache:'no-store'});const j=await r.json().catch(()=>({}));if(!r.ok)throw new Error(j.error||('HTTP '+r.status));return j}
|
||||
|
||||
Reference in new issue
Block a user