Fix accessible output window, revert Tab/Ctrl+Space, sync readme keys

- output_functions.xml: merge NotepadSmartScroll's Win32/Alien-based
  smart-scroll logic in directly (was a separate plugin). Fixed the
  caret-restore bug: gating the restore on scroll position was wrong
  since the notepad tracks near the bottom during normal reading; now
  gated on whether the caret was literally at the end of the old text.
  Added a jump-to-end-on-focus toggle (Ctrl+Alt+J) with a 0.2s polling
  timer to catch focus landing on the output notepad by any means.
- Reverted the Tab/Ctrl+Space rebinding - removed switch_to_output and
  trigger_word_completion along with their accelerators.
- cosmic rage.MCL: dropped the dangling NotepadSmartScroll.xml include
  (file no longer exists, was reintroduced by an auto-save).
- readme.md: documented the actual current CosmicRage.xml keys (several
  had drifted out of sync - Alt+F12 is now stop-sounds not restart,
  Alt+Delete/Ctrl+S/Ctrl+F1-3/Alt+F3/Alt+Shift+H/F11 were undocumented)
  and added the output_functions.xml accessible-output keybindings,
  which weren't documented at all before.
This commit is contained in:
OlegTheSnowman
2026-07-11 00:02:28 +03:00
parent 21fa3e0d57
commit 09785a9080
8 changed files with 719 additions and 155 deletions

BIN
cosmic rage/alien/core.dll Normal file

Binary file not shown.

Binary file not shown.

245
cosmic rage/lua/alien.lua Normal file
View File

@@ -0,0 +1,245 @@
local core = require "alien.core"
local io = require "io"
local pairs, ipairs = pairs, ipairs
local setmetatable = setmetatable
local error = error
local pcall = pcall
local type = type
local rawset = rawset
local unpack = unpack
local math = math
local print = print
module "alien"
loaded = {}
local load_library, find_library = {}, {}
local function find_library_helper(libname, opt)
local expr = '/[^() ]*lib' .. libname .. '\\.so[^() ]*'
local cmd = '/sbin/ldconfig ' .. opt ..
' 2>/dev/null | egrep -o "' .. expr .. '"'
local pipe = io.popen(cmd)
if pipe then
local res = pipe:read()
pipe:close()
return res and res:match("([^%s]*)")
end
return nil
end
function find_library.linux(libname)
return find_library_helper(libname, "-p")
end
function find_library.bsd(libname)
return find_library_helper(libname, "-r")
end
function find_library.darwin(libname)
local ok, lib = pcall(core.load, libname .. ".dylib")
if ok then return lib end
ok, lib = pcall(core.load, libname .. ".framework/" .. libname)
if ok then return lib end
return nil
end
local function load_library_helper(libname, libext)
if libname:match("/") or libname:match("%" .. libext) then
return core.load(libname)
else
local ok, lib = pcall(core.load, "lib" .. libname .. libext)
if not ok then
ok, lib = pcall(core.load, "./lib" .. libname .. libext)
if not ok then
local name = find_library[core.platform](libname)
if name then
lib = core.load(name)
else
error("library " .. libname .. " not found")
end
end
end
return lib
end
end
function load_library.linux(libname)
return load_library_helper(libname, ".so")
end
load_library.bsd = load_library.linux
function load_library.darwin(libname)
return load_library_helper(libname, ".dylib")
end
setmetatable(load_library, { __index = function (t, plat)
return core.load
end } )
function load_library.windows(libname)
return core.load(libname)
end
setmetatable(loaded, { __index = function (t, libname)
local lib =
load_library[core.platform](libname)
t[libname] = lib
return lib
end })
setmetatable(_M, { __index = loaded })
for name, f in pairs(core) do
_M[name] = f
end
function load(libname)
return loaded[libname]
end
function callback(f, ...)
local cb = core.callback(f)
cb.types(cb, ...)
return cb
end
local array_methods = {}
local function array_next(arr, i)
if i < arr.length then
return i + 1, arr[i + 1]
else
return nil
end
end
function array_methods:ipairs()
return array_next, self, 0
end
local function array_get(arr, key)
if type(key) == "number" then
if key < 1 or key > arr.length then
error("array access out of bounds")
end
local offset = (key - 1) * arr.size + 1
return arr.buffer:get(offset, arr.type)
else
return array_methods[key]
end
end
local function array_set(arr, key, val)
if type(key) == "number" then
if key < 1 or key > arr.length then
error("array access out of bounds")
end
local offset = (key - 1) * arr.size + 1
arr.buffer:set(offset, val, arr.type)
if type(val) == "string" or type(val) == "userdata" then
arr.pinned[key] = val
end
else
rawset(arr, key, val)
end
end
function array(t, length, init)
local ok, size = pcall(core.sizeof, t)
if not ok then
error("type " .. t .. " does not exist")
end
if type(length) == "table" then
init = length
length = #length
end
local arr = { type = t, length = length, size = size, pinned = {} }
setmetatable(arr, { __index = array_get, __newindex = array_set })
if type(init) == "userdata" then
arr.buffer = init
else
arr.buffer = core.buffer(size * length)
if type(init) == "table" then
for i = 1, length do
arr[i] = init[i]
end
end
end
return arr
end
local function struct_new(s_proto, ptr)
local buf = core.buffer(ptr or s_proto.size)
local function struct_get(_, key)
if s_proto.offsets[key] then
return buf:get(s_proto.offsets[key] + 1, s_proto.types[key])
else
error("field " .. key .. " does not exist")
end
end
local function struct_set(_, key, val)
if s_proto.offsets[key] then
buf:set(s_proto.offsets[key] + 1, val, s_proto.types[key])
else
error("field " .. key .. " does not exist")
end
end
return setmetatable({}, { __index = struct_get, __newindex = struct_set,
__call = function () return buf end })
end
local function struct_byval(s_proto)
local types = {}
local size = s_proto.size
for i = 0, size - 1, 4 do
if size - i == 1 then
types[#types + 1] = "char"
elseif size - i == 2 then
types[#types + 1] = "short"
else
types[#types + 1] = "int"
end
end
return unpack(types)
end
function defstruct(t)
local off = 0
local names, offsets, types = {}, {}, {}
for _, field in ipairs(t) do
local name, type = field[1], field[2]
names[#names + 1] = name
off = math.ceil(off / core.align(type)) * core.align(type)
offsets[name] = off
types[name] = type
off = off + core.sizeof(type)
end
return { names = names, offsets = offsets, types = types, size = off, new = struct_new,
byval = struct_byval }
end
function byval(buf)
if buf.size then
local size = buf.size
local types = { "char", "short"}
local vals = {}
for i = 1, size, 4 do
if size - i == 0 then
vals[#vals + 1] = buf:get(i, "char")
elseif size - i == 1 then
vals[#vals + 1] = buf:get(i, "short")
else
vals[#vals + 1] = buf:get(i, "int")
end
end
return unpack(vals)
else
error("this type of buffer can't be passed by value")
end
end

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<!-- Saved on Sunday, May 25, 2025, 1:55 PM -->
<!-- Saved on Friday, July 10, 2026, 11:31 PM -->
<!-- MuClient version 5.06 -->
<!-- Written by Nick Gammon -->
<!-- Home Page: http://www.mushclient.com/ -->
@@ -8,13 +8,13 @@
<world
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
auto_say_override_prefix="-"
auto_say_string="say "
chat_name="Name-not-set"
command_stack_character=";"
id="d2211ef7d54c4a7bd0cc9ab7"
id="7c5e6f4701b5e6f94dbeb6b2"
input_font_name="Fixedsys"
mapping_failure="Alas, you cannot go that way."
name="Cosmic Rage"
@@ -111,7 +111,7 @@
<triggers
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
<trigger
enabled="y"
@@ -131,7 +131,7 @@
<aliases
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
</aliases>
@@ -140,7 +140,7 @@
<timers
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
</timers>
@@ -149,7 +149,7 @@
<macros
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
<macro name="up" type="send_now" >
@@ -243,7 +243,7 @@
<variables
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
</variables>
@@ -252,7 +252,7 @@
<colours
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
<ansi>
@@ -309,7 +309,7 @@
<keypad
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
<key name="0" >
@@ -423,7 +423,7 @@
<printing
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
<ansi>
@@ -448,7 +448,6 @@
</printing>
<!-- plugins -->
<include name="LuaAudio.xml" plugin="y" />
<include name="CaptureMudOutput.xml" plugin="y" />
<include name="Copy_Output.xml" plugin="y" />
<include name="direction_plugin.xml" plugin="y" />
@@ -457,5 +456,7 @@
<include name="Omit_Blank_Lines.xml" plugin="y" />
<include name="Reconnecter.xml" plugin="y" />
<include name="Repeat_Command.xml" plugin="y" />
<include name="GagManager.xml" plugin="y" />
<include name="LuaAudio.xml" plugin="y" />
<include name="CosmicRage.xml" plugin="y" />
</muclient>

View File

@@ -25,7 +25,7 @@ sequence="10000"
for i = 1, 9 do
Accelerator("alt + " .. i, "@buffer " .. i)
end
Accelerator("alt + 0", "@buffer 10") -- 10 is a special case
Accelerator("alt + 0", "@buffer 10")
Accelerator("alt + right", "@buffer forward")
Accelerator("alt + left", "@buffer backward")
Accelerator("alt + down", "@buffer down")
@@ -34,41 +34,64 @@ Accelerator("alt + pagedown", "@buffer scrolldown")
Accelerator("alt + pageup", "@buffer scrollup")
Accelerator("alt + end", "@buffer bottom")
Accelerator("alt + home", "@buffer top")
Accelerator("alt+shift+delete", "@buffer clear")
Accelerator("alt+delete", "@buffer clear")
Accelerator("alt+shift+delete", "@buffer clearall")
Accelerator("alt+shift+right", "@buffer add")
Accelerator("alt+shift+left", "@buffer delete")
Accelerator("f4", "@sp-settings master volume")
Accelerator("f5", "@sp-settings General volume")
Accelerator("f6", "@sp-settings ambiance volume")
Accelerator("f7", "@sp-settings in and out of character communications volume")
Accelerator("f8", "@sp-settings planetary music volume")
Accelerator("ctrl+f5", "@sp-settings clocks volume")
Accelerator("ctrl+f6", "@sp-settings Combat music volume")
Accelerator("ctrl+f7", "@sp-settings IC social volume")
Accelerator("ctrl+f8", "@sp-settings OOC social volume")
Accelerator("alt+f5", "@sp-settings Ground Combat volume")
Accelerator("alt+f6", "@sp-settings harvesting activity volume")
Accelerator("alt+f7", "@sp-settings Salvage activity volume")
Accelerator("alt+f8", "@sp-settings volume for games such as Pilot's Parody")
Accelerator("shift+f5", "@sp-settings starship combat volume")
Accelerator("shift+f6", "@sp-settings ship volume")
Accelerator("shift+f7", "@sp-settings ship movement volume")
Accelerator("shift+f8", "@sp-settings vehicle volume")
Accelerator("f9", "@sp-settings IC social sounds")
Accelerator("f10", "@sp-settings OOC social sounds")
Accelerator("alt+f9", "@sp-settings panning of sounds")
Accelerator("alt+f10", "@sp-settings pitch-shifting sounds")
Accelerator("alt+f11", "@sp-settings ambiance")
Accelerator("ctrl+f9", "@sp-settings planetary music")
Accelerator("ctrl+f10", "@sp-settings space combat music")
Accelerator("ctrl+f11", "@sp-settings weather sounds")
Accelerator("alt+f12", "@sp-restart")
Accelerator("shift+alt+f9", "@sp-settings Interrupt OOC Social sounds when the next one plays")
Accelerator("f2", "@sp-settings")
Accelerator("f5", "@sp-settings General volume")
Accelerator("f6", "@sp-settings ambiance volume")
Accelerator("f7", "@sp-settings in and out of character communications volume")
Accelerator("f8", "@sp-settings planetary music volume")
Accelerator("ctrl+f5", "@sp-settings clocks volume")
Accelerator("ctrl+f6", "@sp-settings Combat music volume")
Accelerator("ctrl+f7", "@sp-settings IC social volume")
Accelerator("ctrl+f8", "@sp-settings OOC social volume")
Accelerator("alt+f5", "@sp-settings Ground Combat volume")
Accelerator("alt+f6", "@sp-settings harvesting activity volume")
Accelerator("alt+f7", "@sp-settings Salvage activity volume")
Accelerator("alt+f8", "@sp-settings volume for games such as Pilot's Parody")
Accelerator("shift+f5", "@sp-settings starship combat volume")
Accelerator("shift+f6", "@sp-settings ship volume")
Accelerator("shift+f7", "@sp-settings ship movement volume")
Accelerator("shift+f8", "@sp-settings vehicle volume")
Accelerator("f9", "@sp-settings IC social sounds")
Accelerator("f10", "@sp-settings OOC social sounds")
Accelerator("alt+f9", "@sp-settings panning of sounds")
Accelerator("alt+f10", "@sp-settings pitch-shifting sounds")
Accelerator("alt+f11", "@sp-settings ambiance")
Accelerator("ctrl+f9", "@sp-settings planetary music")
Accelerator("ctrl+f10", "@sp-settings space combat music")
Accelerator("ctrl+f11", "@sp-settings weather sounds")
Accelerator("shift+alt+f9", "@sp-settings Interrupt OOC Social sounds when the next one plays")
Accelerator("f2", "@sp-settings")
Accelerator("shift+f1", "@sp-toggle")
Accelerator("Ctrl+shift+enter", "@sp-settings buffer sounds")
Accelerator("ctrl+shift+v", "@sp-version")
Accelerator("ctrl+s", "save_state_func")
Accelerator("alt+shift+h", "localsphelp")
Accelerator("ctrl+f2", "mainvoldown")
Accelerator("ctrl+f3", "mainvolup")
Accelerator("alt+f3", "intromusictoggle")
Accelerator("ctrl+f1", "smute")
Accelerator("f11", "f11_stop")
Accelerator("alt+f12", "alt_f12_stop")
function save_state_func()
SaveState()
Execute("tts_interrupt Client-side Soundpack Settings have been saved successfully!")
end
function f11_stop()
ppi.stop(0)
Execute("tts_interrupt All currently playing sounds have been stopped.")
Send("@sp-restart")
end
function alt_f12_stop()
ppi.stop(0)
Execute("tts_interrupt All currently playing sounds have been stopped.")
end
function RegisterMushClient(name, line, wildcards)
@@ -91,7 +114,15 @@ Note("You are currently running version " .. tostring(CURRENT_SP_VERSION).." of
if version_diff > 0 then
ColourNote("yellow", "", "Warning: Your soundpack is " .. version_diff .. " version(s) out of date. We are now on version " .. user_version .. " of the soundpack.")
ColourNote("yellow", "", "Run Updater.bat to pull the latest version.")
ColourNote("yellow", "", "Launching the updater...")
ppi.play(dir.."sounds/ogg/general/comms/announcement.ogg", 0, 0, 100)
local bat_path = dir .. "..\\updator.bat"
-- Signature is (filename, params, defdir, operation, show) - this was
-- previously passing "open" as the filename, so it never actually
-- launched cmd.exe/updator.bat at all.
utils.shellexecute("cmd", '/c "' .. bat_path .. '"', "", "open", 1)
ColourNote("yellow", "", "The updater has been launched in a separate window. Once the update is complete, please restart MUSHclient to apply the updates!")
Execute("tts_interrupt Warning: Your soundpack is out of date. The updater has been launched. Once the update is complete, please restart MUSHclient to apply the updates!")
else
ColourNote("green", "", "Your soundpack is up to date!")
end
@@ -102,6 +133,109 @@ function url_encode(str)
return string.format("%%%02X", string.byte(c))
end))
end
pending_downloads = {}
pending_play = {}
function download_sound(path, action, pan, volume, pitch, id)
if pending_downloads[path] then
table.insert(pending_play[path], {action=action, pan=pan, volume=volume, pitch=pitch, id=id})
return
end
pending_downloads[path] = os.time()
pending_play[path] = { {action=action, pan=pan, volume=volume, pitch=pitch, id=id} }
local sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
local safe_path = sound_path:gsub("/", "\\")
local parent_dir = safe_path:match("^(.+)\\[^\\]+$")
local encoded_path = url_encode(path)
local url = "http://nathantech.net:3000/CosmicRage/CosmicRageSounds/raw/branch/main/ogg/" .. encoded_path .. ".ogg"
-- Download to a .part file and only rename it to the real filename after
-- curl succeeds, so check_downloads() can never mistake a half-written or
-- failed transfer for a completed one. Clears any leftover .err first, and
-- deletes it again on success, so a stale error/empty file can't linger.
local cmd_args = string.format(
'/C mkdir "%s" 2>nul & del /q "%s.err" 2>nul & ' ..
'curl.exe -s -S -L -f -o "%s.part" "%s" 2>"%s.err" ' ..
'&& (move /y "%s.part" "%s" >nul & del /q "%s.err" 2>nul) || del /q "%s.part" 2>nul',
parent_dir, safe_path, safe_path, url, safe_path, safe_path, safe_path, safe_path, safe_path
)
-- NOTE: correct shellexecute signature is (filename, params, defdir, operation, show)
utils.shellexecute("cmd", cmd_args, "", "open", 0)
end
-- If a download hasn't finished or failed within this long, give up on it
-- rather than let a stuck curl process silently block that sound forever.
DOWNLOAD_TIMEOUT_SECONDS = 30
function check_downloads(name)
for path, started_at in pairs(pending_downloads) do
local sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
local safe_path = sound_path:gsub("/", "\\")
local f = io.open(sound_path, "r")
if f then
f:close()
local queue = pending_play[path]
pending_downloads[path] = nil
pending_play[path] = nil
if queue then
for _, p in ipairs(queue) do
play_sound_now(path, p.action, p.pan, p.volume, p.pitch, p.id)
end
end
else
local ef = io.open(safe_path .. ".err", "r")
if ef then
local msg = ef:read("*all")
ef:close()
if msg and msg ~= "" then
os.remove(safe_path .. ".err")
pending_downloads[path] = nil
pending_play[path] = nil
Note("Soundpack: download failed for " .. path .. ": " .. msg:gsub("[\r\n]", ""))
end
elseif os.time() - started_at > DOWNLOAD_TIMEOUT_SECONDS then
pending_downloads[path] = nil
pending_play[path] = nil
Note("Soundpack: download timed out for " .. path)
end
end
end
end
function play_sound_now(path, action, pan, volume, pitch, id)
local sound_file = dir.."sounds/ogg/"..path..".ogg"
local temp
if (action == "play") then
temp = ppi.play(sound_file, 0, pan, volume)
if (pitch > -10000) then
ppi.setPitch(pitch, temp)
end
if (pan > -10000) then
ppi.setPan(pan, temp)
end
elseif (action == "playrec") then
temp = ppi.play(sound_file, 0, pan, volume)
SetVariable(id, temp)
if (pitch > -10000) then
ppi.setPitch(pitch, temp)
end
if (pan > -10000) then
ppi.setPan(pan, temp)
end
elseif (action == "loop") then
temp = ppi.play(sound_file, 1, pan, volume)
SetVariable(id, temp)
if (pitch > -10000) then
ppi.setPitch(pitch, temp)
end
if (pan > -10000) then
ppi.setPan(pan, temp)
end
end
end
]]>
@@ -197,106 +331,87 @@ match="^\$sphook\s+(?P<action>[\w]+):(?P<path>[-\w./\\\s]+):(?P<volume>[\w]+):(?
pan="%5"
id="%6"
volume=tonumber(volume)
if (path == "na") then
else
sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
directory_path = sound_path:match("(.*/)")
if directory_path then
test_file = io.open(directory_path .. "tester.txt", "w")
if test_file then
test_file:close()
os.remove(directory_path .. "checker.txt") -- Clean up
else
os.execute('mkdir "' .. directory_path .. '" 2>nul')
end
end
file = io.open(sound_path, "r")
if file then
file:close()
else
local encoded_path = url_encode(path)
local download_url = "http://nathantech.net:3000/CosmicRage/CosmicRageSounds/raw/branch/main/ogg/" .. encoded_path .. ".ogg"
body = {}
result, status_code, headers, status = socket.http.request{
url = download_url, sink = ltn12.sink.table(body)
}
if result and status_code == 200 then
save_file = io.open(sound_path, "wb")
if save_file then
save_file:write(table.concat(body))
save_file:close()
end
else
Note("Download of "..path.." failed with status code: " .. tostring(status_code))
end
end
end
if (pitch == "na") then
if (pitch == "na") then
pitch = -10000
else
else
pitch = tonumber(pitch)
pitch = (pitch - 44100) / 98
end
end
if (pan == "na") then
if (pan == "na") then
pan = -10000
else
else
pan = tonumber(pan)
end
end
if (action == "play") then
temp = ppi.play(dir.."sounds/ogg/"..path..".ogg", 0, pan, volume)
if (path == "na") then
if (action == "adjustsound") then
temp = GetVariable(id)
if volume and volume > -1 then
ppi.setVol(volume, temp)
end
if (pitch > -10000) then
ppi.setPitch(pitch, temp)
end
if (pan > -10000) then
ppi.setPan(pan, temp)
end
end
if (action=="playrec") then
temp=ppi.play(dir.."sounds/ogg/"..path..".ogg", 0, pan, volume)
SetVariable(id, temp)
if(pitch>-10000) then
ppi.setPitch(pitch,temp)
end
if(pan>-10000) then
ppi.setPan(pan,temp)
end
end
if (action=="loop") then
temp=ppi.play(dir.."sounds/ogg/"..path..".ogg", 1, pan, volume)
SetVariable(id, temp)
if(pitch>-10000) then
ppi.setPitch(pitch,temp)
end
if(pan>-10000) then
ppi.setPan(pan,temp)
end
end
if (action=="adjustsound") then
temp=GetVariable (id)
if(volume>-1) then
ppi.setVol(volume,temp)
end
if(pitch>-10000) then
ppi.setPitch(pitch,temp)
end
if(pan>-10000) then
ppi.setPan(pan,temp)
end
end
if (action=="stop") then
temp=GetVariable(id)
elseif (action == "stop") then
temp = GetVariable(id)
if temp then
ppi.stop(temp)
end
end
else
local sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
local file = io.open(sound_path, "r")
if file then
file:close()
play_sound_now(path, action, pan, volume, pitch, id)
else
download_sound(path, action, pan, volume, pitch, id)
end
end
</send>
</trigger>
</triggers>
<!-- Aliases -->
<!-- Timers -->
<timers>
<timer
enabled="y"
second="0.5"
script="check_downloads"
>
</timer>
</timers>
<aliases>
<alias
match="f11_stop"
enabled="y"
script="f11_stop"
omit_from_output="y"
omit_from_log="y"
omit_from_command_history="y"
send_to="12"
sequence="100"
>
</alias>
<alias
match="alt_f12_stop"
enabled="y"
script="alt_f12_stop"
omit_from_output="y"
omit_from_log="y"
omit_from_command_history="y"
send_to="12"
sequence="100"
>
</alias>
</aliases>
</muclient>

View File

@@ -298,6 +298,10 @@ function OnPluginScreendraw(t, log, line)
-- checking for an empty string, or a string composed only of spaces.
-- If we don't, NVDA says blank.
if (t == 0 or t == 1) and not line:find("^%s*$") then
if GetVariable("skip_speech") == "1" then
SetVariable("skip_speech", "0")
return
end
if subst.status==0 then
return
end

View File

@@ -14,8 +14,8 @@ save_state="y"
language="Lua"
purpose="provides functions to help tts users."
date_written="2010-04-06 08:37:40"
requires="4.46"
version="1.0"
requires="4.82"
version="1.1"
>
</plugin>
@@ -172,6 +172,17 @@ sequence="100">
</alias>
</aliases>
<!-- Timers -->
<timers>
<timer
enabled="y"
second="0.2"
script="poll_output_focus"
>
</timer>
</timers>
<!-- Get our standard constants -->
@@ -200,6 +211,7 @@ Accelerator("ctrl+shift+n","endline")
Accelerator("ctrl+shift+y","topline")
Accelerator("ctrl+shift+h","whichline")
Accelerator("ctrl+shift+alt+s","snap_shot")
Accelerator("ctrl+alt+j","toggle_jump_to_end")
function selectscr(eol)
buffercheck()
@@ -280,12 +292,188 @@ function OnPluginCommandEntered(s)
end
end
------------------------------------------------------------------
-- Notepad smart-scroll (formerly the separate NotepadSmartScroll
-- plugin, merged in here). See SmartAppendToNotepad's comment below
-- for why this needs raw Win32 access via Alien.
------------------------------------------------------------------
require "alien"
local user32 = alien.load("user32.dll")
local kernel32 = alien.load("kernel32.dll")
local FindWindowEx = user32.FindWindowExA
FindWindowEx:types{ ret = "pointer", "pointer", "pointer", "string", "string", abi = "stdcall" }
local GetWindowThreadProcessId = user32.GetWindowThreadProcessId
GetWindowThreadProcessId:types{ ret = "uint", "pointer", "ref uint", abi = "stdcall" }
local GetCurrentProcessId = kernel32.GetCurrentProcessId
GetCurrentProcessId:types{ ret = "uint", abi = "stdcall" }
local SendMessage = user32.SendMessageA
SendMessage:types{ ret = "long", "pointer", "uint", "uint", "long", abi = "stdcall" }
local GetModuleHandle = kernel32.GetModuleHandleA
GetModuleHandle:types{ ret = "pointer", "string", abi = "stdcall" }
local GetProcAddress = kernel32.GetProcAddress
GetProcAddress:types{ ret = "pointer", "pointer", "string", abi = "stdcall" }
local GetFocus = user32.GetFocus
GetFocus:types{ ret = "pointer", abi = "stdcall" }
-- EM_GETSEL's wParam/lParam are *pointers* to DWORD (needed to get the
-- real 32-bit selection range - the classic packed-return form is capped
-- at 64K). SendMessage above is already typed with "uint"/"long" for its
-- generic wParam/lParam, so a second binding of the same underlying
-- function is needed with "pointer" typed args for this one message.
local user32_handle = GetModuleHandle("user32.dll")
local SendMessagePtr = alien.funcptr(GetProcAddress(user32_handle, "SendMessageA"))
SendMessagePtr:types{ ret = "long", "pointer", "uint", "pointer", "pointer", abi = "stdcall" }
local WM_GETTEXTLENGTH = 0x000E
local EM_GETFIRSTVISIBLELINE = 0x00CE
local EM_LINESCROLL = 0x00B6
local EM_GETSEL = 0x00B0
local EM_SETSEL = 0x00B1
local EM_SCROLLCARET = 0x00B7
local my_pid = GetCurrentProcessId()
-- Finds the "Edit" control belonging to a MUSHclient notepad MDI child
-- window whose title exactly matches. Scoped to top-level windows owned
-- by this process, so it never touches unrelated apps on the desktop.
local function find_notepad_edit(title)
local top = nil
while true do
top = FindWindowEx(nil, top, nil, nil)
if not top then return nil end
local _, pid = GetWindowThreadProcessId(top, 0)
if pid == my_pid then
local mdiClient = FindWindowEx(top, nil, "MDIClient", nil)
if mdiClient then
local frame = FindWindowEx(mdiClient, nil, nil, title)
if frame then
local edit = FindWindowEx(frame, nil, "Edit", nil)
if edit then
return edit
end
end
end
end
end
end
local function get_selection(edit)
local start_buf = alien.buffer(4)
local end_buf = alien.buffer(4)
SendMessagePtr(edit, EM_GETSEL, start_buf, end_buf)
return start_buf:get(1, "uint"), end_buf:get(1, "uint")
end
local function scroll_to_line(edit, target)
local current = SendMessage(edit, EM_GETFIRSTVISIBLELINE, 0, 0)
local delta = target - current
if delta ~= 0 then
SendMessage(edit, EM_LINESCROLL, 0, delta)
end
end
-- Wraps AppendToNotepad(title, text): if the caret wasn't already sitting
-- at the very end of the text (i.e. the user has moved it to read
-- somewhere else), both the caret/selection and the scroll position are
-- restored after the append instead of being left jumped to the end.
-- AppendToNotepad hardcodes SetSel(len,len)+ReplaceSel on every call (see
-- scripting/methods/methods_notepad.cpp in the MUSHclient source) with no
-- scriptable override, so this reads the notepad Edit control's caret and
-- scroll position via raw Win32 calls (through the Alien FFI library,
-- running in-process on MUSHclient's own thread) before appending, and
-- restores them afterward when appropriate.
--
-- Note: text should use "\r\n" line endings, not bare "\n" - the plain
-- Win32 Edit control that backs the notepad doesn't recognise bare "\n"
-- as a line break for scrolling/line-count purposes.
function SmartAppendToNotepad(title, text)
local edit = find_notepad_edit(title)
local was_at_end = true
local sel_start, sel_end, first_visible
if edit then
local old_length = SendMessage(edit, WM_GETTEXTLENGTH, 0, 0)
sel_start, sel_end = get_selection(edit)
was_at_end = (sel_start == old_length) and (sel_end == old_length)
if not was_at_end then
first_visible = SendMessage(edit, EM_GETFIRSTVISIBLELINE, 0, 0)
end
end
AppendToNotepad(title, text)
if edit and not was_at_end then
-- Re-find in case the append above created/recreated the window.
local edit2 = find_notepad_edit(title) or edit
SendMessage(edit2, EM_SETSEL, sel_start, sel_end)
scroll_to_line(edit2, first_visible)
end
end
-- Moves the caret to the very end of a notepad and scrolls it into view.
local function jump_notepad_to_end(edit)
local len = SendMessage(edit, WM_GETTEXTLENGTH, 0, 0)
SendMessage(edit, EM_SETSEL, len, len)
SendMessage(edit, EM_SCROLLCARET, 0, 0)
end
function toggle_jump_to_end()
if GetVariable("jump_to_end_on_focus") == "0" then
SetVariable("jump_to_end_on_focus", "1")
Execute("tts_interrupt Jump to end when switching to output: on")
else
SetVariable("jump_to_end_on_focus", "0")
Execute("tts_interrupt Jump to end when switching to output: off")
end
end
-- Polls for the "output" notepad gaining focus (mouse click, Ctrl+Tab
-- between MDI windows, ActivateNotepad from a script, etc.), so the
-- jump-to-end behaviour applies regardless of how the user switches to
-- it. MUSHclient's own Accelerator/hotkey system can't see keystrokes
-- while a separate notepad window has focus (confirmed by Nick Gammon on
-- the MUSHclient forum), so this can't be event-driven - polling every
-- 0.2s is the safe, non-hooking way to catch it.
was_output_focused = false
function poll_output_focus()
if GetVariable("jump_to_end_on_focus") == "0" then
was_output_focused = false
return
end
local edit = find_notepad_edit("output")
if not edit then
was_output_focused = false
return
end
local focused = GetFocus()
local is_focused = (focused == edit)
if is_focused and not was_output_focused then
jump_notepad_to_end(edit)
end
was_output_focused = is_focused
end
------------------------------------------------------------------
function OnPluginInstall()
if GetVariable("output") == nil then
SetVariable("output","1")
end
if GetVariable("jump_to_end_on_focus") == nil then
SetVariable("jump_to_end_on_focus","1")
end
modes = {}
msgbuffer = {}
cline = 1
lastcount=0
line = 0
@@ -297,23 +485,7 @@ function OnPluginScreendraw(t,l,line)
if(GetVariable("output")=="0") then
return
end
if GetInfo(113) then
AppendToNotepad("output", line.."\r\n")
else
table.insert(msgbuffer, line)
end
end
function OnPluginGetFocus()
if GetVariable("output") == "0" then
return
end
if #msgbuffer > 0 then
for i, buffered in ipairs(msgbuffer) do
AppendToNotepad("output", buffered.."\r\n")
end
msgbuffer={}
end
SmartAppendToNotepad("output", line.."\r\n")
end
function prev_line()

View File

@@ -40,7 +40,8 @@ MushClient is a portable client designed specifically for Cosmic Rage. The game
- **Alt + PageUp**: Scroll buffer up
- **Alt + End**: Jump to bottom of buffer
- **Alt + Home**: Jump to top of buffer
- **Alt + Shift + Delete**: Clear buffer
- **Alt + Delete**: Clear current buffer
- **Alt + Shift + Delete**: Clear all buffers
- **Alt + Shift + Right**: Add buffer
- **Alt + Shift + Left**: Delete buffer
@@ -71,13 +72,39 @@ MushClient is a portable client designed specifically for Cosmic Rage. The game
- **Ctrl + F9**: Toggle planetary music
- **Ctrl + F10**: Toggle space combat music
- **Ctrl + F11**: Toggle weather sounds
- **Alt + F12**: Restart
- **Shift + Alt + F9**: Interrupt OOC social sounds on next play
- **Ctrl + F1**: Mute/unmute master sound
- **Ctrl + F2**: Master volume down
- **Ctrl + F3**: Master volume up
- **Alt + F3**: Toggle intro music
- **F11**: Stop all currently playing sounds and restart the soundpack
- **Alt + F12**: Stop all currently playing sounds
### Other Functions
- **Shift + F1**: Toggle SP on/off
- **Ctrl + Shift + Enter**: Toggle buffer sounds
- **Ctrl + Shift + V**: Show version
- **Ctrl + S**: Save client-side soundpack settings
- **Alt + Shift + H**: Show local help
### Accessible Output Window
The soundpack mirrors game text into a separate notepad-style window titled "output", useful for reviewing text with a screen reader without losing your place when new lines arrive.
- **Ctrl + Alt + O**: Toggle whether output is mirrored to the output window
- **Ctrl + Alt + J**: Toggle whether switching to the output window jumps you to the newest line
- **Ctrl + Shift + C**: Clear the output buffer
- **Ctrl + Alt + Shift + C**: Clear the output window
- **Ctrl + Shift + I**: Speak the current line
- **Ctrl + Shift + U**: Speak the previous line
- **Ctrl + Shift + O**: Speak the next line
- **Ctrl + Shift + Y**: Jump to and speak the top line
- **Ctrl + Shift + N**: Jump to and speak the bottom line
- **Ctrl + Shift + H**: Announce current line number and total line count
- **Ctrl + Shift + Space**: Start/stop selecting lines (copies with line breaks)
- **Ctrl + Shift + Alt + Space**: Start/stop selecting lines (copies joined by spaces, no line breaks)
- **Ctrl + Shift + Alt + S**: Take a snapshot of the whole buffer into a "snapshot" notepad
- **Ctrl + Alt + Enter**: Toggle stopping speech whenever you press Enter
- **Ctrl + 1** to **Ctrl + 0**: Recall a recent output line by position from the end (tap once to speak it, twice to copy it, three times to paste it into the command line)
## FAQ