From 09785a9080a6cc21d740b513009583a868f50458 Mon Sep 17 00:00:00 2001 From: OlegTheSnowman <123775785+OlegTheSnowman@users.noreply.github.com> Date: Sat, 11 Jul 2026 00:02:28 +0300 Subject: [PATCH] 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. --- cosmic rage/alien/core.dll | Bin 0 -> 26112 bytes cosmic rage/alien/struct.dll | Bin 0 -> 11776 bytes cosmic rage/lua/alien.lua | 245 ++++++++++++ .../worlds/Cosmic Rage/cosmic rage.MCL | 25 +- cosmic rage/worlds/plugins/CosmicRage.xml | 357 ++++++++++++------ cosmic rage/worlds/plugins/MushReader.xml | 4 + .../worlds/plugins/output_functions.xml | 212 ++++++++++- readme.md | 31 +- 8 files changed, 719 insertions(+), 155 deletions(-) create mode 100644 cosmic rage/alien/core.dll create mode 100644 cosmic rage/alien/struct.dll create mode 100644 cosmic rage/lua/alien.lua diff --git a/cosmic rage/alien/core.dll b/cosmic rage/alien/core.dll new file mode 100644 index 0000000000000000000000000000000000000000..b7da37f1eced1a96129560af636472d6583ce35e GIT binary patch literal 26112 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�K6+!6g+5X99Hgd%KGDKch$CF47LHaWbf_;b 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 E4Rj5!M*si- literal 0 HcmV?d00001 diff --git a/cosmic rage/alien/struct.dll b/cosmic rage/alien/struct.dll new file mode 100644 index 0000000000000000000000000000000000000000..26e9c0b2a5ed19b1b109960e90dc0de56df12b35 GIT binary patch literal 11776 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 F{|WOrK(hb< literal 0 HcmV?d00001 diff --git a/cosmic rage/lua/alien.lua b/cosmic rage/lua/alien.lua new file mode 100644 index 0000000..71c08b7 --- /dev/null +++ b/cosmic rage/lua/alien.lua @@ -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 diff --git a/cosmic rage/worlds/Cosmic Rage/cosmic rage.MCL b/cosmic rage/worlds/Cosmic Rage/cosmic rage.MCL index b213029..80725e9 100644 --- a/cosmic rage/worlds/Cosmic Rage/cosmic rage.MCL +++ b/cosmic rage/worlds/Cosmic Rage/cosmic rage.MCL @@ -1,6 +1,6 @@ - + @@ -8,13 +8,13 @@ @@ -140,7 +140,7 @@ @@ -149,7 +149,7 @@ @@ -243,7 +243,7 @@ @@ -252,7 +252,7 @@ @@ -309,7 +309,7 @@ @@ -423,7 +423,7 @@ @@ -448,7 +448,6 @@ - @@ -457,5 +456,7 @@ + + diff --git a/cosmic rage/worlds/plugins/CosmicRage.xml b/cosmic rage/worlds/plugins/CosmicRage.xml index 884c932..d2bd085 100644 --- a/cosmic rage/worlds/plugins/CosmicRage.xml +++ b/cosmic rage/worlds/plugins/CosmicRage.xml @@ -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[\w]+):(?P[-\w./\\\s]+):(?P[\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 + + if (pitch == "na") then + pitch = -10000 else - os.execute('mkdir "' .. directory_path .. '" 2>nul') + pitch = tonumber(pitch) + pitch = (pitch - 44100) / 98 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() + + if (pan == "na") then + pan = -10000 + else + pan = tonumber(pan) + end + + 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 + 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 -else - Note("Download of "..path.." failed with status code: " .. tostring(status_code)) - end -end -end - -if (pitch == "na") then - pitch = -10000 -else - pitch = tonumber(pitch) - pitch = (pitch - 44100) / 98 -end - -if (pan == "na") then - pan = -10000 -else - pan = tonumber(pan) -end - -if (action == "play") then - temp = ppi.play(dir.."sounds/ogg/"..path..".ogg", 0, pan, volume) - 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) - ppi.stop(temp) end - + + + + + + + + + + + \ No newline at end of file diff --git a/cosmic rage/worlds/plugins/MushReader.xml b/cosmic rage/worlds/plugins/MushReader.xml index 387798d..fe8939f 100644 --- a/cosmic rage/worlds/plugins/MushReader.xml +++ b/cosmic rage/worlds/plugins/MushReader.xml @@ -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 diff --git a/cosmic rage/worlds/plugins/output_functions.xml b/cosmic rage/worlds/plugins/output_functions.xml index 57795f4..e71e33c 100644 --- a/cosmic rage/worlds/plugins/output_functions.xml +++ b/cosmic rage/worlds/plugins/output_functions.xml @@ -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" > @@ -172,6 +172,17 @@ sequence="100"> + + + + + + + @@ -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() diff --git a/readme.md b/readme.md index fe9dbfd..b85475e 100644 --- a/readme.md +++ b/readme.md @@ -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