Understand a Save Format With Hex Bytes and Controlled Changes
Decode an original four-byte record, identify endianness and validate a proposed schema.

Thumbnail: an actual related playable BINX demo. It is a gameplay reference, not evidence that a save, inventory, network or native reverse-engineering integration has been demonstrated.
Difficulty: beginner to intermediate. Prerequisites: basic variables, functions and a test copy of your project. Reference: Forge workshop 1.0, browser ES modules and Node 24 checks. Engine-specific translation is a separate integration.
What you will learn
Decode an original four-byte record, identify endianness and validate a proposed schema.
Download complete MIT workshop- Extract into a new folder and run
node check.mjsfrom that folder. - For the browser games, run
python3 -m http.server 8080with Python 3 and openhttp://localhost:8080/index.html. Do not open modules with file://. - Read the full setup instructions and the original code licence. Keep your existing game on a separate test branch.
The JavaScript behaviour checks and native C comparison can run independently of the browser. Ghidra UI, physical-phone and your engine integration are separate checks. This is a small reference, not a certification of your project.
Explore the related game idea · Start with Build a Game · Improve your existing game
Five practical steps
Use an owned specimen
This exercise studies an invented four-byte record from systems.mjs, not a commercial game save. The sample hex bytes are 01 0c 00 01. Save a copy and its hash before experimenting; work only on files you own or have permission to inspect.
Expected result: A fixed specimen with known provenance.
Check it: Confirm its size is four bytes; do not run an arbitrary downloaded binary.
decodeRecordObserve the inputs → check the state → compare the resultChange one field at a time
Create records for 12, 13 and 300 coins while keeping the room fixed. Compare bytes by offset. A change at one offset is evidence, but it does not prove there is no checksum, compression or wider field in another format.
Expected result: An offset table derived from controlled inputs.
Check it: Twelve to thirteen changes 0c to 0d; 300 is 2c 01 across two bytes.
Test the endian hypothesis
For the middle two bytes, little-endian gives 0x012c = 300; big-endian gives 0x2c01 = 11265. Use DataView.getUint16(1,true) for the proposed little-endian field. Passing the actual byteOffset matters for slices of larger buffers.
Expected result: A two-byte unsigned count consistent with all samples.
Check it: Decode a four-byte subarray of a larger buffer and confirm it matches the direct sample.
Validate length and enums
The reference checks exactly four bytes, version one and room zero or one. It returns a new data object and never interprets bytes as executable instructions. Unknown versions and lengths need explicit refusal, not a guess that destroys progress.
Expected result: A small parser that rejects unsupported records.
Check it: Try three bytes, version two and room three; each throws without modifying a game.
Document your confidence
Write offset, width, signedness, endianness and observed range for each field. To study a different permitted save, repeat the controlled experiment and identify wrappers, checksums or compression from evidence. Do not transfer this toy layout to unrelated games.
Expected result: A reproducible format note with limitations.
Check it: Keep the original specimen, comparison samples and expected outputs so another person can repeat the conclusion.
Read and reuse the actual code
systems.mjs: complete source · Standalone behavioural checks · Shared browser runner and input. Original BINX Forge code, MIT; the package includes every required file and its notice.
Inspect complete systems.mjs
// Original BINX Forge practice code. MIT; see LICENSE.txt.
export function readSave(raw) {
const s = JSON.parse(raw);
if (!s || s.version !== 1 || !Number.isInteger(s.coins) || s.coins < 0 || s.coins > 9999 || !['room-a','room-b'].includes(s.room)) throw Error('Invalid save');
return {version:1, coins:s.coins, room:s.room};
}
export function transfer(from, to, id, count, capacity=10) {
if (!Number.isInteger(count) || count < 1 || !Number.isInteger(from[id]) || from[id] < count || Object.values(to).reduce((a,b)=>a+b,0)+count > capacity) return false;
from[id]-=count; to[id]=(to[id]||0)+count; return true;
}
export function pointerInput() {
const owners=new Map();
return {press:(id,action)=>owners.set(id,action),release:id=>owners.delete(id),clear:()=>owners.clear(),held:action=>[...owners.values()].includes(action)};
}
export function enemyMode(distance, hp, cooldown) {
if (hp<=0) return 'dead';
if (cooldown>0) return 'recover';
if (distance<24) return 'attack';
return distance<180?'chase':'idle';
}
export function frameSummary(samples) {
const s=samples.filter(Number.isFinite).filter(x=>x>=0).sort((a,b)=>a-b);
if (!s.length) throw Error('No samples');
return {median:s[Math.floor((s.length-1)*.5)],p95:s[Math.ceil(s.length*.95)-1],count:s.length};
}
export function fixedStep(clock, elapsed, update) {
clock.carry+=Math.max(0,Math.min(.1,elapsed));
while(clock.carry>=1/60){update(1/60);clock.carry-=1/60;}
}
// Owned protocol specimen: version:u8, coins:u16 little-endian, room:u8.
export function decodeRecord(bytes) {
if(bytes.length!==4)throw Error('Expected four bytes');
const v=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);
if(v.getUint8(0)!==1||v.getUint8(3)>1)throw Error('Unknown record');
return {version:1,coins:v.getUint16(1,true),room:v.getUint8(3)};
}
export function applyInput(player, packet) {
if(!packet||!Number.isInteger(packet.seq)||packet.seq<=player.seq||![-1,0,1].includes(packet.dx))return false;
player.seq=packet.seq;player.x=Math.max(0,Math.min(100,player.x+packet.dx*2));return true;
}
export function jumpTrace(speed=300,gravity=900,dt=1/120) {
let y=0,vy=-speed,t=0,peak=0;
const rows=[{t,y,vy}];
while(t<3){vy+=gravity*dt;y+=vy*dt;t+=dt;peak=Math.min(peak,y);rows.push({t,y,vy});if(y>=0)break;}
return {rows,height:-peak,airtime:t};
}
Code rights: the included MIT notice permits use, modification and distribution, including commercial games and source products, with the copyright and licence notice. Added third-party files have their own terms. Read the official licence & usage terms.
Common failures and fixes
A sliced buffer gives nonsense
Respect Uint8Array.byteOffset and byteLength when constructing DataView.
A large value looks negative
Check width and signedness against multiple samples before choosing an integer reader.
Editing a real save corrupts it
Work on copies; checksum or compression may invalidate direct byte edits.
Make Your Game Better
Change one system after the baseline works. Keep the free reference and compare the same inputs before and after.
Add a checksum to your own format
Define and test it separately; show why a parser must validate before loading.
Build a format viewer
Display offsets and decoded values locally with bounded file size and no automatic upload.
Licensing summary, not legal advice. Before publishing or selling, check exact terms for finished-game use, reselling files and including files in a source/template product separately.
Learn at the original sources
Sources checked 11 October 2026. The numbered plan is original Forge instruction; linked documentation does not imply every engine or device has been tested.
Take this guide to your AI
Review and copy into your assistant. Nothing is sent automatically. These prompts include the complete focus source, full-package links, checks and compatibility limits.
Review a prompt or select its text manually.
Review build prompt
Review debug prompt
Review upgrade prompt
Keep learning
Original reverse-engineering practice lab · Sonic Unleashed creator-project study · SpaghettiKart creator-project study
Browse every guide and recipe →