Separate looted currency and preserve growing realm state in atomic chunks
Core tests / core (push) Successful in 49s
Core tests / core (push) Successful in 49s
This commit is contained in:
@@ -4,6 +4,12 @@ AuthDBExecute([[CREATE TABLE IF NOT EXISTS azerothai_outcomes (
|
||||
abandoned INT UNSIGNED NOT NULL DEFAULT 0,loot_events INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
loot_items INT UNSIGNED NOT NULL DEFAULT 0,party_kills INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)]])
|
||||
AuthDBExecute([[CREATE TABLE IF NOT EXISTS azerothai_currency (
|
||||
guid INT UNSIGNED PRIMARY KEY,looted_copper BIGINT UNSIGNED NOT NULL DEFAULT 0)]])
|
||||
local money={}
|
||||
RegisterPlayerEvent(37,function(_,p,amount)
|
||||
if p:IsPlayerbot() and amount and amount>0 then local id=p:GetGUIDLow();money[id]=(money[id] or 0)+amount end
|
||||
end)
|
||||
local pending={}
|
||||
local function add(p,key,amount)
|
||||
if not p:IsPlayerbot() then return end
|
||||
@@ -28,7 +34,10 @@ local function flush()
|
||||
for guid,r in pairs(pending) do
|
||||
AuthDBExecute('INSERT INTO azerothai_outcomes(guid,quests,abandoned,loot_events,loot_items,party_kills) VALUES ('..guid..','..r.quests..','..r.abandoned..','..r.loot_events..','..r.loot_items..','..r.party_kills..') ON DUPLICATE KEY UPDATE quests=quests+VALUES(quests),abandoned=abandoned+VALUES(abandoned),loot_events=loot_events+VALUES(loot_events),loot_items=loot_items+VALUES(loot_items),party_kills=party_kills+VALUES(party_kills),updated_at=NOW()')
|
||||
end
|
||||
pending={}
|
||||
for guid,amount in pairs(money) do
|
||||
AuthDBExecute('INSERT INTO azerothai_currency(guid,looted_copper) VALUES ('..guid..','..amount..') ON DUPLICATE KEY UPDATE looted_copper=looted_copper+VALUES(looted_copper)')
|
||||
end
|
||||
money={};pending={}
|
||||
end
|
||||
CreateLuaEvent(flush,60000,0)
|
||||
-- Native profiler JSON is generated every five minutes; enabling it is an operator setting.
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {RuntimeConfig} from '@/core/types';
|
||||
import {observeInsights,activityReason,reportWindow,summarize,DAY,type Cursor,type Evidence} from '@/core/realm-insights';
|
||||
const recovery=z.object({id:z.number().int().positive(),guid:z.number().int().positive(),name:z.string().max(64),at:z.number().finite(),detail:z.string().max(255),status:z.enum(['pending','resumed','no_progress','unverified']).optional(),checkedAt:z.number().nonnegative().optional(),result:z.string().max(255).optional()});
|
||||
const count=z.number().int().nonnegative();
|
||||
const outcome=z.object({guid:count,name:z.string().max(64),quests:count,abandoned:count,lootEvents:count,lootItems:count,partyKills:count,rewarded:count});
|
||||
const outcome=z.object({guid:count,name:z.string().max(64),quests:count,abandoned:count,lootEvents:count,lootItems:count,partyKills:count,rewarded:count,lootedCopper:count.default(0)});
|
||||
const measurement=z.number().finite().nonnegative();
|
||||
const performance=z.object({at:measurement,enabled:z.boolean(),ticks:measurement,averageTickMs:measurement,rows:z.array(z.object({category:z.string().max(40),name:z.string().max(160),calls:measurement,totalMs:measurement,averageMs:measurement,maxMs:measurement,perTickMs:measurement})).max(20)});
|
||||
async function ensure(){await env.DB!.batch([
|
||||
|
||||
+3
-1
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
import {useEffect,useState} from 'react';
|
||||
import {Activity,CheckCircle2,AlertTriangle,ArrowRight,Clock3,Search,ShieldCheck,Package,ScrollText,Users,Timer} from 'lucide-react';
|
||||
import {formatCopper} from '@/core/realm-outcomes';
|
||||
import type {Attention,Perf} from '@/core/realm-outcomes';
|
||||
import type {Evidence} from '@/core/realm-insights';
|
||||
type Recovery={name:string;at:number;status:string;result:string;detail:string;checkedAt:number|null};
|
||||
@@ -21,7 +22,7 @@ export function RealmDiagnostics({page,onNavigate,initialQuery=''}:{page:string;
|
||||
const attempts=recoveries.filter(r=>r.name.toLowerCase().includes(query.toLowerCase())&&(filter==='all'||r.status===filter));
|
||||
return <div className="diagnostics"><div className="diag-hero"><div><div className="eyebrow">EVIDENCE FROM YOUR REALM</div><h2>{page==='Needs attention'?'A clear view of what needs you.':page==='Gameplay Results'?'Plans become progress.':page==='Recovery Results'?'Did the recovery help?':'Inside the bot engine.'}</h2><p>{page==='Needs attention'?'Prioritized signals, with a direct path to the details.':page==='Gameplay Results'?'Native quest, loot, and group observations from the last 24 hours.':page==='Recovery Results'?'Native follow-up separates an attempted fix from observed progress.':'Measured execution time, sampled every five minutes.'}</p></div><span className={'diag-status '+(stale?'warning':'good')}><span/>{stale?'Collection delayed':'Collector active'}</span></div><p className="diag-updated"><Clock3 size={14}/> Last collection {stamp(data.collectedAt)} · View refreshes every 15 seconds</p>{error&&<div className="notice" role="alert">{error}</div>}
|
||||
{page==='Needs attention'&&<><div className="diag-metrics"><Metric label="Needs attention" value={data.attention.length} note="Current signals, not historical totals" icon={AlertTriangle} tone={data.attention.length?'warning':'good'}/><Metric label="Critical" value={critical} note="Resource pressure or population over cap" icon={ShieldCheck} tone={critical?'critical':''}/><Metric label="Online residents" value={o.bots.length} note="Target: 50 · 25 per faction" icon={Users}/><Metric label="Observed quest rewards" value={metrics.rewarded??0} note="New rewarded quest records · 24 hours" icon={ScrollText}/></div><div className="diag-section-title"><h2>Follow-up queue</h2><span className="fineprint">Critical signals first</span></div>{data.attention.length?<div className="diag-alerts">{data.attention.map(a=><button className={'diag-alert '+a.severity} key={a.key} onClick={()=>onNavigate(a.target,a.name)}><AlertTriangle size={20}/><div><span className="diag-label">{a.severity}</span><h3>{a.title}</h3><p>{a.detail}</p><small>Open {a.target}</small></div><ArrowRight size={18}/></button>)}</div>:<div className="diag-clear"><CheckCircle2 size={36}/><h3>No current attention signals</h3><p>The monitored checks are clear. This is not proof that every gameplay path is bug-free.</p></div>}<div className="diag-links">{['Gameplay Results','Recovery Results','Bot Performance'].map(p=><button key={p} onClick={()=>onNavigate(p)}>{p}<ArrowRight size={16}/></button>)}</div></>}
|
||||
{page==='Gameplay Results'&&<><div className="diag-metrics"><Metric label="Quest rewards" value={metrics.rewarded??0} note="New rewarded quest records" icon={ScrollText}/><Metric label="Items looted" value={metrics.lootItems??0} note={`${metrics.lootEvents??0} native loot collections`} icon={Package}/><Metric label="Shared-target kills" value={metrics.partyKills??0} note="Killer had a nearby bot fighting the same target" icon={Users}/><Metric label="Quests abandoned" value={metrics.abandoned??0} note="An abandonment is not automatically a bug" icon={Activity}/></div><div className="diag-note">Evidence since {stamp(o.startedAt)}. Native counters last collected {stamp(o.at)}. Quest rewards count newly recorded quest IDs; repeated rewards for an already recorded quest may not increase this count. Quest objectives completed: <strong>{metrics.quests??0}</strong>. Party actions rejected: <strong>{metrics.partyFailed??0}</strong>. Gaps or a server reload can miss unflushed events.</div><div className="diag-toolbar"><label className="diag-search"><Search size={17}/><input aria-label="Search gameplay results" placeholder="Find a resident or zone…" value={query} onChange={e=>setQuery(e.target.value)}/></label><select aria-label="Filter gameplay results" value={filter} onChange={e=>setFilter(e.target.value)}><option value="all">All online residents</option><option value="grouped">Currently grouped</option></select></div><div className="diag-bot-grid">{visibleBots.map(b=><section className="diag-bot" key={b.name}><div className="diag-bot-head"><span className="diag-avatar">{b.name[0]}</span><div><h3>{b.name}</h3><small>{b.zone}</small></div><span className={'diag-chip '+(b.grouped?'good':'')}>{b.grouped?'Grouped':'Solo / unknown'}</span></div><div className="diag-bot-values"><div><strong>{b.totals.rewarded}</strong><small>Quest rewards</small></div><div><strong>{b.totals.lootItems}</strong><small>Items looted</small></div><div><strong>{b.totals.partyKills}</strong><small>Shared kills</small></div></div><details><summary>Evidence details</summary><p>Objectives completed: {b.totals.quests} · Abandoned: {b.totals.abandoned} · Loot collections: {b.totals.lootEvents}</p><p>Observed bot companions: {b.groupBots.filter(n=>n!==b.name).join(', ')||'None reported'}</p><button className="diag-text-link" onClick={()=>onNavigate('Bot Activity',b.name)}>View activity <ArrowRight size={14}/></button></details></section>)}</div>{!visibleBots.length&&<div className="empty-note">No residents match your filter.</div>}<details className="diag-timeline"><summary>Recent evidence ({o.recent.length})</summary>{o.recent.map(e=><div key={e.id}><small>{stamp(e.at)}</small><strong>{e.name}</strong><p>{e.detail} · +{e.amount}</p></div>)}</details></>}
|
||||
{page==='Gameplay Results'&&<><div className="diag-metrics"><Metric label="Quest rewards" value={metrics.rewarded??0} note="New rewarded quest records" icon={ScrollText}/><Metric label="Items looted" value={metrics.lootItems??0} note={`${metrics.lootEvents??0} native loot collections`} icon={Package}/><Metric label="Currency looted" value={formatCopper(metrics.lootedCopper??0)} note="Looted coins only · excludes quest rewards and sales" icon={Package}/><Metric label="Shared-target kills" value={metrics.partyKills??0} note="Killer had a nearby bot fighting the same target" icon={Users}/><Metric label="Quests abandoned" value={metrics.abandoned??0} note="An abandonment is not automatically a bug" icon={Activity}/></div><div className="diag-note">Evidence since {stamp(o.startedAt)}. Native counters last collected {stamp(o.at)}. Quest rewards count newly recorded quest IDs; repeated rewards for an already recorded quest may not increase this count. Quest objectives completed: <strong>{metrics.quests??0}</strong>. Party actions rejected: <strong>{metrics.partyFailed??0}</strong>. Currency tracking starts with this update; historical coin loot is unavailable. Gaps or a server reload can miss unflushed events.</div><div className="diag-toolbar"><label className="diag-search"><Search size={17}/><input aria-label="Search gameplay results" placeholder="Find a resident or zone…" value={query} onChange={e=>setQuery(e.target.value)}/></label><select aria-label="Filter gameplay results" value={filter} onChange={e=>setFilter(e.target.value)}><option value="all">All online residents</option><option value="grouped">Currently grouped</option></select></div><div className="diag-bot-grid">{visibleBots.map(b=><section className="diag-bot" key={b.name}><div className="diag-bot-head"><span className="diag-avatar">{b.name[0]}</span><div><h3>{b.name}</h3><small>{b.zone}</small></div><span className={'diag-chip '+(b.grouped?'good':'')}>{b.grouped?'Grouped':'Solo / unknown'}</span></div><div className="diag-bot-values"><div><strong>{b.totals.rewarded}</strong><small>Quest rewards</small></div><div><strong>{b.totals.lootItems}</strong><small>Items looted</small></div><div><strong>{formatCopper(b.totals.lootedCopper??0)}</strong><small>Currency looted</small></div><div><strong>{b.totals.partyKills}</strong><small>Shared kills</small></div></div><details><summary>Evidence details</summary><p>Objectives completed: {b.totals.quests} · Abandoned: {b.totals.abandoned} · Loot collections: {b.totals.lootEvents}</p><p>Observed bot companions: {b.groupBots.filter(n=>n!==b.name).join(', ')||'None reported'}</p><button className="diag-text-link" onClick={()=>onNavigate('Bot Activity',b.name)}>View activity <ArrowRight size={14}/></button></details></section>)}</div>{!visibleBots.length&&<div className="empty-note">No residents match your filter.</div>}<details className="diag-timeline"><summary>Recent evidence ({o.recent.length})</summary>{o.recent.map(e=><div key={e.id}><small>{stamp(e.at)}</small><strong>{e.name}</strong><p>{e.detail} · +{e.kind==='lootedCopper'?formatCopper(e.amount):e.amount}</p></div>)}</details></>}
|
||||
{page==='Recovery Results'&&<><div className="diag-metrics">{['resumed','no_progress','pending','unverified'].map(status=><Metric key={status} label={labels[status]} value={recoveries.filter(r=>r.status===status).length} note="Among the latest 100 retained attempts" tone={status==='resumed'?'good':status==='no_progress'?'warning':''} icon={status==='resumed'?CheckCircle2:Clock3}/>)}</div><p className="diag-note">Progress resumed means native movement over five yards, a map change, or a level change was observed within the follow-up window. It does not prove lasting recovery or that the intervention caused it. Three minutes without that evidence is flagged for review. Restarts or unavailable residents may prevent verification.</p><div className="diag-toolbar"><label className="diag-search"><Search size={17}/><input aria-label="Search recovery results" placeholder="Find a resident…" value={query} onChange={e=>setQuery(e.target.value)}/></label><select aria-label="Filter recovery results" value={filter} onChange={e=>setFilter(e.target.value)}><option value="all">All outcomes</option>{Object.entries(labels).map(([k,v])=><option key={k} value={k}>{v}</option>)}</select></div><div className="diag-alerts">{attempts.map((r,i)=><article className="diag-recovery" key={r.at+':'+r.name+':'+i}><div><h3>{r.name}</h3><small>{stamp(r.at)}</small></div><span className={'diag-chip '+(r.status==='resumed'?'good':r.status==='no_progress'?'warning':'')}>{labels[r.status]}</span><p>{r.result}</p><details><summary>Attempt details</summary><p>{r.detail}</p><small>Checked: {stamp(r.checkedAt)}</small></details></article>)}</div>{!attempts.length&&<div className="diag-clear"><ShieldCheck size={36}/><h3>No matching recovery attempts</h3><p>Results appear after the native guard attempts a recovery. No intervention is forced to populate this view.</p></div>}</>}
|
||||
{page==='Bot Performance'&&<><div className="diag-metrics"><Metric label="Average full tick" value={perf?perf.averageTickMs.toFixed(2)+' ms':'—'} note="Cumulative mean since profiler start/reset" icon={Timer}/><Metric label="Measured full ticks" value={perf?Math.round(perf.ticks).toLocaleString():'—'} note="Bot update cycles, not world-server ticks"/><Metric label="Profiler" value={perf?.enabled?'Enabled':'Unavailable'} note={stamp(perf?.at??null)} tone={perf?.enabled?'good':'warning'}/><Metric label="Stored samples" value={data.performance.history.length} note="Up to 144 snapshots retained" icon={Clock3}/></div>{(!perf||Date.now()/1000-perf.at>720)&&<div className="notice">Profiler data is missing or stale. Measurements below are not current.</div>}<section className="panel"><div className="panel-head"><h2>Average full-tick history</h2><small>Milliseconds · cumulative average</small></div><div className="panel-body"><div className="diag-bars" role="img" aria-label="History of cumulative average bot tick duration">{data.performance.history.slice(-48).map(p=><div key={p.at} title={`${stamp(p.at)}: ${p.averageTickMs.toFixed(3)} ms`} style={{height:Math.max(3,100*p.averageTickMs/Math.max(.001,...data.performance.history.slice(-48).map(x=>x.averageTickMs)))+'%'}}/>)}</div><p className="fineprint">{stamp(data.performance.history[0]?.at??null)} — {stamp(perf?.at??null)}. Timers can overlap; they must not be added together or interpreted as CPU utilization.</p></div></section><section className="panel" style={{marginTop:20}}><div className="panel-head"><h2>Most expensive measured operations</h2><small>Sorted by accumulated time</small></div><div className="diag-table-scroll"><table className="diag-table"><thead><tr><th>Operation</th><th>Calls</th><th>Mean</th><th>Maximum</th><th>Per full tick</th></tr></thead><tbody>{perf?.rows.map((r,i)=><tr key={i}><td><strong>{r.name}</strong><small>{r.category}</small></td><td>{Math.round(r.calls).toLocaleString()}</td><td>{r.averageMs.toFixed(3)} ms</td><td>{r.maxMs.toFixed(3)} ms</td><td>{r.perTickMs.toFixed(3)} ms</td></tr>)}</tbody></table></div>{!perf?.rows.length&&<p className="empty-note">Waiting for the first native profiler export.</p>}</section></>}
|
||||
</div>;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type {State} from './types';
|
||||
import {activityReason,type Evidence} from './realm-insights';
|
||||
export const counterKeys=['quests','abandoned','lootEvents','lootItems','partyKills','rewarded'] as const;
|
||||
export const counterKeys=['quests','abandoned','lootEvents','lootItems','partyKills','rewarded','lootedCopper'] as const;
|
||||
export type NativeOutcome={guid:number;name:string}&Record<typeof counterKeys[number],number>;
|
||||
export type OutcomeBaseline={at:number;startedAt:number;rows:NativeOutcome[]};
|
||||
export function outcomeChanges(rows:NativeOutcome[],previous:OutcomeBaseline|undefined,now:number){
|
||||
const events:Evidence[]=[];
|
||||
for(const r of rows){const old=previous?.rows.find(b=>b.guid===r.guid);if(!old)continue;
|
||||
for(const key of counterKeys){const amount=r[key]-old[key];if(amount>0)events.push({id:`outcome:${r.guid}:${key}:${now}:${r[key]}`,at:now,kind:key,name:r.name,amount,detail:({quests:'Quest objectives completed',abandoned:'Quests abandoned',lootEvents:'Loot collections',lootItems:'Items collected',partyKills:'Kills with a nearby bot attacking the same target',rewarded:'New rewarded quest records'})[key]});}
|
||||
for(const key of counterKeys){const amount=r[key]-old[key];if(amount>0)events.push({id:`outcome:${r.guid}:${key}:${now}:${r[key]}`,at:now,kind:key,name:r.name,amount,detail:({lootedCopper:'Currency looted',quests:'Quest objectives completed',abandoned:'Quests abandoned',lootEvents:'Loot collections',lootItems:'Items collected',partyKills:'Kills with a nearby bot attacking the same target',rewarded:'New rewarded quest records'})[key]});}
|
||||
}
|
||||
return {baseline:{at:now,startedAt:previous?.startedAt??now,rows},events};
|
||||
}
|
||||
@@ -35,3 +35,5 @@ export function attentionItems(s:State,now:number,collectedAt:number|null,outcom
|
||||
for(const r of latest.values())if(r.status==='no_progress'&&now-r.at<3600&&online.some(b=>b.autonomy?.characterGuid===r.guid&&activityReason(b,now).label==='Possible stall'))add('recovery:'+r.id,'warning',r.name+' · recovery needs follow-up','No native movement or level progress was observed within three minutes.','Recovery Results',r.name);
|
||||
return items.sort((a,b)=>Number(b.severity==='critical')-Number(a.severity==='critical'));
|
||||
}
|
||||
|
||||
export function formatCopper(amount:number){const n=Math.max(0,Math.floor(Number.isFinite(amount)?amount:0));return `${Math.floor(n/10000)}g ${Math.floor(n%10000/100)}s ${n%100}c`;}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// Bound each SQL value to at most 512 KiB of UTF-8; preserve all resident history.
|
||||
export function splitState(state:unknown){const text=JSON.stringify(state),chunks:string[]=[];for(let i=0;i<text.length;){let end=Math.min(i+120000,text.length);const c=text.charCodeAt(end-1);if(c>=0xd800&&c<=0xdbff)end--;chunks.push(text.slice(i,end));i=end;}return chunks;}
|
||||
+15
-6
@@ -1,4 +1,5 @@
|
||||
import { env } from 'cloudflare:workers';
|
||||
import { splitState } from './state-chunks';
|
||||
import { seed } from './seed';
|
||||
import type { State,StateStore,Bot,Recipe,Home } from './types';
|
||||
|
||||
@@ -68,13 +69,21 @@ export class D1Store implements StateStore {
|
||||
if(!env.DB) throw new Error('Realm database unavailable');
|
||||
const initial=seed();
|
||||
await env.DB.prepare('INSERT OR IGNORE INTO realm_state (id,revision,payload) VALUES (?,?,?)').bind('realm',0,JSON.stringify(initial)).run();
|
||||
const row=await env.DB.prepare('SELECT payload FROM realm_state WHERE id=?').bind('realm').first<{payload:string}>();
|
||||
if(!row)throw new Error('Realm state unavailable');
|
||||
return migrate(JSON.parse(row.payload));
|
||||
await this.chunks();
|
||||
const result=await env.DB.prepare("SELECT s.payload AS manifest,c.payload AS chunk FROM realm_state s LEFT JOIN realm_state_chunks c ON c.token=json_extract(s.payload,'$.chunkToken') WHERE s.id=? ORDER BY c.part").bind('realm').all<{manifest:string;chunk:string|null}>();
|
||||
const rows=result.results;if(!rows.length)throw new Error('Realm state unavailable');
|
||||
const manifest=JSON.parse(rows[0].manifest);
|
||||
if(manifest.chunkToken){if(rows.length!==manifest.parts||rows.some(r=>r.chunk===null))throw new Error('Realm state chunks incomplete');return migrate(JSON.parse(rows.map(r=>r.chunk).join('')));}
|
||||
return migrate(manifest);
|
||||
}
|
||||
private async chunks(){await env.DB!.prepare('CREATE TABLE IF NOT EXISTS realm_state_chunks (token TEXT,part INTEGER,payload TEXT,PRIMARY KEY(token,part))').run();}
|
||||
async save(s:State,expected:number){
|
||||
s.revision=expected+1;
|
||||
const result=await env.DB!.prepare('UPDATE realm_state SET revision=?,payload=? WHERE id=? AND revision=?').bind(s.revision,JSON.stringify(s),'realm',expected).run();
|
||||
if(result.meta.changes!==1)throw new Error('State changed in another request. Refresh and try again.');
|
||||
await this.chunks();s.revision=expected+1;
|
||||
const token=crypto.randomUUID(),parts=splitState(s),manifest=JSON.stringify({chunkToken:token,parts:parts.length});
|
||||
const statements=[env.DB!.prepare('UPDATE realm_state SET revision=?,payload=? WHERE id=? AND revision=?').bind(s.revision,manifest,'realm',expected),
|
||||
...parts.map((part,i)=>env.DB!.prepare('INSERT INTO realm_state_chunks(token,part,payload) SELECT ?,?,? WHERE EXISTS (SELECT 1 FROM realm_state WHERE id=? AND payload=?)').bind(token,i,part,'realm',manifest)),
|
||||
env.DB!.prepare('DELETE FROM realm_state_chunks WHERE token<>? AND EXISTS (SELECT 1 FROM realm_state WHERE id=? AND payload=?)').bind(token,'realm',manifest)];
|
||||
const result=await env.DB!.batch(statements);
|
||||
if(result[0].meta.changes!==1)throw new Error('State changed in another request. Refresh and try again.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ at most 100 memories and 100 relationship records on this path. Completed and
|
||||
failed conversation requests are pruned after 24 hours; character memorials and
|
||||
other game/event history have separate retention policies.
|
||||
|
||||
Memories and relationships are stored in `realm_state.payload` in the application's
|
||||
Memories and relationships are stored in `realm_state_chunks` (with an atomic revision manifest in `realm_state.payload`) in the application's
|
||||
D1 SQLite database under `.wrangler/state/v3/d1` in the Ubuntu deployment. This
|
||||
uses the VM's disk, backed by the host PC's storage. The per-resident memory limit
|
||||
is not a total disk quota: additional identities and other logs can still grow.
|
||||
|
||||
@@ -21,3 +21,5 @@ Recovery Results retains up to 100 attempts. Native follow-up checks position, m
|
||||
Bot Performance reads the native `playerbots pmon tick` JSON export every five minutes. Enable `AiPlayerbot.PerfMonEnabled = 1` in the installed module configuration; on an already-running server, toggle the native profiler once only if it is currently disabled. Deploy `ale/azerothai_outcomes.lua` alongside the existing bridge and recovery scripts. The collector reads only fixed container paths. Timings are converted from microseconds to milliseconds. Operation timings overlap; they are not CPU percentages. Cumulative means are explicitly labeled, and only the top 20 operations and 144 snapshots are retained in D1. No new chat/model calls are made by diagnostics.
|
||||
|
||||
Visitor access remains read-only. The same components render both monitors; diagnostics expose aggregate evidence and managed character names, excluding raw database identities, credentials, command sources and private chat. Native counter/result tables are included in the existing full auth database backup, and all D1 diagnostic tables are covered by the existing D1 backup.
|
||||
|
||||
Currency looted is tracked independently through native player event 37, in copper, and displayed as gold/silver/copper. Item event 32 remains item quantities only. Coin totals exclude quest rewards, sales, trades, and existing wallet balances; no historical currency is inferred. The first sample establishes a baseline, then the last-24-hour view reports observed gains. Both admin and visitor use the same read-only measurements.
|
||||
|
||||
@@ -109,7 +109,12 @@ def backup():
|
||||
assert db.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table'").fetchone()[0]>0
|
||||
if db.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='realm_state'").fetchone():
|
||||
row=db.execute("SELECT payload FROM realm_state WHERE id='realm'").fetchone()
|
||||
if row:restored_state=json.loads(row[0])
|
||||
if row:
|
||||
restored_state=json.loads(row[0])
|
||||
if restored_state.get('chunkToken'):
|
||||
chunks=db.execute('SELECT payload FROM realm_state_chunks WHERE token=? ORDER BY part',(restored_state['chunkToken'],)).fetchall()
|
||||
assert len(chunks)==restored_state['parts'], 'Incomplete restored state'
|
||||
restored_state=json.loads(''.join(r[0] for r in chunks))
|
||||
restored_file.unlink()
|
||||
if not restored_state or not restored_state.get('bots'):raise RuntimeError('Restored app realm state missing')
|
||||
manifest['restoredMemories']=sum(len(b.get('memories',[])) for b in restored_state['bots'])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Read native counters and profiler output; return bounded public measurements."""
|
||||
import json,subprocess,time,math
|
||||
COUNTERS=('quests','abandoned','lootEvents','lootItems','partyKills','rewarded')
|
||||
COUNTERS=('quests','abandoned','lootEvents','lootItems','partyKills','rewarded','lootedCopper')
|
||||
def normalize_performance(raw):
|
||||
def number(v):
|
||||
try:
|
||||
@@ -27,9 +27,10 @@ def collect(sql,state):
|
||||
guids=','.join(map(str,allowed))
|
||||
for line in sql('SELECT guid,COUNT(*) FROM acore_characters.character_queststatus_rewarded WHERE guid IN ('+guids+') GROUP BY guid').splitlines():
|
||||
guid,count=map(int,line.split('\t'));rewarded[guid]=count
|
||||
money={int(g):int(c) for g,c in (line.split('\t') for line in sql('SELECT guid,looted_copper FROM acore_auth.azerothai_currency').splitlines())}
|
||||
result=[]
|
||||
for guid,b in allowed.items():
|
||||
counters=rows.get(guid,dict.fromkeys(COUNTERS,0));counters['rewarded']=rewarded.get(guid,0)
|
||||
counters=rows.get(guid,dict.fromkeys(COUNTERS,0));counters['rewarded']=rewarded.get(guid,0);counters['lootedCopper']=money.get(guid,0)
|
||||
result.append({'guid':guid,'name':b['name'],**counters})
|
||||
return result
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {spawnSync} from 'node:child_process';
|
||||
import {writeFileSync,mkdirSync} from 'node:fs';
|
||||
const result=spawnSync(process.execPath,['node_modules/typescript/bin/tsc','core/realm-outcomes.ts','core/realm-insights.ts','core/service.ts','core/seed.ts','core/ale.ts','core/conversation.ts','core/autonomy.ts','--types','@cloudflare/workers-types,node','--outDir','.test-build','--module','commonjs','--target','es2022','--moduleResolution','node','--esModuleInterop','--skipLibCheck','--strict'],{stdio:'inherit'});
|
||||
const result=spawnSync(process.execPath,['node_modules/typescript/bin/tsc','core/state-chunks.ts','core/realm-outcomes.ts','core/realm-insights.ts','core/service.ts','core/seed.ts','core/ale.ts','core/conversation.ts','core/autonomy.ts','--types','@cloudflare/workers-types,node','--outDir','.test-build','--module','commonjs','--target','es2022','--moduleResolution','node','--esModuleInterop','--skipLibCheck','--strict'],{stdio:'inherit'});
|
||||
if(result.status)process.exit(result.status);
|
||||
mkdirSync('.test-build',{recursive:true});writeFileSync('.test-build/package.json','{"type":"commonjs"}');
|
||||
process.exit(spawnSync(process.execPath,['--test','tests/realm-outcomes.test.cjs','tests/realm-insights.test.cjs','tests/core.test.cjs','tests/hardcore.test.cjs','tests/living-world.test.cjs','tests/ale.test.cjs','tests/brains.test.cjs','tests/conversation.test.cjs','tests/autonomy.test.cjs','tests/community.test.cjs'],{stdio:'inherit'}).status??1);
|
||||
|
||||
@@ -4,3 +4,7 @@ const row={guid:1,name:'A',quests:10,abandoned:1,lootEvents:4,lootItems:10,party
|
||||
test('outcome baseline excludes history, reports native deltas, handles resets without invented gains',()=>{const first=outcomeChanges([row],undefined,100);assert.equal(first.events.length,0);const next=outcomeChanges([{...row,lootItems:14,rewarded:9}],first.baseline,160);assert.deepEqual(next.events.map(e=>[e.kind,e.amount]),[['lootItems',4],['rewarded',1]]);assert.equal(outcomeChanges(next.baseline.rows,next.baseline,220).events.length,0);assert.equal(outcomeChanges([{...row,lootItems:0}],next.baseline,280).events.length,0);});
|
||||
test('attention skips false population alarms when roster is stale and prioritizes resource pressure',()=>{const s={bots:[],aleRosterAt:0};const h=[{host:'ubuntu',at:1000,memoryAvailableMB:100,memoryTotalMB:8000,diskFreeGB:20,cpuPercent:10},{host:'windows',at:1000,memoryAvailableMB:2000,memoryTotalMB:16000,diskFreeGB:20,cpuPercent:10}];const a=attentionItems(s,1000,1000,1000,{at:1000,enabled:true},[],h);assert.equal(a[0].key,'memory:ubuntu');assert.equal(a.some(x=>x.key==='population'),false);});
|
||||
test('resolved old recovery failures do not remain actionable',()=>{const b={id:'a',name:'A',status:'online',autonomy:{managed:true,characterGuid:1},world:{at:1000,stationarySince:990},lastDecision:{action:'quest'}};const a=attentionItems({bots:[b]},1000,1000,1000,null,[{id:1,guid:1,name:'A',at:900,status:'no_progress'}],[]);assert.equal(a.some(x=>x.key.startsWith('recovery:')),false);});
|
||||
|
||||
test('currency formats copper and remains independent of item counts and historical baselines',()=>{const {formatCopper}=require('../.test-build/realm-outcomes.js');assert.equal(formatCopper(12345),'1g 23s 45c');assert.equal(formatCopper(0),'0g 0s 0c');const first=outcomeChanges([{...row,lootedCopper:100}],{at:1,startedAt:1,rows:[row]},2);assert.equal(first.events.length,0);const next=outcomeChanges([{...row,lootedCopper:250}],first.baseline,3);assert.deepEqual(next.events.map(e=>[e.kind,e.amount]),[['lootedCopper',150]]);});
|
||||
|
||||
test('large state chunks preserve Unicode and all history within per-value limits',()=>{const {splitState}=require('../.test-build/state-chunks.js');const input={history:'🎮'.repeat(600000)};const chunks=splitState(input);assert.ok(chunks.length>1);assert.deepEqual(JSON.parse(chunks.join('')),input);for(const c of chunks)assert.ok(Buffer.byteLength(c)<512*1024);});
|
||||
|
||||
Reference in New Issue
Block a user