Keep the score out of the runtime and refuse a pinned script at the gate.

The HUD carried a table of which script functions it still owed and where each
line came from; nothing read it, so it goes. publish-check now rejects a script
pinned by content hash and line range the way it already rejects a retail
address — a gap id naming a hole in our own runtime is not that shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bHwyKwUjaYmtVG1F3Ttku
This commit is contained in:
vtroc
2026-09-14 23:36:18 +03:00
parent a7c0958668
commit 0cecc56887
3 changed files with 48 additions and 109 deletions
-100
View File
@@ -59,103 +59,3 @@ pub(crate) fn hudelem_text_fx(elem: &HudElem, cg_time: i32) -> Option<TextRunFx>
},
})
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ProducerStatus {
Implemented,
Unreachable(&'static str),
Gap,
}
pub struct ScriptedProducer {
pub script_fn: &'static str,
pub cite: &'static str,
pub produces: &'static str,
pub status: ProducerStatus,
}
pub const SCRIPTED_PRODUCERS: &[ScriptedProducer] = &[
ScriptedProducer {
script_fn: "_hud_message::init",
cite: "gsc:maps/mp/gametypes/_hud_message.gsc@sha256:e5773c6ad4e840b7#L5-41",
produces: "precached outcome strings and splash defaults",
status: ProducerStatus::Gap,
},
ScriptedProducer {
script_fn: "_hud_message::initNotifyMessage",
cite: "gsc:maps/mp/gametypes/_hud_message.gsc@sha256:e5773c6ad4e840b7#L69-145",
produces: "title / text / text2 / icon / overlay elements, fonts, offsets, glow",
status: ProducerStatus::Gap,
},
ScriptedProducer {
script_fn: "_hud_message::notifyMessage",
cite: "gsc:maps/mp/gametypes/_hud_message.gsc@sha256:e5773c6ad4e840b7#L163-188",
produces: "the four splash queues and their serialisation",
status: ProducerStatus::Gap,
},
ScriptedProducer {
script_fn: "_hud_message::showNotifyMessage",
cite: "gsc:maps/mp/gametypes/_hud_message.gsc@sha256:e5773c6ad4e840b7#L221-374",
produces: "the splash timeline: text/value, pulse, icon layering, fade, scale",
status: ProducerStatus::Gap,
},
ScriptedProducer {
script_fn: "_hud_message::lowerMessageThink",
cite: "gsc:maps/mp/gametypes/_hud_message.gsc@sha256:e5773c6ad4e840b7#L720-742",
produces: "the lower message and its timer",
status: ProducerStatus::Gap,
},
ScriptedProducer {
script_fn: "_hud_message::matchOutcomeNotify",
cite: "gsc:maps/mp/gametypes/_hud_message.gsc@sha256:e5773c6ad4e840b7#L766-806",
produces: "match outcome presentation",
status: ProducerStatus::Gap,
},
ScriptedProducer {
script_fn: "_hud_message::outcomeNotify",
cite: "gsc:maps/mp/gametypes/_hud_message.gsc@sha256:e5773c6ad4e840b7#L1038-1171",
produces: "FFA title + reason + HE_TYPE_PLAYERNAME 1st/2nd/3rd leftover",
status: ProducerStatus::Implemented,
},
ScriptedProducer {
script_fn: "_hud_message::hintMessage",
cite: "gsc:maps/mp/gametypes/_hud_message.gsc@sha256:e5773c6ad4e840b7#L58-66",
produces: "OBJECTIVES_DM_HINT leftover (notifyText, 4s, glow 0.3/0.6/0.3); not the splash queue",
status: ProducerStatus::Implemented,
},
ScriptedProducer {
script_fn: "_damagefeedback::updateDamageFeedback",
cite: "gsc:maps/mp/gametypes/_damagefeedback.gsc@sha256:141664ce14ccb2d7#L28-93",
produces: "attacker hit X on g_hudelems (archived=true); CopyInUseHudElems + BG_LerpHudColors at cg.time",
status: ProducerStatus::Implemented,
},
ScriptedProducer {
script_fn: "_rank::scorePopup",
cite: "gsc:maps/mp/gametypes/_rank.gsc@sha256:4b31033a8baf6c9a#L504-557",
produces: "center HE_TYPE_VALUE + MP_PLUS; FFA !rankingEnabled colour from givePlayerScore",
status: ProducerStatus::Implemented,
},
ScriptedProducer {
script_fn: "_gamelogic::matchStartTimer",
cite: "gsc:maps/mp/gametypes/_gamelogic.gsc@sha256:75b0836bafb28b3f#L985-1022",
produces: "server FontString loc + pulsing hudbig setValue; leftover CG_DrawHudElem",
status: ProducerStatus::Implemented,
},
ScriptedProducer {
script_fn: "_events::firstBlood",
cite: "gsc:maps/mp/_events.gsc@sha256:88cc647a71e0925e#L563-572",
produces: "SplashNotifyDelayed firstblood when level.numKills==1; ActivateSplash stand-in",
status: ProducerStatus::Implemented,
},
];
pub fn open_producer_count() -> usize {
SCRIPTED_PRODUCERS
.iter()
.filter(|p| p.status == ProducerStatus::Gap)
.count()
}
+1 -3
View File
@@ -37,9 +37,7 @@ pub use draw2d::{
pub use gaps::{GapCause, HudGap, HudPresentationGaps};
pub use gpu_list::{HudTessBatch, HudTessGpuFrame, HudTessTechnique, HudTessVertex};
pub use hitmarker::PendingHitmarker;
pub use hudelem::{
HudElemSoundLatch, ProducerStatus, SCRIPTED_PRODUCERS, ScriptedProducer, open_producer_count,
};
pub use hudelem::HudElemSoundLatch;
pub use overhead_names::{
OverheadPosedHead, OverheadPosedPlayerFrame, OverheadPosedPlayerFramePublished,
};
+47 -6
View File
@@ -12,12 +12,13 @@
//! private key, a piece of an original game install — none of it is the
//! product, and a stray `git add -f` is all it takes. This is the half worth
//! running before every push.
//! 2. **Retail offsets.** A standalone runtime resolves nothing against a
//! retail image, so a hardcoded address from one is dead weight wherever it
//! appears, and a decompiler placeholder name (`FUN_…`, `DAT_…`) is a
//! half-finished symbol. Naming IW4x, KisakCOD, an original executable or
//! Ghidra is *not* a finding: crediting what was read is the policy, not a
//! leak.
//! 2. **Retail offsets and pinned source.** A standalone runtime resolves
//! nothing against a retail image, so a hardcoded address from one is dead
//! weight wherever it appears, a decompiler placeholder name (`FUN_…`,
//! `DAT_…`) is a half-finished symbol, and a script file pinned by hash and
//! line range is an index into somebody else's tree. Naming IW4x, KisakCOD,
//! an original executable or Ghidra is *not* a finding: crediting what was
//! read is the policy, not a leak.
//!
//! It walks the tracked tree, so it sees exactly what a push would publish —
//! and only that. It cannot see history, and it cannot see a release archive;
@@ -322,6 +323,25 @@ fn address_in_name(lower: &str) -> bool {
})
}
/// A script file pinned by content hash and line range. Nothing here reads one,
/// and a `ScriptGap` id, which names a hole in our own runtime, is not this
/// shape.
fn gsc_citation(lower: &str) -> bool {
let Some(at) = lower.find(".gsc@sha256:") else {
return lower.find(".gsc#l").is_some_and(|at| {
lower[at + ".gsc#l".len()..]
.as_bytes()
.first()
.is_some_and(u8::is_ascii_digit)
});
};
lower[at + ".gsc@sha256:".len()..]
.bytes()
.take_while(|b| is_hex(*b))
.count()
>= 16
}
fn scan_line(line: &str) -> Option<String> {
let lower = line.to_ascii_lowercase();
let bytes = lower.as_bytes();
@@ -362,6 +382,9 @@ fn scan_line(line: &str) -> Option<String> {
if address_in_name(&lower) {
return Some("address inside an identifier".to_string());
}
if gsc_citation(&lower) {
return Some("script citation (path, hash and line range)".to_string());
}
None
}
@@ -508,6 +531,24 @@ mod tests {
assert!(!looks_binary(b"pub const KICK_STEP_MS: i32 = 5;\n"));
}
/// A script pinned by hash and line range is an index into somebody else's
/// tree. The gap ids, which name holes in our own runtime, are not that.
#[test]
fn catches_a_pinned_script() {
for line in [
r#"pub const CITE: &str = "gsc:maps/mp/_destructables.gsc@sha256:05084ef406fb5ea1#L1-19";"#,
"// maps/mp/gametypes/_rank.gsc#L504-557 is where the popup lives",
] {
assert!(scan_line(line).is_some(), "missed: {line}");
}
for line in [
r#"Self::DestructablesPlayFx => "gsc.mp._destructables.destructable_destruct","#,
r#"let raw = zone.rawfile("maps/mp/_destructables.gsc")?;"#,
] {
assert!(scan_line(line).is_none(), "false positive: {line}");
}
}
/// Naming what was read is the policy, so none of this is a finding any
/// more. Each line is one the old gate rejected.
#[test]