Skip to content

Revert the import of OpenCMR2 into port/ #51

Revert the import of OpenCMR2 into port/

Revert the import of OpenCMR2 into port/ #51

name: Build + Update reccmp Report
on:
pull_request:
branches:
- main
push:
branches:
- main
workflow_dispatch:
concurrency:
group: ${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
fetch-deps:
name: Download original binary
uses: ./.github/workflows/cmr2bin.yml
build:
name: Build (MSVC 6.0)
needs: [fetch-deps]
runs-on: "windows-latest"
steps:
- uses: actions/checkout@main
with:
submodules: recursive
- name: Fetch MSVC 6.0
uses: actions/checkout@main
with:
repository: itsmattkc/msvc600
ref: 001c4bafdcf2ef4b474d693acccd35a91e848f40
path: msvc600
# The original was built with the VC6 SP3 compiler passes (Rich header build 8447)
- name: Cache VC6 SP3 compiler passes
uses: actions/cache@v4
with:
path: vc6sp3
key: vc6sp3-v1
- name: Install VC6 SP3 compiler passes
shell: cmd
run: |
python scripts/fetch_vc6sp3.py --cache vc6sp3 --msvc-root msvc600/VC98
- name: Build
shell: cmd
run: |
python scripts/build.py --msvc-root msvc600/VC98
- name: Upload Build
uses: actions/upload-artifact@main
with:
name: Win32
path: |
build/CMR2.exe
build/CMR2.pdb
build/manifest.json
verify:
name: Verify decomp
needs: [build, fetch-deps]
runs-on: windows-latest
steps:
- uses: actions/checkout@main
with:
ref: ${{ github.head_ref }}
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: actions/download-artifact@main
with:
name: Win32
path: build
- name: Restore cached original binary
id: cache-original-binary
uses: actions/cache/restore@v4
with:
enableCrossOsArchive: true
path: cmr2bin
key: cmr2bin
- name: Install reccmp
shell: bash
run: |
pip install -r scripts/requirements.txt
- name: Detect binary
run: |
reccmp-project detect --what original --search-path cmr2bin
reccmp-project detect --what recompiled --search-path build
- name: Compare Accuracy to main
shell: bash
env:
RELEASE_URL: https://raw.githubusercontent.com/${{ github.repository }}/refs/heads/main/CMR2PROGRESS/
run: |
# Download the current main state
curl -fLSs -o summary-old.json $RELEASE_URL/summary.json || echo "" >summary-old.json
# Compare with current main
python scripts/reccmp_compat.py reccmp --target CMR2 --html index.html --json CMR2PROGRESS/summary.json --diff summary-old.json || echo "Current master not found"
# decomp.dev picks up any "<version>_report" artifact holding an objdiff report.json
- name: Generate objdiff report
shell: bash
run: |
python scripts/objdiff_report.py --units function -o build/report.json
- name: Upload objdiff report
uses: actions/upload-artifact@v4
with:
name: CMR2_report
path: build/report.json
- name: Upload HTML accuracy report
uses: actions/upload-artifact@v4
with:
name: accuracy-html
path: index.html
- name: Test Exports
shell: bash
run: |
python scripts/reccmp_compat.py verexp --target CMR2
# Don't have any vtables yet
# - name: Check Vtables
# shell: bash
# run: |
# reccmp-vtable --target CMR2
- name: Check Variables
shell: bash
run: |
python scripts/reccmp_compat.py datacmp --target CMR2
- name: Upsert PR Accuracy Report
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const TAG = '<!-- CMR2-ACCURACY-REPORT -->';
const STUB_THRESHOLD = 0.001;
const LOW_ACCURACY_THRESHOLD = 80;
function load(path) {
if (!fs.existsSync(path)) return null;
return JSON.parse(fs.readFileSync(path, 'utf8'));
}
const currentRaw = load('CMR2PROGRESS/summary.json');
if (!currentRaw) {
core.setFailed('summary.json not found');
return;
}
const previousRaw = load('summary-old.json');
const current = currentRaw?.data || [];
const previous = previousRaw?.data || [];
if (!current.length) return;
const currMap = new Map(current.map(f => [f.address, f]));
const prevMap = new Map((previous || []).map(f => [f.address, f]));
const increased = [];
const decreased = [];
const added = [];
const gone = [];
const stubbed = [];
const lowAccuracy = [];
const newlyPerfect = [];
let total = current.length;
let matched = 0;
let fullMatch = 0;
for (const fn of current) {
matched += fn.matching;
if (!fn.diff || fn.diff.length === 0) fullMatch++;
const currPct = fn.matching * 100;
const prev = prevMap.get(fn.address);
const prevPct = prev ? prev.matching * 100 : null;
const isStub = fn.matching <= STUB_THRESHOLD;
const isPerfect = currPct === 100;
// ➕ Added (record early so it doesn't get skipped)
if (!prev) {
added.push(fn);
if (isStub) stubbed.push({ fn, reason: 'new stub' });
}
// 🎉 Newly perfect (brand-new OR improved to 100%)
if (isPerfect && !isStub && (prevPct === null || prevPct < 100)) {
newlyPerfect.push({
fn,
prevPct
});
}
// 🚨 Below 80% (exclude stubs, include new functions)
if (!isStub && currPct < LOW_ACCURACY_THRESHOLD) {
lowAccuracy.push({
fn,
currPct,
delta: prevPct !== null ? currPct - prevPct : null
});
}
// Stop here for brand-new functions (no deltas possible)
if (!prev) continue;
// Δ comparisons
if (currPct > prevPct + 0.01) {
increased.push({ fn, prevPct, currPct });
} else if (currPct < prevPct - 0.01) {
decreased.push({ fn, prevPct, currPct });
}
// 🧱 Regressed to stub
if (prev.matching > STUB_THRESHOLD && isStub) {
stubbed.push({ fn, reason: 'regressed to stub' });
}
}
// ❌ Gone
for (const [addr, fn] of prevMap) {
if (!currMap.has(addr)) gone.push(fn);
}
const effective = ((matched / total) * 100).toFixed(2);
const fullMatchPct = ((fullMatch / total) * 100).toFixed(2);
function formatName(fn) {
return fn.matching * 100 === 100 ? `🏆 ${fn.name}` : fn.name;
}
function formatAccuracy(pct) {
return pct === 100 ? '**100.00% ✅**' : `${pct.toFixed(2)}%`;
}
function deltaTable(rows) {
if (!rows.length) return '_None_';
return [
'| Address | Function | Before | After |',
'|--------:|----------|--------|-------|',
...rows.map(r =>
`| \`${r.fn.address}\` | ${formatName(r.fn)} | ${r.prevPct.toFixed(2)}% | ${formatAccuracy(r.currPct)} |`
)
].join('\n');
}
function simpleTable(rows) {
if (!rows.length) return '_None_';
return [
'| Address | Function | Accuracy |',
'|--------:|----------|----------|',
...rows.map(f =>
`| \`${f.address}\` | ${formatName(f)} | ${formatAccuracy(f.matching * 100)} |`
)
].join('\n');
}
function newlyPerfectTable(rows) {
if (!rows.length) return '_None_';
return [
'| Address | Function | Before | Now |',
'|--------:|----------|--------|-----|',
...rows.map(r => {
const before = r.prevPct === null ? '—' : `${r.prevPct.toFixed(2)}%`;
return `| \`${r.fn.address}\` | 🏆 ${r.fn.name} | ${before} | **100.00% ✅** |`;
})
].join('\n');
}
function lowAccuracyTable(rows) {
if (!rows.length) return '_None_';
return [
'| Address | Function | Accuracy | Δ Since Last PR |',
'|--------:|----------|----------|----------------|',
...rows
.sort((a, b) => a.currPct - b.currPct)
.map(r => {
const delta =
r.delta === null
? '—'
: `${r.delta > 0 ? '+' : ''}${r.delta.toFixed(2)}%`;
return `| \`${r.fn.address}\` | ${r.fn.name} | **${r.currPct.toFixed(2)}%** | ${delta} |`;
})
].join('\n');
}
const goneTable = gone.length
? [
'| Address | Function | Previous Accuracy |',
'|--------:|----------|-------------------|',
...gone.map(f =>
`| \`${f.address}\` | ${f.name} | ${(f.matching * 100).toFixed(2)}% |`
)
].join('\n')
: '_None_';
const stubTable = stubbed.length
? [
'| Address | Function | Reason |',
'|--------:|----------|--------|',
...stubbed.map(s =>
`| \`${s.fn.address}\` | ${s.fn.name} | ${s.reason} |`
)
].join('\n')
: '_None_';
const timestamp = new Date().toISOString().replace('T', ' ').replace(/\..+/, ' UTC');
const body = [
TAG,
'',
'## 📊 CMR2 Accuracy Report',
'',
`**Functions:** ${total} `,
`**Effective Accuracy:** **${effective}%** `,
`**Fully Matching:** ${fullMatch} (${fullMatchPct}%)`,
'',
'---',
'',
'### 🎉 Newly Perfect Since Last PR',
newlyPerfectTable(newlyPerfect),
'',
'---',
'',
'### 🚨 Functions Below 80% Accuracy (Non-Stubs)',
'_Note: Missing data may be causing low accuracy on some of these_',
lowAccuracyTable(lowAccuracy),
'',
'---',
'',
'### ⚠️ Decreased',
deltaTable(decreased),
'',
'---',
'',
'### ✅ Increased',
deltaTable(increased),
'',
'---',
'',
'### ➕ Added',
simpleTable(added),
'',
'---',
'',
'### 🧱 Stubbed',
stubTable,
'',
'---',
'',
'### ❌ Gone',
goneTable,
'',
`_Last updated by CMR2Decomp Bot on ${timestamp}_`
].join('\n');
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const comments = await github.paginate(
github.rest.issues.listComments,
{ owner, repo, issue_number }
);
const existing = comments.find(c => c.body.includes(TAG));
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number, body });
}