Initial commit — Godot space roguelite source
- Touch controls: direct InputEventScreenTouch in shop_ui (bypass relay) - ItemDB: static preload list instead of DirAccess scan (export fix) - All 18 items with EN localisation (name_en, desc_en, category_en) - Ship playstyles: NOVA-1 shield, INFERNO ram, AURORA agile/tank - Quasar: SMBH visual, jet boost, merge, push, BH-eating - Atlas & UI text updated EN+DE Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
// spacel leaderboard server
|
||||
// Setup: cd server && npm install && node server.js
|
||||
// Config: set env var SPACEL_SECRET to the same value as HMAC_SECRET in online_leaderboard.gd
|
||||
// set env var PORT to override default 3000
|
||||
|
||||
const express = require('express');
|
||||
const Database = require('better-sqlite3');
|
||||
const crypto = require('crypto');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const app = express();
|
||||
const db = new Database(process.env.DB_PATH || 'scores.db');
|
||||
const SECRET = process.env.SPACEL_SECRET || 'CHANGE-ME-BEFORE-DEPLOY';
|
||||
const PORT = parseInt(process.env.PORT || '3000', 10);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS scores (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
score INTEGER NOT NULL CHECK(score >= 0 AND score <= 500000),
|
||||
wave INTEGER NOT NULL CHECK(wave >= 1 AND wave <= 100),
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
app.use(express.json({ limit: '4kb' }));
|
||||
|
||||
const submitLimit = rateLimit({
|
||||
windowMs: 10 * 60 * 1000,
|
||||
max: 1,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: 'Rate limit: 1 submission per 10 minutes per IP.' }
|
||||
});
|
||||
|
||||
// GET /scores — top 10 by score
|
||||
app.get('/scores', (_req, res) => {
|
||||
const rows = db
|
||||
.prepare('SELECT name, score, wave, created_at FROM scores ORDER BY score DESC LIMIT 10')
|
||||
.all();
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// POST /scores — submit a new score
|
||||
// Body: { name, score, wave, timestamp, hmac }
|
||||
// HMAC = SHA256(SECRET, name + score + wave + timestamp)
|
||||
app.post('/scores', submitLimit, (req, res) => {
|
||||
const { name, score, wave, timestamp, hmac } = req.body ?? {};
|
||||
|
||||
if (name == null || score == null || wave == null || timestamp == null || hmac == null)
|
||||
return res.status(400).json({ error: 'Missing fields' });
|
||||
|
||||
if (typeof name !== 'string' ||
|
||||
!Number.isInteger(score) ||
|
||||
!Number.isInteger(wave) ||
|
||||
!Number.isInteger(timestamp) ||
|
||||
typeof hmac !== 'string')
|
||||
return res.status(400).json({ error: 'Invalid field types' });
|
||||
|
||||
// Reject stale timestamps (±5 minutes)
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (Math.abs(now - timestamp) > 300)
|
||||
return res.status(400).json({ error: 'Timestamp expired' });
|
||||
|
||||
if (score < 0 || score > 500000)
|
||||
return res.status(400).json({ error: 'Score out of range' });
|
||||
if (wave < 1 || wave > 100)
|
||||
return res.status(400).json({ error: 'Wave out of range' });
|
||||
|
||||
const cleanName = name.trim().slice(0, 12);
|
||||
if (cleanName.length === 0)
|
||||
return res.status(400).json({ error: 'Name required' });
|
||||
|
||||
// Verify HMAC — must match what online_leaderboard.gd sends
|
||||
const raw = name + String(score) + String(wave) + String(timestamp);
|
||||
const expected = crypto.createHmac('sha256', SECRET).update(raw).digest('hex');
|
||||
let valid = false;
|
||||
try {
|
||||
// timingSafeEqual requires equal-length buffers; mismatch length → invalid
|
||||
if (hmac.length === expected.length) {
|
||||
valid = crypto.timingSafeEqual(
|
||||
Buffer.from(hmac, 'hex'),
|
||||
Buffer.from(expected, 'hex')
|
||||
);
|
||||
}
|
||||
} catch { /* invalid hex */ }
|
||||
|
||||
if (!valid)
|
||||
return res.status(403).json({ error: 'Invalid signature' });
|
||||
|
||||
db.prepare(
|
||||
'INSERT INTO scores (name, score, wave, created_at) VALUES (?, ?, ?, ?)'
|
||||
).run(cleanName, score, wave, new Date().toISOString().split('T')[0]);
|
||||
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.listen(PORT, () =>
|
||||
console.log(`spacel leaderboard running on port ${PORT}`)
|
||||
);
|
||||
Reference in New Issue
Block a user