Skip to Content
Modlock

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" />
KindPut it inRefer to it as
Layouts, scriptscontent/layout/, content/scripts/, shaped like the game's panorama/file://{resources}/layout/{addon}/board.xml
Style sheetscontent/styles/file://{resources}/styles/{addon}/scoreboard.css
Images, compiled as .vtex_ccontent/images/file://{images}/{addon}/mark.png
Compiled models, materials, particlescontent/models/, content/materials/, content/particles/models/{addon}/pawn.vmdl
  • content/layout/scoreboard.xml builds to build/panorama/layout/SLUG-DIGEST/scoreboard.vxml_c, and models to models/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 .vmdl or 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

CommandMounts
modlock dev, modlock playEach 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 joinThe host's content, before the guest's game starts. A guest keeps the content it joined with.

On this page