Skip to content

Extensions

Extensions are for advanced projects that need code without modifying the core runtime.

Most projects should start with Properties fields and built-in nodes. Extensions are for the cases where your story needs behavior that would be too specific to build into the plugin for everyone.

This page assumes you are comfortable editing a Luau ModuleScript and already have a project you can test in Studio play mode. If a built-in node or Properties field can express the behavior, skip extensions for now. Use Testing Runtime when you are ready to verify extension behavior in play mode.

VNC exposes a small, supported shape for your extension modules. Instead of editing the runtime directly, you point the story to modules that match that shape.

The current supported seams are runtime button behavior and server functions triggered by Run Function nodes.

Button extensions use this conventional module path:

ReplicatedStorage.VisualNovelCreatorExtensions.Client.Buttons

Run Function nodes use modules under:

ServerScriptService.VisualNovelCreatorExtensions.Server.Functions

Use Runtime Menu > Button Extensions > Create Module or a Run Function node’s Function > Create Module action to scaffold the right shape, then enable extensions from plugin settings when the project intentionally uses trusted code.

Use built-in controls first

If a menu, dialog, sprite, or node property can express the behavior, prefer that.

Use extensions for project logic

If the behavior belongs to your game or story style, keep it in a small extension module.

Avoid runtime edits

Changing core runtime scripts directly makes updates harder and creates merge work later.

Extensions are disabled by default. Only enable them when the project intentionally uses trusted code.

VNC can check conventional module shapes without running your extension code from the editor. Runtime execution still happens in-game, so test extensions in a play session.

Button extensions run on the client and should stay presentation-focused. Use them for runtime button labels, hover/press behavior, enabled state, scale, and transparency. Do not use them as gameplay authority.

Run Function modules run on the server. Treat them like gameplay code: keep them small, validate any Roblox instances or external data they touch, and prefer the provided context variable store for authored story variables.

Runtime lifecycle callbacks, client-side story scripting, and advanced sprite animation extension APIs are not part of the supported extension shape yet.

The supported shape makes customization safer:

  • The core runtime remains updateable.
  • Extension modules have predictable button inputs.
  • Run Function modules get typed helpers for project variables.
  • Advanced creators can get better autocomplete and type guidance.
  • You do not need every possible styling or behavior option exposed in the plugin UI.

The Buttons module must return one function. It receives a ButtonAdapter and may return a cleanup function.

This example matches the module shape created by Runtime Menu > Button Extensions > Create Module. It uses the public button IDs, then adds a small hover scale to the New Game button.

VisualNovelCreatorExtensions/Client/Buttons/NewGame.luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Runtime = ReplicatedStorage:WaitForChild("VisualNovelCreatorRuntime")
local Shared = Runtime:WaitForChild("Shared")
local Contracts = require(Shared:WaitForChild("ExtensionContracts"))
local BUTTON_HOVER_SCALE = 1.04
return function(button: Contracts.ButtonAdapter): (() -> ())?
-- VNC calls this once for each runtime button.
if button.Id ~= Contracts.ButtonIds.RuntimeMenu.NewGame then
return nil
end
local disconnect = button:OnHoverChanged(function(isHovered)
button:SetScale(if isHovered then BUTTON_HOVER_SCALE else 1)
end)
return function()
disconnect()
button:SetScale(1)
end
end

The adapter exposes:

MemberUse
IdStable button ID from Contracts.ButtonIds.
RoleBroad role from Contracts.ButtonRoles.
InstanceThe underlying GuiButton.
MetadataButton-specific metadata table.
HasTextWhether this adapter wraps a TextButton.
SetText(text)Change visible button text when HasText is true. Returns whether text was applied.
SetEnabled(enabled)Enable or disable interaction.
SetScale(scale)Apply presentation scale.
SetTransparency(transparency)Apply presentation transparency.
OnHoverChanged(callback)Subscribe to hover state. Returns cleanup.
OnPressedChanged(callback)Subscribe to pressed state. Returns cleanup.
OnActivated(callback)Subscribe to activation. Returns cleanup.
  • Contracts.ButtonIds.RuntimeMenu.NewGame
  • Contracts.ButtonIds.RuntimeMenu.Continue
  • Contracts.ButtonIds.RuntimeMenu.Settings
  • Contracts.ButtonIds.RuntimeMenu.ChapterSelect
  • Contracts.ButtonIds.RuntimeMenu.SaveGame
  • Contracts.ButtonIds.RuntimeMenu.LoadGame
  • Contracts.ButtonIds.RuntimeMenu.Log
  • Contracts.ButtonIds.RuntimeMenu.Return
  • Contracts.ButtonIds.RuntimeMenu.Close
  • Contracts.ButtonIds.RuntimeMenu.OpenMenu

Use these constants instead of hardcoded strings. That keeps your extension aligned when button text or layout changes.

The path constants are also exposed through Contracts.Paths, including ButtonsModulePath, FunctionsFolderPath, and StoryModulePath.

A Run Function module must return one server function. It receives a ServerFunctionContext and may return a FunctionResult.

When VNC scaffolds or compiles the project, it also refreshes a generated helper module:

ServerScriptService.VisualNovelCreatorExtensions.Server.Generated.Story

Use that generated Story module for IntelliSense instead of typing raw variable IDs by hand.

VisualNovelCreatorExtensions/Server/Functions/SetDemoValues.luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Runtime = ReplicatedStorage:WaitForChild("VisualNovelCreatorRuntime")
local Shared = Runtime:WaitForChild("Shared")
local Contracts = require(Shared:WaitForChild("ExtensionContracts"))
local Story = require(script.Parent.Parent.Generated.Story)
return function(context: Contracts.ServerFunctionContext): Contracts.FunctionResult?
Story.Variables.PlayerName.set(context, "Bobo")
Story.Variables.FavoriteColor.set(context, "blue")
return {
ok = true,
}
end

The generated helper exposes:

MemberUse
Story.VariableIds.PlayerNameStable raw variable ID for advanced cases.
Story.Variables.PlayerName.get(context)Reads a typed variable value.
Story.Variables.PlayerName.set(context, value)Writes a typed variable value.
Story.VariablesById.playerName.get(context)Rename-stable access derived from the stable variable ID.

Return values control how the story proceeds:

Continue normally
return { ok = true } -- Continue normally.
Stop with a message
return {
ok = false,
message = "Could not finish the custom action.",
}
Stop the story
return {
ok = true,
stopStory = true,
}

Missing modules, load errors, modules that do not return a function, thrown errors, and { ok = false } all follow the node’s On Error setting. Choose Continue for a recoverable failure or Stop Story when proceeding would be unsafe.

  1. Confirm the behavior cannot be expressed with existing nodes or properties.
  2. Enable extensions in plugin settings.
  3. Create the extension module using the supported module shape.
  4. Compile once after changing variables so generated helpers refresh.
  5. Keep the module focused on one behavior.
  6. Test the behavior in a Studio play session, not only in preview.

Keep each extension narrow: button modules should stay presentation-oriented, while Run Function modules should contain only the trusted server behavior needed at that story point.

Extensions are part of the beta surface. Check Beta Limitations before depending on them for a release-critical workflow.