mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-26 02:11:39 -04:00
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:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user