Skip to content

Commit 26fa1ef

Browse files
authored
adding a few new event functions and a ContainerObject (#598)
# DESCRIPTION Adds two new pre-condition script hooks (`onTryPurchase` for items, `onTryEnter` for rooms) and a new `ContainerObject` scripting type that replaces the previous string-only container API. All three additions follow the existing hook conventions and are documented in the admin scripting pages. ## CHANGES - **`onTryPurchase` item hook** — fires on the item's script after the buyer's gold is confirmed sufficient but before any gold is deducted or stock removed. Return `false` to silently block the purchase. Signature: `onTryPurchase(user ActorObject, item ItemObject, room RoomObject)`. - **`onTryEnter` room hook** — fires on the *destination* room's script before a player enters, for both normal movement and combat fleeing. Return `false` to block the movement. Not called for admin teleports, spawns, or script-driven `MoveRoom` calls. Signature: `onTryEnter(user ActorObject, room RoomObject)`. - **`ContainerObject`** — new scripting type returned by `RoomObject.GetContainers()` (previously returned `[]string`). Exposes the following methods: - `Name()` — the container's name key - `HasLock()` — true if a lock is configured - `IsLocked()` — true if currently locked - `Lock()` / `Unlock()` — set lock state - `GetItems()` — returns all items as `[]ItemObject` - `FindItem(itemName)` — fuzzy-searches container contents, returns `ItemObject` or null - `AddItem(item)` / `RemoveItem(item)` — mutate container contents - `GetGold()` — returns gold amount - `AddGold(amount)` / `RemoveGold(amount)` — mutate container gold; `RemoveGold` returns the actual amount removed - `Count(itemId)` — count of items matching a spec ID - `IsTemporary()` — true if the container has a despawn round set - **`RoomObject.GetContainers()`** return type changed from `[]string` to `[]ContainerObject`. - Updated `/admin/scripting-items` with `onTryPurchase` hook documentation. - Updated `/admin/scripting-rooms` with `onTryEnter` hook documentation. - Updated `/admin/scripting-functions` with a new `ContainerObject` reference section and updated `GetContainers` return type. - Updated scripting schema (`GetScriptFunctionsSchema`) with all new hooks and the `ContainerObject` type definition.
1 parent 47dce69 commit 26fa1ef

10 files changed

Lines changed: 538 additions & 14 deletions

File tree

_datafiles/html/admin/scripting-functions.html

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ <h3>Scripting Docs</h3>
138138
<a href="#item">ItemObject</a>
139139
<a href="#pet">PetObject</a>
140140
<a href="#party">PartyObject</a>
141+
<a href="#container">ContainerObject</a>
141142
<a href="#util">Utility Functions</a>
142143
<a href="#messaging">Messaging Functions</a>
143144
<a href="#panels">Panel Layout Functions</a>
@@ -1149,9 +1150,9 @@ <h2>RoomObject</h2>
11491150
</div>
11501151
</details>
11511152

1152-
<details class="fn-card" data-search="GetContainers containers array string room">
1153-
<summary><span class="fn-name-sig"><span class="fn-n">RoomObject.GetContainers</span><span class="fn-p">()</span></span><span class="fn-ret-badge ret-array">[]string</span></summary>
1154-
<div class="fn-body"><p class="fn-desc">Returns the names of all containers in the room.</p></div>
1153+
<details class="fn-card" data-search="GetContainers containers array room">
1154+
<summary><span class="fn-name-sig"><span class="fn-n">RoomObject.GetContainers</span><span class="fn-p">()</span></span><span class="fn-ret-badge ret-array">[]ContainerObject</span></summary>
1155+
<div class="fn-body"><p class="fn-desc">Returns all containers in the room as <a href="#container">ContainerObject</a> instances. Each object exposes the container's name, lock state, items, and gold.</p></div>
11551156
</details>
11561157

11571158
<details class="fn-card" data-search="GetExits exits array object room direction lock secret">
@@ -1616,6 +1617,121 @@ <h2>PartyObject</h2>
16161617
</div>
16171618
</section>
16181619

1620+
<!-- ═══════════════════════════════════════════════════════════════════
1621+
CONTAINER OBJECT
1622+
════════════════════════════════════════════════════════════════════ -->
1623+
<section class="fn-group" id="container">
1624+
<div class="fn-group-header">
1625+
<h2>ContainerObject</h2>
1626+
<span class="fn-group-badge">Rooms</span>
1627+
</div>
1628+
<p class="fn-group-desc">Returned by <a href="#room">RoomObject</a>.GetContainers(). Represents a named container inside a room and provides access to its contents, lock state, and gold.</p>
1629+
<div class="fn-list">
1630+
1631+
<details class="fn-card" data-search="ContainerObject Name name string container">
1632+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.Name</span><span class="fn-p">()</span></span><span class="fn-ret-badge ret-string">string</span></summary>
1633+
<div class="fn-body"><p class="fn-desc">Returns the container's name (its key in the room's container map).</p></div>
1634+
</details>
1635+
1636+
<details class="fn-card" data-search="ContainerObject HasLock has lock bool container">
1637+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.HasLock</span><span class="fn-p">()</span></span><span class="fn-ret-badge ret-bool">bool</span></summary>
1638+
<div class="fn-body"><p class="fn-desc">Returns <code>true</code> if the container has a lock configured (difficulty &gt; 0), regardless of whether it is currently locked or unlocked.</p></div>
1639+
</details>
1640+
1641+
<details class="fn-card" data-search="ContainerObject IsLocked locked bool container">
1642+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.IsLocked</span><span class="fn-p">()</span></span><span class="fn-ret-badge ret-bool">bool</span></summary>
1643+
<div class="fn-body"><p class="fn-desc">Returns <code>true</code> if the container is currently locked.</p></div>
1644+
</details>
1645+
1646+
<details class="fn-card" data-search="ContainerObject Lock lock container">
1647+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.Lock</span><span class="fn-p">()</span></span><span class="fn-ret-badge ret-void">void</span></summary>
1648+
<div class="fn-body"><p class="fn-desc">Locks the container. Has no effect if the container has no lock configured.</p></div>
1649+
</details>
1650+
1651+
<details class="fn-card" data-search="ContainerObject Unlock unlock container">
1652+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.Unlock</span><span class="fn-p">()</span></span><span class="fn-ret-badge ret-void">void</span></summary>
1653+
<div class="fn-body"><p class="fn-desc">Unlocks the container. Has no effect if the container has no lock configured.</p></div>
1654+
</details>
1655+
1656+
<details class="fn-card" data-search="ContainerObject GetItems items array container">
1657+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.GetItems</span><span class="fn-p">()</span></span><span class="fn-ret-badge ret-array">[]ItemObject</span></summary>
1658+
<div class="fn-body"><p class="fn-desc">Returns all items currently inside the container as an array of <a href="#item">ItemObject</a>.</p></div>
1659+
</details>
1660+
1661+
<details class="fn-card" data-search="ContainerObject FindItem find item search container">
1662+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.FindItem</span><span class="fn-p">(</span><span class="fn-a">itemName</span> <span class="fn-t">string</span><span class="fn-p">)</span></span><span class="fn-ret-badge ret-object">ItemObject|null</span></summary>
1663+
<div class="fn-body">
1664+
<p class="fn-desc">Searches the container for an item matching the given name. Supports fuzzy matching.</p>
1665+
<table class="arg-table"><thead><tr><th>Argument</th><th>Type</th><th>Description</th></tr></thead><tbody>
1666+
<tr><td class="an">itemName</td><td class="at">string</td><td>The name to search for.</td></tr>
1667+
</tbody></table>
1668+
<p class="fn-note">Returns the matching ItemObject, or <code>null</code> if not found.</p>
1669+
</div>
1670+
</details>
1671+
1672+
<details class="fn-card" data-search="ContainerObject AddItem add item container">
1673+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.AddItem</span><span class="fn-p">(</span><span class="fn-a">item</span> <span class="fn-t">ItemObject</span><span class="fn-p">)</span></span><span class="fn-ret-badge ret-bool">bool</span></summary>
1674+
<div class="fn-body">
1675+
<p class="fn-desc">Adds an item to the container. Returns <code>false</code> if the item is invalid.</p>
1676+
<table class="arg-table"><thead><tr><th>Argument</th><th>Type</th><th>Description</th></tr></thead><tbody>
1677+
<tr><td class="an">item</td><td class="at"><a href="#item">ItemObject</a></td><td>The item to add.</td></tr>
1678+
</tbody></table>
1679+
</div>
1680+
</details>
1681+
1682+
<details class="fn-card" data-search="ContainerObject RemoveItem remove item container">
1683+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.RemoveItem</span><span class="fn-p">(</span><span class="fn-a">item</span> <span class="fn-t">ItemObject</span><span class="fn-p">)</span></span><span class="fn-ret-badge ret-bool">bool</span></summary>
1684+
<div class="fn-body">
1685+
<p class="fn-desc">Removes an item from the container. Returns <code>true</code> if the item was found and removed.</p>
1686+
<table class="arg-table"><thead><tr><th>Argument</th><th>Type</th><th>Description</th></tr></thead><tbody>
1687+
<tr><td class="an">item</td><td class="at"><a href="#item">ItemObject</a></td><td>The item to remove.</td></tr>
1688+
</tbody></table>
1689+
</div>
1690+
</details>
1691+
1692+
<details class="fn-card" data-search="ContainerObject GetGold gold amount container">
1693+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.GetGold</span><span class="fn-p">()</span></span><span class="fn-ret-badge ret-int">int</span></summary>
1694+
<div class="fn-body"><p class="fn-desc">Returns the amount of gold currently in the container.</p></div>
1695+
</details>
1696+
1697+
<details class="fn-card" data-search="ContainerObject AddGold add gold container">
1698+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.AddGold</span><span class="fn-p">(</span><span class="fn-a">amount</span> <span class="fn-t">int</span><span class="fn-p">)</span></span><span class="fn-ret-badge ret-void">void</span></summary>
1699+
<div class="fn-body">
1700+
<p class="fn-desc">Adds the specified amount of gold to the container. Amount must be positive.</p>
1701+
<table class="arg-table"><thead><tr><th>Argument</th><th>Type</th><th>Description</th></tr></thead><tbody>
1702+
<tr><td class="an">amount</td><td class="at">int</td><td>Amount of gold to add.</td></tr>
1703+
</tbody></table>
1704+
</div>
1705+
</details>
1706+
1707+
<details class="fn-card" data-search="ContainerObject RemoveGold remove gold container">
1708+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.RemoveGold</span><span class="fn-p">(</span><span class="fn-a">amount</span> <span class="fn-t">int</span><span class="fn-p">)</span></span><span class="fn-ret-badge ret-int">int</span></summary>
1709+
<div class="fn-body">
1710+
<p class="fn-desc">Removes gold from the container. Removes at most the amount currently present. Returns the actual amount removed.</p>
1711+
<table class="arg-table"><thead><tr><th>Argument</th><th>Type</th><th>Description</th></tr></thead><tbody>
1712+
<tr><td class="an">amount</td><td class="at">int</td><td>Amount of gold to remove.</td></tr>
1713+
</tbody></table>
1714+
</div>
1715+
</details>
1716+
1717+
<details class="fn-card" data-search="ContainerObject Count count items itemId container">
1718+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.Count</span><span class="fn-p">(</span><span class="fn-a">itemId</span> <span class="fn-t">int</span><span class="fn-p">)</span></span><span class="fn-ret-badge ret-int">int</span></summary>
1719+
<div class="fn-body">
1720+
<p class="fn-desc">Returns the number of items with the given item spec ID currently in the container.</p>
1721+
<table class="arg-table"><thead><tr><th>Argument</th><th>Type</th><th>Description</th></tr></thead><tbody>
1722+
<tr><td class="an">itemId</td><td class="at">int</td><td>The item spec ID to count.</td></tr>
1723+
</tbody></table>
1724+
</div>
1725+
</details>
1726+
1727+
<details class="fn-card" data-search="ContainerObject IsTemporary temporary despawn container">
1728+
<summary><span class="fn-name-sig"><span class="fn-n">ContainerObject.IsTemporary</span><span class="fn-p">()</span></span><span class="fn-ret-badge ret-bool">bool</span></summary>
1729+
<div class="fn-body"><p class="fn-desc">Returns <code>true</code> if this container is temporary (it has a despawn round set and will disappear over time).</p></div>
1730+
</details>
1731+
1732+
</div>
1733+
</section>
1734+
16191735
<!-- ═══════════════════════════════════════════════════════════════════
16201736
UTILITY FUNCTIONS
16211737
════════════════════════════════════════════════════════════════════ -->

_datafiles/html/admin/scripting-items.html

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ <h3>Scripting Docs</h3>
9393
<a href="#hook-onCommand">onCommand</a>
9494
<a href="#hook-onCommand_x">onCommand_{cmd}</a>
9595
<a href="#hook-onPurchase">onPurchase</a>
96+
<a href="#hook-onTryPurchase">onTryPurchase</a>
9697
</div>
9798
</aside>
9899

@@ -210,6 +211,27 @@ <h2>Event Hooks</h2>
210211
</div>
211212
</details>
212213

214+
<details class="hook-card" id="hook-onTryPurchase">
215+
<summary><span class="hook-sig"><span class="fn-name">onTryPurchase</span><span class="fn-paren">(</span><span class="fn-param">user</span> <span class="fn-type">ActorObject</span>, <span class="fn-param">item</span> <span class="fn-type">ItemObject</span>, <span class="fn-param">room</span> <span class="fn-type">RoomObject</span><span class="fn-paren">)</span></span></summary>
216+
<div class="hook-body">
217+
<p class="hook-desc">Called after the player has been confirmed to have enough gold, but before any gold is deducted or stock removed. Return <code>false</code> to block the purchase entirely — no gold is spent and the item is not removed from the shop.</p>
218+
<div class="hook-returns"><strong>Return value:</strong> <code>false</code> = block the purchase. Any other return value (or none) = allow it.</div>
219+
<table class="arg-table"><thead><tr><th>Argument</th><th>Type</th><th>Description</th></tr></thead><tbody>
220+
<tr><td class="arg-name">user</td><td class="arg-type"><a href="/admin/scripting-functions#actor">ActorObject</a></td><td>The player attempting the purchase.</td></tr>
221+
<tr><td class="arg-name">item</td><td class="arg-type"><a href="/admin/scripting-functions#item">ItemObject</a></td><td>The item being purchased.</td></tr>
222+
<tr><td class="arg-name">room</td><td class="arg-type"><a href="/admin/scripting-functions#room">RoomObject</a></td><td>The shop room.</td></tr>
223+
</tbody></table>
224+
<div class="code-wrap"><button class="copy-btn" onclick="copyCode(this)">Copy</button><pre><code class="language-javascript">function onTryPurchase(user, item, room) {
225+
// Only allow purchase during daytime
226+
if (UtilGetTime().Night) {
227+
user.SendText('The shop is closed at night.');
228+
return false;
229+
}
230+
return true;
231+
}</code></pre></div>
232+
</div>
233+
</details>
234+
213235
</div>
214236
</section>
215237

_datafiles/html/admin/scripting-rooms.html

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ <h3>Scripting Docs</h3>
132132
<a href="#hook-onLoad">onLoad</a>
133133
<a href="#hook-onEnter">onEnter</a>
134134
<a href="#hook-onTryExit">onTryExit</a>
135+
<a href="#hook-onTryEnter">onTryEnter</a>
135136
<a href="#hook-onExit">onExit</a>
136137
<a href="#hook-onIdle">onIdle</a>
137138
<a href="#hook-onCommand">onCommand</a>
@@ -259,6 +260,37 @@ <h2>Event Hooks</h2>
259260
</div>
260261
</details>
261262

263+
<!-- onTryEnter -->
264+
<details class="hook-card" id="hook-onTryEnter">
265+
<summary>
266+
<span class="hook-sig">
267+
<span class="fn-name">onTryEnter</span><span class="fn-paren">(</span><span class="fn-param">user</span> <span class="fn-type">ActorObject</span>, <span class="fn-param">room</span> <span class="fn-type">RoomObject</span><span class="fn-paren">)</span>
268+
</span>
269+
</summary>
270+
<div class="hook-body">
271+
<p class="hook-desc">Called on the <strong>destination</strong> room before a player enters it. Return <code>false</code> to block the movement. This fires for both normal movement and combat fleeing. It is not called for admin teleports, spawns, or script-driven <code>MoveRoom</code> calls.</p>
272+
<div class="hook-returns"><strong>Return value:</strong> <code>false</code> = block the movement. Any other return value (or none) allows it.</div>
273+
<table class="arg-table">
274+
<thead><tr><th>Argument</th><th>Type</th><th>Description</th></tr></thead>
275+
<tbody>
276+
<tr><td class="arg-name">user</td><td class="arg-type"><a href="/admin/scripting-functions#actor">ActorObject</a></td><td>The player attempting to enter.</td></tr>
277+
<tr><td class="arg-name">room</td><td class="arg-type"><a href="/admin/scripting-functions#room">RoomObject</a></td><td>The room being entered (this room).</td></tr>
278+
</tbody>
279+
</table>
280+
<div class="code-wrap">
281+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
282+
<pre><code class="language-javascript">function onTryEnter(user, room) {
283+
// Require a specific quest to enter
284+
if (!user.HasQuest('dungeon-key:start')) {
285+
user.SendText('The dungeon gate is sealed. You need a key.');
286+
return false;
287+
}
288+
return true;
289+
}</code></pre>
290+
</div>
291+
</div>
292+
</details>
293+
262294
<!-- onExit -->
263295
<details class="hook-card" id="hook-onExit">
264296
<summary>

internal/hooks/NewRound_DoCombat.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ func handlePlayerCombat(evt events.NewRound) (affectedPlayerIds []int, affectedM
143143
continue
144144
}
145145

146+
if blocked, err := scripting.TryRoomTryEnterEvent(user.UserId, exitRoomId); err == nil && blocked {
147+
user.SendText(`Something prevents you from fleeing!`)
148+
continue
149+
}
150+
146151
user.SendText(fmt.Sprintf(`You flee to the <ansi fg="exit">%s</ansi> exit!`, exitName))
147152
uRoom.SendText(fmt.Sprintf(`<ansi fg="username">%s</ansi> flees to the <ansi fg="exit">%s</ansi> exit!`, user.Character.Name, exitName), user.UserId)
148153

internal/scripting/item.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,44 @@ func TryItemCommand(cmd string, item items.Item, userId int) (bool, error) {
135135
return false, ErrEventNotFound
136136
}
137137

138+
func TryItemTryPurchaseEvent(item items.Item, userId int) (bool, error) {
139+
140+
sItem := GetItem(item)
141+
142+
timestart := time.Now()
143+
defer func() {
144+
mudlog.Debug("TryItemTryPurchaseEvent()", "itemId", item.ItemId, "userId", userId, "time", time.Since(timestart))
145+
}()
146+
147+
vmw, err := getItemVM(sItem)
148+
if err != nil {
149+
return false, err
150+
}
151+
152+
if onTryPurchaseFunc, ok := vmw.GetFunction(`onTryPurchase`); ok {
153+
154+
sUser := GetActor(userId, 0)
155+
sRoom := GetRoom(sUser.GetRoomId())
156+
157+
res, err := runCallable(vmw, scriptItemTimeout, onTryPurchaseFunc,
158+
vmw.VM.ToValue(sUser),
159+
vmw.VM.ToValue(sItem),
160+
vmw.VM.ToValue(sRoom),
161+
)
162+
if err != nil {
163+
return false, fmt.Errorf("onTryPurchase(): %w", err)
164+
}
165+
166+
if boolVal, ok := res.Export().(bool); ok && !boolVal {
167+
return true, nil
168+
}
169+
170+
return false, nil
171+
}
172+
173+
return false, ErrEventNotFound
174+
}
175+
138176
func getItemVM(sItem *ScriptItem) (*VMWrapper, error) {
139177

140178
scriptId := strconv.Itoa(sItem.ItemId())

internal/scripting/room.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,49 @@ func TryRoomScriptEvent(eventName string, userId int, roomId int) (bool, error)
9494
return false, ErrEventNotFound
9595
}
9696

97+
func TryRoomTryEnterEvent(userId int, destRoomId int) (bool, error) {
98+
99+
vmw, err := getRoomVM(destRoomId)
100+
if err != nil {
101+
return false, err
102+
}
103+
104+
timestart := time.Now()
105+
defer func() {
106+
mudlog.Debug("TryRoomTryEnterEvent()", "destRoomId", destRoomId, "time", time.Since(timestart))
107+
}()
108+
109+
if onTryEnterFunc, ok := vmw.GetFunction(`onTryEnter`); ok {
110+
111+
// Set forced ansi tag wrappers
112+
userTextWrap.Set(`script-text`, ``, ``)
113+
roomTextWrap.Set(`script-text`, ``, ``)
114+
115+
sUser := GetActor(userId, 0)
116+
sRoom := GetRoom(destRoomId)
117+
118+
res, err := runCallable(vmw, scriptRoomTimeout, onTryEnterFunc,
119+
vmw.VM.ToValue(sUser),
120+
vmw.VM.ToValue(sRoom),
121+
)
122+
123+
userTextWrap.Reset()
124+
roomTextWrap.Reset()
125+
126+
if err != nil {
127+
return false, fmt.Errorf("onTryEnter(): %w", err)
128+
}
129+
130+
if boolVal, ok := res.Export().(bool); ok && !boolVal {
131+
return true, nil
132+
}
133+
134+
return false, nil
135+
}
136+
137+
return false, ErrEventNotFound
138+
}
139+
97140
func TryRoomTryExitEvent(exitName string, userId int, roomId int) (bool, error) {
98141

99142
vmw, err := getRoomVM(roomId)

0 commit comments

Comments
 (0)