Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/bvm-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,9 @@ none = void/Bool. Parameter sigils inside the argument list use the same.
| `CREATEEMITTER` | `CREATEEMITTER(PARAM1%, PARAM2$, PARAM3%, PARAM4%, PARAM5#=0, PARAM6#=0, PARAM7#=0, PARAM8%=0)` | None |
| `DAY` | `DAY()` : Int | None |
| `DEFAULTFACTIONRATING` | `DEFAULTFACTIONRATING(PARAM1$, PARAM2$)` : Int | None |
| `DELETEABILITY` | `DELETEABILITY(PARAM1%, PARAM2$)` | None |
| `DELETEABILITY` | `DELETEABILITY(PARAM1%, PARAM2$)` | Privileged |
| `DELETEACTOREFFECT` | `DELETEACTOREFFECT(PARAM1%, PARAM2$)` | None |
| `DELETEQUEST` | `DELETEQUEST(PARAM1%, PARAM2$)` | None |
| `DELETEQUEST` | `DELETEQUEST(PARAM1%, PARAM2$)` | Privileged |
| `DEQUOTE` | `DEQUOTE(PARAM1$)` : String | None |
| `DOTTEDIP` | `DOTTEDIP(Param1%)` : String | Privileged |
| `FINDACTOR` | `FINDACTOR(PARAM1$, ACTORTYPE% = 3)` : Int | None |
Expand Down
19 changes: 19 additions & 0 deletions src/Modules/ScriptingCommands.bb
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,16 @@ Function BVM_ADDABILITY(Param1%, Param2$, Param3%=1)
End Function

Function BVM_DELETEABILITY(Param1%, Param2$)
; Full-priv gate. Strips an ability/spell from any actor handle --
; equivalent-effect bypass of the already-gated BVM_SETABILITYLEVEL
; (which gates because zeroing ability levels bricks a player's
; combat toolkit; deleting the ability outright is strictly worse).
; For Examine/Trade/RightClick/ItemScript spawns SI\AI = Handle(clicker),
; so a self-or-priv gate would let DeleteAbility(clicker, "<spell>")
; through -- RequirePrivileged refuses regardless of the target.
; No shipped content script calls DeleteAbility (grep of data/ found
; zero callers), so the gate breaks nothing.
If Not BVM_RequirePrivileged() Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
SpellName$ = Upper$(Param2$)
Expand Down Expand Up @@ -2222,6 +2232,15 @@ Function BVM_COMPLETEQUEST(Param1%, Param2$)
End Function

Function BVM_DELETEQUEST(Param1%, Param2$)
; Full-priv gate. Wipes a target's quest-log entry -- equivalent-effect
; bypass of the gated quest/progression mutators: a non-priv clicker
; script could erase a player's quest progress. For Examine/Trade/
; RightClick/ItemScript spawns SI\AI = Handle(clicker), so a self-or-priv
; gate would let DeleteQuest(clicker, "<quest>") through --
; RequirePrivileged refuses regardless of the target. No shipped content
; script calls DeleteQuest (grep of data/ found zero callers), so the
; gate breaks nothing.
If Not BVM_RequirePrivileged() Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
If Actor\RNID > 0
Expand Down
87 changes: 87 additions & 0 deletions src/Tests/Modules/BVMPrivilegeGateTest.bb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ EnableGC
; BVM_REMOVEZONEINSTANCE (admin-only) RequirePrivileged
; BVM_SETACTORGLOBAL (per-actor state) RequireSelfOrPrivileged
; BVM_SETSUPERGLOBAL (server-wide state) RequirePrivileged
; BVM_DELETEABILITY BVM_SETABILITYLEVEL RequirePrivileged
; BVM_DELETEQUEST (quest-log wipe) RequirePrivileged
;
; Four sibling functions (SETACTORAISTATE / SETACTORTARGET / SETNAME /
; SETTAG) intentionally stay UNGATED -- shipped content scripts in
Expand Down Expand Up @@ -111,6 +113,8 @@ Global MutationSetActorGroup = 0
Global MutationRemoveZone = 0
Global MutationSetActorGlobal = 0 ; self-or-priv gated
Global MutationSetSuperGlobal = 0 ; full-priv gated
Global MutationDeleteAbility = 0 ; full-priv gated
Global MutationDeleteQuest = 0 ; full-priv gated

Function MockBVM_CHANGEGOLD(Param1%, Param2%)
If Not BVM_RequirePrivileged() Then Return
Expand Down Expand Up @@ -225,6 +229,24 @@ Function MockBVM_SETSUPERGLOBAL(Param1%, Param2$)
MutationSetSuperGlobal = MutationSetSuperGlobal + 1
End Function

; DeleteAbility gate. Strips an ability/spell from any actor handle --
; equivalent-effect bypass of the gated SETABILITYLEVEL (deleting is
; strictly worse than zeroing the level). Param1 is an actor handle and
; the clicker-spawn shape makes SI\AI = Handle(clicker), so full-priv
; (not self-or-priv). Zero shipped content-script callers.
Function MockBVM_DELETEABILITY(Param1%, Param2$)
If Not BVM_RequirePrivileged() Then Return
MutationDeleteAbility = MutationDeleteAbility + 1
End Function

; DeleteQuest gate. Wipes a target's quest-log entry -- a non-priv
; clicker script could erase a player's quest progress. Same clicker
; SI\AI = Handle(clicker) shape, so full-priv. Zero shipped callers.
Function MockBVM_DELETEQUEST(Param1%, Param2$)
If Not BVM_RequirePrivileged() Then Return
MutationDeleteQuest = MutationDeleteQuest + 1
End Function

; --- Test fixture helpers ----------------------------------------------
Function ResetMutationCounters()
MutationGold = 0
Expand All @@ -245,6 +267,8 @@ Function ResetMutationCounters()
MutationRemoveZone = 0
MutationSetActorGlobal = 0
MutationSetSuperGlobal = 0
MutationDeleteAbility = 0
MutationDeleteQuest = 0
LastScriptLog$ = ""
End Function

Expand Down Expand Up @@ -807,3 +831,66 @@ Test testSetSuperGlobalGatePassesForPrivileged()
MockBVM_SETSUPERGLOBAL(5, "value")
Assert(MutationSetSuperGlobal = 1)
End Test

; ======================================================================
; DeleteAbility / DeleteQuest gates -- full privileged. Both take an
; actor/target handle and destroy persistent player state (a learned
; ability, a quest-log entry). They are equivalent-effect bypasses of
; the gated SETABILITYLEVEL / quest-progression mutators: deleting is
; strictly worse than zeroing. The clicker-driven spawn shape sets
; SI\AI = Handle(clicker), so a self-or-priv gate would let
; DeleteAbility(clicker, ...) / DeleteQuest(clicker, ...) through --
; full RequirePrivileged refuses regardless of how the target relates
; to SI\AI. Zero shipped content-script callers, so the gate breaks
; nothing.
; ======================================================================

Test testDeleteAbilityGateBlocksNonPrivileged()
InstallScript(0, 0, 0)
ResetMutationCounters()
MockBVM_DELETEABILITY(999, "Fireball")
Assert(MutationDeleteAbility = 0)
; Refusal must be audit-logged, not silently dropped.
Assert(Len(LastScriptLog$) > 0)
End Test

Test testDeleteAbilityGateBlocksStrippingOwnAITarget()
; Clicker shape: SI\AI = clicker handle (777), Param1 = clicker.
; A self-or-priv gate would match SI\AI and let the strip through;
; full-priv must still refuse.
InstallScript(0, 777, 200)
ResetMutationCounters()
MockBVM_DELETEABILITY(777, "Fireball")
Assert(MutationDeleteAbility = 0)
End Test

Test testDeleteAbilityGatePassesForPrivileged()
InstallScript(1, 0, 0)
ResetMutationCounters()
MockBVM_DELETEABILITY(999, "Fireball")
Assert(MutationDeleteAbility = 1)
End Test

Test testDeleteQuestGateBlocksNonPrivileged()
InstallScript(0, 0, 0)
ResetMutationCounters()
MockBVM_DELETEQUEST(999, "MainQuest")
Assert(MutationDeleteQuest = 0)
Assert(Len(LastScriptLog$) > 0)
End Test

Test testDeleteQuestGateBlocksWipingOwnAITarget()
; Clicker shape: SI\AI = clicker (777), Param1 = clicker. Full-priv
; refuses where self-or-priv would not.
InstallScript(0, 777, 200)
ResetMutationCounters()
MockBVM_DELETEQUEST(777, "MainQuest")
Assert(MutationDeleteQuest = 0)
End Test

Test testDeleteQuestGatePassesForPrivileged()
InstallScript(1, 0, 0)
ResetMutationCounters()
MockBVM_DELETEQUEST(999, "MainQuest")
Assert(MutationDeleteQuest = 1)
End Test
Loading