Client content
Ship layouts, style sheets, models and sounds to every player.
The build packs the files under your project's content/ into the mod's own
addon, which every player's game mounts. The addon's name, SLUG-DIGEST, comes
from the mod's slug and a digest of the content. Write {addon} wherever the
addon's name goes, and the build or the host fills it in:
<include src="file://{resources}/scripts/{addon}/scoreboard.js" />| Kind | Put it in | Refer to it as |
|---|---|---|
| Layouts, scripts | content/layout/, content/scripts/, shaped like the game's panorama/ | file://{resources}/layout/{addon}/board.xml |
| Style sheets | content/styles/ | file://{resources}/styles/{addon}/scoreboard.css |
Images, compiled as .vtex_c | content/images/ | file://{images}/{addon}/mark.png |
| Compiled models, materials, particles | content/models/, content/materials/, content/particles/ | models/{addon}/pawn.vmdl |
content/layout/scoreboard.xmlbuilds tobuild/panorama/layout/SLUG-DIGEST/scoreboard.vxml_c, and models tomodels/SLUG-DIGEST/and so on.- A release that changes only code has the same addon and paths as the one before it. A game that mounted the previous release still finds the new one's resources.
- Keep sources outside
content/: the build refuses a source the game cannot load, such as a.vmdlor a Hammer map's.vmap. The publishing check refuses client files outside the addon's directories. - The build does not rewrite a path inside a compiled resource. Point such a path at the game's own resources, or at the addon's name for the version you build.
Draw a layout in the world
Entity key values take {addon} too, so a world panel can draw the mod's own layout:
create('point_clientui_world_panel', {
position,
keys: {
enabled: true,
width: 48,
height: 24,
panel_dpi: 32,
opaque: false,
orientation: 1,
dialog_layout_name: 'file://{resources}/layout/{addon}/board.xml',
},
})local keys: entities.CPointClientUIWorldPanelKeys = {
enabled = true,
width = 48,
height = 24,
panel_dpi = 32,
opaque = false,
orientation = 1,
dialog_layout_name = "file://{resources}/layout/{addon}/board.xml",
}
entities.create("point_clientui_world_panel", { position = position, keys = keys })keys: entities.CPointClientUIWorldPanelKeys = {
"enabled": True,
"width": 48,
"height": 24,
"panel_dpi": 32,
"opaque": False,
"orientation": 1,
"dialog_layout_name": "file://{resources}/layout/{addon}/board.xml",
}
entities.create("point_clientui_world_panel", position, keys=keys)keys := entity.CPointClientUIWorldPanelKeys{
Width: new(float32(48)),
Height: new(float32(24)),
PanelDpi: new(float32(32)),
Opaque: new(false),
Orientation: new(int32(1)),
}
keys.Enabled = new(true)
keys.DialogLayoutName = new("file://{resources}/layout/{addon}/board.xml")
_, _ = entity.PointClientuiWorldPanel.Create(&mod.EntityOptions{Position: position}, &keys)A panel that the map placed got its layout path before the addon's name
existed. Point it at your mod's layout with writeField, which also fills in
{addon}. Mods start before the map's entities exist, so do it as each world
loads. Each write sends
connected players a full update that stalls their game for a moment. Write a
layout only when it has changed:
onWorld(() => {
for (const panel of findEntities('point_clientui_world_panel')) {
writeField(
panel,
'CBaseClientUIEntity',
'm_DialogXMLName',
'string',
'file://{resources}/layout/{addon}/board.xml',
)
}
})modlock.onWorld(function()
for _, panel in modlock.findEntities("point_clientui_world_panel") do
modlock.writeField(
panel,
"CBaseClientUIEntity",
"m_DialogXMLName",
"string",
"file://{resources}/layout/{addon}/board.xml"
)
end
end)@modlock.on_world
def point_panels(map_name: str) -> None:
for panel in modlock.find_entities("point_clientui_world_panel"):
modlock.write_field(
panel,
"CBaseClientUIEntity",
"m_DialogXMLName",
"string",
"file://{resources}/layout/{addon}/board.xml",
)mod.OnWorld(func(mapName string) {
panels, _ := mod.FindEntities("point_clientui_world_panel")
for _, panel := range panels {
_ = mod.WriteField(panel, "CBaseClientUIEntity", "m_DialogXMLName", mod.FieldTypeString,
"file://{resources}/layout/{addon}/board.xml")
}
})world-panel
is the whole mod.
inventory
draws a scripted bag through the layout element: the
player drags items with the freed cursor, the script sends each move as a
console command, and the mod answers with the new slots. Its style sheet
styles the frame and slots, and a class the mod adds fades the bag in.
A layout that loads a script or handles an event runs code in each player's game, so a publishing service may hold it for creators it trusts. A layout that only draws runs no code in a player's game, so a service has no such reason to hold it.
Models and particles
Put {addon} in every path you pass to precache, createModel, createParticle and entity key values:
precache({ resources: ['models/{addon}/pawn.vmdl'] })
createModel({ resource: 'models/{addon}/pawn.vmdl', position })modlock.precache({ resources = { "models/{addon}/pawn.vmdl" } })
modlock.createModel({ resource = "models/{addon}/pawn.vmdl", position = position })modlock.precache(modlock.PrecacheOptions(resources=["models/{addon}/pawn.vmdl"]))
modlock.create_model(modlock.ModelOptions(resource="models/{addon}/pawn.vmdl", position=position))_ = mod.Precache(&mod.PrecacheOptions{Resources: []string{"models/{addon}/pawn.vmdl"}})
_, _ = mod.CreateModel(&mod.ModelOptions{Resource: "models/{addon}/pawn.vmdl", Position: position})Mounting
| Command | Mounts |
|---|---|
modlock dev, modlock play | Each mod's content, before the server starts, so the server and the player's game both load it. Each rebuild adds changed content under the addon's new name, and the server and game load it the next time the mod asks, without a restart. |
modlock join | The host's content, before the guest's game starts. A guest keeps the content it joined with. |