2026-07-11 00:09:58 +03:00
|
|
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
|
|
|
<!DOCTYPE muclient>
|
|
|
|
|
|
|
|
|
|
<muclient>
|
|
|
|
|
<plugin
|
|
|
|
|
name="GagManager"
|
2026-07-11 01:49:06 +03:00
|
|
|
author="The Cosmic Rage hosting team"
|
2026-07-11 00:09:58 +03:00
|
|
|
id="d2211ef7d54c4a7bd0cc9ab8"
|
|
|
|
|
save_state="y"
|
|
|
|
|
language="Lua"
|
|
|
|
|
purpose="VIPMud-like accessible gagging plugin."
|
|
|
|
|
requires="4.46"
|
|
|
|
|
version="1.1"
|
|
|
|
|
>
|
|
|
|
|
</plugin>
|
|
|
|
|
|
|
|
|
|
<!-- Aliases -->
|
|
|
|
|
<aliases>
|
|
|
|
|
<alias
|
|
|
|
|
match="gagconfig"
|
|
|
|
|
enabled="y"
|
|
|
|
|
script="gagconfig"
|
|
|
|
|
omit_from_output="y"
|
|
|
|
|
omit_from_log="y"
|
|
|
|
|
omit_from_command_history="y"
|
|
|
|
|
send_to="12"
|
|
|
|
|
sequence="100"
|
|
|
|
|
>
|
|
|
|
|
</alias>
|
|
|
|
|
</aliases>
|
|
|
|
|
|
|
|
|
|
<!-- Script -->
|
|
|
|
|
<script>
|
|
|
|
|
<![CDATA[
|
|
|
|
|
require "serialize"
|
|
|
|
|
|
|
|
|
|
dir = world.GetInfo(67)
|
|
|
|
|
|
|
|
|
|
gags = {}
|
|
|
|
|
subst_map = {}
|
|
|
|
|
|
|
|
|
|
function OnPluginInstall()
|
|
|
|
|
-- Register alt+f2 to open gag configuration menu (was alt+n, which
|
|
|
|
|
-- conflicted with direction_plugin.xml's south-west movement key)
|
|
|
|
|
Accelerator("alt+f2", "gagconfig")
|
|
|
|
|
|
|
|
|
|
load_gags()
|
|
|
|
|
register_all_gags()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function OnPluginClose()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function gags_file_path()
|
|
|
|
|
-- Store gags as a human-readable file next to the world file so users can copy/share them
|
|
|
|
|
return dir .. "gags.lua"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function save_gags()
|
|
|
|
|
-- Save to MUSHclient variable (for fast load)
|
|
|
|
|
local serialized = serialize.save_simple(gags)
|
|
|
|
|
SetVariable("gags_data", serialized)
|
|
|
|
|
-- Also write a plain file next to the world that users can copy/share
|
|
|
|
|
local f = io.open(gags_file_path(), "w")
|
|
|
|
|
if f then
|
|
|
|
|
f:write("-- Cosmic Rage Mush Soundpack - Gag Manager data\n")
|
|
|
|
|
f:write("-- Copy this file to share your gags with others.\n")
|
|
|
|
|
f:write("-- Place it next to your cosmic rage.MCL file to load it.\n\n")
|
|
|
|
|
f:write("return " .. serialized .. "\n")
|
|
|
|
|
f:close()
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function load_gags()
|
|
|
|
|
-- Try loading from file first (portable, shareable)
|
|
|
|
|
local f = io.open(gags_file_path(), "r")
|
|
|
|
|
if f then
|
|
|
|
|
local content = f:read("*all")
|
|
|
|
|
f:close()
|
|
|
|
|
local chunk, err = loadstring(content)
|
|
|
|
|
if chunk then
|
|
|
|
|
local ok, result = pcall(chunk)
|
|
|
|
|
if ok and type(result) == "table" then
|
|
|
|
|
gags = result
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if not gags or gags == {} then
|
|
|
|
|
-- Fall back to MUSHclient variable
|
|
|
|
|
local serialized = GetVariable("gags_data")
|
|
|
|
|
if serialized and serialized ~= "" then
|
|
|
|
|
local chunk, err = loadstring("return " .. serialized)
|
|
|
|
|
if chunk then
|
|
|
|
|
local ok, result = pcall(chunk)
|
|
|
|
|
if ok then gags = result end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
gags = gags or {}
|
|
|
|
|
|
|
|
|
|
-- Migrate old entries (from before each gag had a stable trigger name) and
|
|
|
|
|
-- make sure every gag has a unique trigger name we can add/delete directly.
|
|
|
|
|
local next_id = 1
|
|
|
|
|
for _, gag in ipairs(gags) do
|
|
|
|
|
if gag.name then
|
|
|
|
|
local n = tonumber(gag.name:match("gag_(%d+)"))
|
|
|
|
|
if n and n >= next_id then next_id = n + 1 end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
for _, gag in ipairs(gags) do
|
|
|
|
|
if not gag.name then
|
|
|
|
|
gag.name = "gag_" .. tostring(next_id)
|
|
|
|
|
next_id = next_id + 1
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Adds (or replaces) the MUSHclient trigger for a single gag entry.
|
|
|
|
|
function add_trigger_for_gag(gag)
|
|
|
|
|
-- Enabled=1, KeepEvaluating=8, Replace=1024, Temporary=16384
|
|
|
|
|
local flags = 1 + 8 + 1024 + 16384
|
|
|
|
|
local script_name = ""
|
|
|
|
|
|
|
|
|
|
if gag.method == "all" then
|
|
|
|
|
flags = flags + 4 -- OmitFromOutput
|
|
|
|
|
elseif gag.method == "voice" then
|
2026-07-11 00:46:56 +03:00
|
|
|
-- "Voice only" was removed from the creation menu (it produced the
|
|
|
|
|
-- exact same trigger flags as "all" anyway - the distinction was
|
|
|
|
|
-- never actually implemented). Kept here so any gag saved with this
|
|
|
|
|
-- method before the change still loads and behaves the same.
|
|
|
|
|
flags = flags + 4 -- OmitFromOutput
|
2026-07-11 00:09:58 +03:00
|
|
|
elseif gag.method == "substitute" then
|
|
|
|
|
flags = flags + 4 -- OmitFromOutput: hide the original line
|
|
|
|
|
script_name = "substitute_trigger"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- No regex needed - MUSHclient's normal trigger matching already treats
|
|
|
|
|
-- "*" as a wildcard, so the gag pattern can be used as-is.
|
|
|
|
|
local res = AddTrigger(gag.name, gag.line, "", flags, -1, 0, "", script_name)
|
|
|
|
|
if res == 0 then
|
|
|
|
|
SetTriggerOption(gag.name, "group", gag.file)
|
|
|
|
|
if gag.method == "substitute" then
|
|
|
|
|
SetTriggerOption(gag.name, "send_to", "14")
|
|
|
|
|
subst_map[gag.name] = gag.subst
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return res
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function register_all_gags()
|
|
|
|
|
subst_map = {}
|
|
|
|
|
|
|
|
|
|
for _, gag in ipairs(gags) do
|
|
|
|
|
add_trigger_for_gag(gag)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Apply enabled/disabled states to trigger groups
|
|
|
|
|
local groups = {"ActivityGags", "ShipGags", "VehicleGags", "MiscGags"}
|
|
|
|
|
for _, g in ipairs(groups) do
|
|
|
|
|
local state = GetVariable(g .. "_state") or "enabled"
|
|
|
|
|
EnableTriggerGroup(g, state == "enabled")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- --- TRIGGER CALLBACKS ---
|
|
|
|
|
|
|
|
|
|
function substitute_trigger(name, line, wildcards)
|
|
|
|
|
local subst = subst_map[name]
|
|
|
|
|
if subst then
|
|
|
|
|
local res = subst
|
|
|
|
|
for i = 1, #wildcards do
|
|
|
|
|
res = res:gsub("%%" .. tostring(i), wildcards[i])
|
|
|
|
|
end
|
|
|
|
|
res = res:gsub("%%0", line)
|
|
|
|
|
Note(res) -- original line is already omitted; print the replacement
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- --- GAG CONFIGURATION DIALOGS ---
|
|
|
|
|
|
|
|
|
|
local function sleep(s)
|
|
|
|
|
local t = os.clock()
|
|
|
|
|
while os.clock() - t < s do end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function toggle_group(group_name)
|
|
|
|
|
sleep(0.2)
|
|
|
|
|
local current = GetVariable(group_name .. "_state") or "enabled"
|
|
|
|
|
local new_state = "disabled"
|
|
|
|
|
if current == "disabled" then
|
|
|
|
|
new_state = "enabled"
|
|
|
|
|
end
|
|
|
|
|
SetVariable(group_name .. "_state", new_state)
|
|
|
|
|
EnableTriggerGroup(group_name, new_state == "enabled")
|
|
|
|
|
Execute("tts_interrupt " .. group_name .. " set to " .. new_state)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function delete_gag()
|
|
|
|
|
sleep(0.2)
|
|
|
|
|
if #gags == 0 then
|
|
|
|
|
Execute("tts_interrupt No gags defined yet.")
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local choices = {}
|
|
|
|
|
for i, gag in ipairs(gags) do
|
|
|
|
|
table.insert(choices, string.format("[%s] %s (%s)", gag.file, gag.line, gag.method))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local result = utils.choose("Select a gag to delete:", "Delete Gag", choices)
|
|
|
|
|
if result then
|
|
|
|
|
local idx
|
|
|
|
|
if type(result) == "number" then
|
|
|
|
|
idx = result
|
|
|
|
|
else
|
|
|
|
|
for i, v in ipairs(choices) do
|
|
|
|
|
if v == result then idx = i; break end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if idx then
|
|
|
|
|
local gag = gags[idx]
|
|
|
|
|
DeleteTrigger(gag.name)
|
|
|
|
|
subst_map[gag.name] = nil
|
|
|
|
|
table.remove(gags, idx)
|
|
|
|
|
save_gags()
|
|
|
|
|
Execute("tts_interrupt Gag deleted: " .. gag.line)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function next_gag_name()
|
|
|
|
|
local max_id = 0
|
|
|
|
|
for _, gag in ipairs(gags) do
|
|
|
|
|
local n = tonumber(gag.name:match("gag_(%d+)"))
|
|
|
|
|
if n and n > max_id then max_id = n end
|
|
|
|
|
end
|
|
|
|
|
return "gag_" .. tostring(max_id + 1)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function addnewgag()
|
|
|
|
|
sleep(0.2)
|
|
|
|
|
-- Prompt 1: Line to gag
|
|
|
|
|
local gag_line = utils.inputbox(
|
|
|
|
|
"Enter the exact text you want to gag, as it appears in the game.\n" ..
|
|
|
|
|
"\n" ..
|
|
|
|
|
"You can use * (asterisk) as a wildcard to match any text.\n" ..
|
|
|
|
|
"Examples:\n" ..
|
|
|
|
|
" warrior hugs you -- gags that exact line\n" ..
|
|
|
|
|
" * hugs you -- gags anyone hugging you\n" ..
|
|
|
|
|
" You receive * -- gags any 'You receive ...' line",
|
|
|
|
|
"Step 1 of 3: Enter gag pattern", "")
|
|
|
|
|
if not gag_line or gag_line == "" then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
sleep(0.2)
|
|
|
|
|
-- Prompt 2: Gag method
|
|
|
|
|
local m_choices = {
|
|
|
|
|
"Full gag - Hide completely from screen and speech",
|
|
|
|
|
"Substitute - Replace the matched line with different text"
|
|
|
|
|
}
|
|
|
|
|
local m_res = utils.choose(
|
|
|
|
|
"How should this line be gagged?\n" ..
|
|
|
|
|
"\n" ..
|
|
|
|
|
"Full gag: The line is completely removed from both the screen and speech. Good for things you never want to see.\n" ..
|
|
|
|
|
"Substitute: Replaces the matched line with your own custom text.",
|
|
|
|
|
"Step 2 of 3: Choose gag method", m_choices)
|
|
|
|
|
if not m_res then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
if type(m_res) == "number" then
|
|
|
|
|
m_res = m_choices[m_res]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local gag_method = ""
|
|
|
|
|
local gag_sub = ""
|
2026-07-11 00:46:56 +03:00
|
|
|
if m_res == "Full gag - Hide completely from screen and speech" then
|
2026-07-11 00:09:58 +03:00
|
|
|
gag_method = "all"
|
|
|
|
|
elseif m_res == "Substitute - Replace the matched line with different text" then
|
|
|
|
|
gag_method = "substitute"
|
|
|
|
|
sleep(0.2)
|
|
|
|
|
gag_sub = utils.inputbox(
|
|
|
|
|
"Enter the replacement text that will be shown instead of the matched line.\n" ..
|
|
|
|
|
"\n" ..
|
|
|
|
|
"You can use placeholders:\n" ..
|
|
|
|
|
" %0 - The entire original matched line\n" ..
|
|
|
|
|
" %1 - The first wildcard * match\n" ..
|
|
|
|
|
" %2 - The second wildcard * match, and so on\n" ..
|
|
|
|
|
"\n" ..
|
|
|
|
|
"Example: If your gag pattern is '* hugs you'\n" ..
|
|
|
|
|
"and you type '%1 pokes you', it shows 'warrior pokes you' instead.",
|
|
|
|
|
"Substitution text", "")
|
|
|
|
|
if not gag_sub or gag_sub == "" then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
sleep(0.2)
|
|
|
|
|
-- Prompt 3: Category
|
|
|
|
|
local f_choices = {
|
|
|
|
|
"Activity Gags - Mining, crafting, harvesting, general activities",
|
|
|
|
|
"Ship Gags - Starship combat, travel, ship systems",
|
|
|
|
|
"Miscellaneous Gags - Everything else that does not fit above",
|
|
|
|
|
"Vehicle Gags - Ground vehicles, speeders, walkers"
|
|
|
|
|
}
|
|
|
|
|
local f_res = utils.choose(
|
|
|
|
|
"Which category should this gag be stored in?\n" ..
|
|
|
|
|
"\n" ..
|
|
|
|
|
"Categories let you toggle entire groups of gags on or off at once from the main menu.\n" ..
|
|
|
|
|
"For example, you can disable all Ship Gags when not flying, and re-enable them when you board a ship.",
|
|
|
|
|
"Step 3 of 3: Choose category", f_choices)
|
|
|
|
|
if not f_res then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
if type(f_res) == "number" then
|
|
|
|
|
f_res = f_choices[f_res]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local gag_file = ""
|
|
|
|
|
if f_res == "Activity Gags - Mining, crafting, harvesting, general activities" then
|
|
|
|
|
gag_file = "ActivityGags"
|
|
|
|
|
elseif f_res == "Ship Gags - Starship combat, travel, ship systems" then
|
|
|
|
|
gag_file = "ShipGags"
|
|
|
|
|
elseif f_res == "Miscellaneous Gags - Everything else that does not fit above" then
|
|
|
|
|
gag_file = "MiscGags"
|
|
|
|
|
elseif f_res == "Vehicle Gags - Ground vehicles, speeders, walkers" then
|
|
|
|
|
gag_file = "VehicleGags"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local gag = {
|
|
|
|
|
name = next_gag_name(),
|
|
|
|
|
line = gag_line,
|
|
|
|
|
method = gag_method,
|
|
|
|
|
subst = gag_sub,
|
|
|
|
|
file = gag_file
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
local res = add_trigger_for_gag(gag)
|
|
|
|
|
if res ~= 0 then
|
|
|
|
|
Execute("tts_interrupt Could not create that gag, error " .. tostring(res))
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
table.insert(gags, gag)
|
|
|
|
|
save_gags()
|
|
|
|
|
Execute("tts_interrupt Gag added successfully!")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function gagconfig()
|
|
|
|
|
repeat
|
|
|
|
|
sleep(0.2)
|
|
|
|
|
local choices = {
|
|
|
|
|
"Create a new gag...",
|
|
|
|
|
"Delete an existing gag...",
|
|
|
|
|
"Toggle Activity-related gags (" .. (GetVariable("ActivityGags_state") or "enabled") .. ")",
|
|
|
|
|
"Toggle Ship-related gags (" .. (GetVariable("ShipGags_state") or "enabled") .. ")",
|
|
|
|
|
"Toggle Vehicle-related gags (" .. (GetVariable("VehicleGags_state") or "enabled") .. ")",
|
|
|
|
|
"Toggle Misc Gags (" .. (GetVariable("MiscGags_state") or "enabled") .. ")"
|
|
|
|
|
}
|
|
|
|
|
local result = utils.choose("Choose a gag option to edit or toggle:", "Gag Configuration (Alt+F2)", choices)
|
|
|
|
|
if not result then break end
|
|
|
|
|
|
|
|
|
|
-- utils.choose may return either an index (number) or the string itself
|
|
|
|
|
local idx
|
|
|
|
|
if type(result) == "number" then
|
|
|
|
|
idx = result
|
|
|
|
|
else
|
|
|
|
|
for i, v in ipairs(choices) do
|
|
|
|
|
if v == result then idx = i; break end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if idx == 1 then
|
|
|
|
|
addnewgag()
|
|
|
|
|
elseif idx == 2 then
|
|
|
|
|
delete_gag()
|
|
|
|
|
elseif idx == 3 then
|
|
|
|
|
toggle_group("ActivityGags")
|
|
|
|
|
elseif idx == 4 then
|
|
|
|
|
toggle_group("ShipGags")
|
|
|
|
|
elseif idx == 5 then
|
|
|
|
|
toggle_group("VehicleGags")
|
|
|
|
|
elseif idx == 6 then
|
|
|
|
|
toggle_group("MiscGags")
|
|
|
|
|
end
|
|
|
|
|
until false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
]]>
|
|
|
|
|
</script>
|
|
|
|
|
</muclient>
|