Use built-in controls first
If a menu, dialog, sprite, or node property can express the behavior, prefer that.
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.ButtonsRun Function nodes use modules under:
ServerScriptService.VisualNovelCreatorExtensions.Server.FunctionsUse 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 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.
--!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) endendThe adapter exposes:
| Member | Use |
|---|---|
Id | Stable button ID from Contracts.ButtonIds. |
Role | Broad role from Contracts.ButtonRoles. |
Instance | The underlying GuiButton. |
Metadata | Button-specific metadata table. |
HasText | Whether 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.NewGameContracts.ButtonIds.RuntimeMenu.ContinueContracts.ButtonIds.RuntimeMenu.SettingsContracts.ButtonIds.RuntimeMenu.ChapterSelectContracts.ButtonIds.RuntimeMenu.SaveGameContracts.ButtonIds.RuntimeMenu.LoadGameContracts.ButtonIds.RuntimeMenu.LogContracts.ButtonIds.RuntimeMenu.ReturnContracts.ButtonIds.RuntimeMenu.CloseContracts.ButtonIds.RuntimeMenu.OpenMenuContracts.ButtonIds.SaveLoad.SaveSlotContracts.ButtonIds.SaveLoad.LoadSlotContracts.ButtonIds.SaveLoad.PreviousPageContracts.ButtonIds.SaveLoad.NextPageContracts.ButtonIds.Settings.AutoAdvanceContracts.ButtonIds.Settings.DeleteStoryDataContracts.ButtonIds.ChapterSelect.ChapterContracts.ButtonIds.Confirmation.ConfirmContracts.ButtonIds.Confirmation.CancelContracts.ButtonRoles.PrimaryContracts.ButtonRoles.SecondaryContracts.ButtonRoles.DangerContracts.ButtonRoles.NavigationContracts.ButtonRoles.SlotUse 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.StoryUse that generated Story module for IntelliSense instead of typing raw variable IDs by hand.
--!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, }endThe generated helper exposes:
| Member | Use |
|---|---|
Story.VariableIds.PlayerName | Stable 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:
return { ok = true } -- Continue normally.return { ok = false, message = "Could not finish the custom action.",}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.
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.