Forest Logo
search
package_2

roblox-conversation

By @hurreman

Roblox

Mirrored from Wally

roblox-conversation

Data-driven, server-authoritative NPC dialogue: Slime Nebula's conversation framework, with Magic Neighborhood's tag binder and global text placeholders, as objects a game builds instead of module singletons.

# wally.toml
[dependencies]
Conversation = "hurreman/roblox-conversation@0.1.0"

How it fits together

  • Definitions are plain data. Nodes hold Text, a list of random variants or {Var} placeholders. Choices go Next to another node, Close, or run an opaque Action, gated by an opaque Condition / HideCondition, with Hide or Disable visibility and highlights.
  • The game registers what condition and action names mean. Conditions fail closed and actions are no-ops when missing; handler errors are caught.
  • The server is the trust boundary. Clients only send (conversationId, nodeId, choiceIndex), and the views they get never carry an Action or Condition. Every request is re-checked: an active session, the node they were shown, the choice exists, its gates still hold, and no request is already in progress.
  • The client controller never builds UI. It drives any Presenter (Init, Open, ShowNode, SetChoices, Close).
-- server
local Conversation = require(ReplicatedStorage.Packages.Conversation)
local service = Conversation.Service.new({
	send = function(player, action, payload) net:SendTo(player, "Conversation", action, payload) end,
	globals = { EventEgg = function() return Seasons.eggName("Event") end }, -- {EventEgg} in any text
	onStarted = function(player, id, source) progress:Fire("TalkTo", player, id) end,
})
service.Conditions:Register("HasCompletedQuest", function(player, args) return quests:Done(player, args.QuestId), "Finish the quest first" end)
service.Actions:Register("OpenVendor", function(player, args, ctx) vendors:Open(player, args.VendorId); ctx.Close("Vendor") end)
service:Listen({ choice = net:Function("ConversationChoice"), close = net:Event("ConversationClose") })
Conversation.Binder.new(service):Start(script.Definitions) -- id = module name; tag models "ConversationSource"

-- client
local client = Conversation.Client.new({
	presenter = DialoguePresenter,
	event = net:Event("Conversation"), choice = net:Function("ConversationChoice"), close = net:Event("ConversationClose"),
})
client:Start()
client:OnOpened(function(source) ... end) -- fires once per transition, like OnClosed

Binder attributes, set on the prompt or the model (the prompt's value wins):

  • ConversationId (required)
  • ConversationDisplayName
  • ConversationSourceKind
  • ConversationPortraitId
  • ConversationMaxDistance

Moving the games over

  • Slime Nebula:
    • ConversationService, its Registry, Session and both resolvers become a Service. ConditionResolver.Register(...) calls keep working on service.Conditions, with a dot or a colon.
    • ConversationInteractionBinder becomes Binder, or stays as the game's own binder calling service:Start.
    • ConversationClientController becomes Client. Opened / Closed become OnOpened / OnClosed.
    • The Integrations/ handlers register on the service and are unchanged.
  • Magic Neighborhood:
    • server/Conversation/* become Service plus Binder. Its GlobalVars become globals, and Definitions/ passes to Binder:Start.
    • The client controller becomes Client, with its own DialoguePresenter.

Two things found in Slime Nebula's code

Both are fixed in Slime Nebula since it moved onto this package (2026-10-26).

  • TalkTo progress never fires. ConversationService:Start fires TalkTo progress through ServerScriptService.Progress.ProgressManager, but the file never declares ServerScriptService. The call errors inside its pcall and is silently swallowed. (Glorpo's prompt fires its own TalkTo, so that one still works.) The package replaces this with the onStarted hook.
  • Its Conversation spec fails on its own code. Commit bf750bac (2026-08-28) added two highlight choices to the test definition without updating the visible-choice counts. Nothing runs the spec: RunAllTests has ENABLE_TESTS_IN_STUDIO = false, and the spec isn't in the Lune list.
    • tests/specs/SlimeNebula.spec.luau runs the spec against the package with those counts corrected, and the choices looked up by Index. Every other assertion is as written.

Developing

lune run tests/run    # specs on hurreman/luau-spec (DevPackages, else ../luau-spec)

The Lune specs cover:

  • Slime Nebula's spec, corrected;
  • view contents, placeholders and globals, onStarted, the Busy guard, ctx.Close, fail-closed conditions, registry validation, Listen and leaving;
  • lifecycle closes: source destroyed or removed, character died, a replaced session;
  • the client: presenter calls, choice round trips, one Closed per transition, closing past MaxDistance, rejected choices.

The binder hasn't been run: a ProximityPrompt only fires on real player input in Play mode.

Package Details

Install command (Click to copy)


Version

0.1.0

License

MIT

check_circle

Safe for commercial use

Automated license review — not legal advice.