mudgangster

Log | Files | Refs

commit bf8e9ba4bf7e429d04909a29df35436960522003
Author: Michael Savage <mikejsavage@gmail.com>
Date:   Sat, 29 Sep 2012 17:01:56 +0100

Initial commit

Diffstat:
.gitignore | 3+++
Makefile | 13+++++++++++++
action.lua | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
alias.lua | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
chat.lua | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
connect.lua | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
event.lua | 39+++++++++++++++++++++++++++++++++++++++
gag.lua | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
handlers.lua | 265+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
interval.lua | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
macro.lua | 31+++++++++++++++++++++++++++++++
main.lua | 44++++++++++++++++++++++++++++++++++++++++++++
script.lua | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
serialize.lua | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/common.h | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/input.c | 210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/input.h | 21+++++++++++++++++++++
src/main.c | 29+++++++++++++++++++++++++++++
src/script.c | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/script.h | 11+++++++++++
src/textbox.c | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/textbox.h | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ui.c | 383+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ui.h | 11+++++++++++
sub.lua | 44++++++++++++++++++++++++++++++++++++++++++++
utils.lua | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
26 files changed, 2651 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,3 @@ +mudGangster +.tags +*.o diff --git a/Makefile b/Makefile @@ -0,0 +1,13 @@ +CC = gcc +SRCDIR = src +CFLAGS = -O2 -std=gnu99 -ggdb +LIBS = -lX11 -L/usr/X11R6/lib -llua +WARNINGS = -Wall -Wextra -Werror +OBJS = main.o textbox.o input.o ui.o script.o +.PHONY: all + +all: ${OBJS} + ${CC} -o mudGangster ${OBJS} ${CFLAGS} ${LIBS} ${WARNINGS} + +%.o: ${SRCDIR}/%.c + ${CC} -c ${SRCDIR}/$*.c ${CFLAGS} ${LIBS} ${WARNINGS} diff --git a/action.lua b/action.lua @@ -0,0 +1,89 @@ +local Actions = { } +local PreActions = { } +local AnsiActions = { } +local AnsiPreActions = { } + +local ChatActions = { } +local ChatPreActions = { } +local ChatAnsiActions = { } +local ChatAnsiPreActions = { } + +local doActions +local doPreActions +local doAnsiActions +local doAnsiPreActions + +local doChatActions +local doChatPreActions +local doChatAnsiActions +local doChatAnsiPreActions + +local function genericActions( actions ) + return + function( pattern, callback, disabled ) + enforce( pattern, "pattern", "string" ) + enforce( callback, "callback", "function", "string" ) + + if type( callback ) == "string" then + local command = callback + + callback = function() + mud.send( command .. "\n" ) + end + end + + local action = { + pattern = pattern, + callback = callback, + + enabled = not disabled, + + enable = function( self ) + self.enabled = true + end, + disable = function( self ) + self.enabled = false + end, + } + + table.insert( actions, action ) + + return action + end, + + function( line ) + for i = 1, #actions do + local action = actions[ i ] + + if action.enabled then + local ok, err = pcall( string.gsub, line, action.pattern, action.callback ) + + if not ok then + mud.print( debug.traceback( "\n#s> action callback failed: %s" % err ) ) + end + end + end + end +end + +mud.action, doActions = genericActions( Actions ) +mud.preAction, doPreActions = genericActions( PreActions ) +mud.ansiAction, doAnsiActions = genericActions( AnsiActions ) +mud.ansiPreAction, doAnsiPreActions = genericActions( AnsiPreActions ) + +mud.chatAction, doChatActions = genericActions( ChatActions ) +mud.preChatAction, doChatPreActions = genericActions( ChatPreActions ) +mud.ansiChatAction, doChatAnsiActions = genericActions( ChatAnsiActions ) +mud.ansiPreChatAction, doChatAnsiPreActions = genericActions( ChatAnsiPreActions ) + +return { + doActions = doActions, + doPreActions = doPreActions, + doAnsiActions = doAnsiActions, + doAnsiPreActions = doAnsiPreActions, + + doChatActions = doChatActions, + doChatPreActions = doChatPreActions, + doChatAnsiActions = doChatAnsiActions, + doChatAnsiPreActions = doChatAnsiPreActions, +} diff --git a/alias.lua b/alias.lua @@ -0,0 +1,101 @@ +local Aliases = { } + +local function doAlias( line ) + local command, args = line:match( "^%s*(%S+)%s*(.*)$" ) + local alias = Aliases[ command ] + + if alias and alias.enabled then + local badSyntax = true + + for i = 1, #alias.callbacks do + local callback = alias.callbacks[ i ] + local _, subs = args:gsub( callback.pattern, callback.callback ) + + if subs ~= 0 then + badSyntax = false + + break + end + end + + if badSyntax then + mud.print( "\nsyntax: %s %s" % { command, alias.syntax } ) + end + + return true + end + + return false +end + +local function simpleAlias( callback, disabled ) + return { + callbacks = { + { + pattern = "^(.*)$", + callback = callback, + }, + }, + + enabled = not disabled, + + enable = function( self ) + self.enabled = true + end, + disable = function( self ) + self.enabled = false + end, + } +end + +local function patternAlias( callbacks, syntax, disabled ) + local alias = { + callbacks = { }, + syntax = syntax, + + enabled = not disabled, + + enable = function( self ) + self.enabled = true + end, + disable = function( self ) + self.enabled = false + end, + } + + for pattern, callback in pairs( callbacks ) do + table.insert( alias.callbacks, { + pattern = pattern, + callback = callback, + } ) + end + + return alias +end + +function mud.alias( command, handler, ... ) + enforce( command, "command", "string" ) + enforce( handler, "handler", "function", "string", "table" ) + + assert( not Aliases[ command ], "alias `%s' already registered" % command ) + + if type( handler ) == "string" then + local command = handler + + handler = function() + mud.input( command ) + end + end + + local alias = type( handler ) == "function" + and simpleAlias( handler, ... ) + or patternAlias( handler, ... ) + + Aliases[ command ] = alias + + return alias +end + +return { + doAlias = doAlias, +} diff --git a/chat.lua b/chat.lua @@ -0,0 +1,174 @@ +local loop = ev.Loop.default + +local handleChat + +local CommandBytes = { + all = "\4", + pm = "\5", + message = "\7", +} + +local Clients = { } + +local function clientFromName( name ) + local idx = tonumber( name ) + + if idx then + return Clients[ idx ] + end + + for _, client in ipairs( Clients ) do + if client.name:startsWith( name ) then + return client + end + end + + return nil +end + +local function dataCoro( client ) + local data = coroutine.yield() + local name = data:match( "^YES:(.+)\n$" ) + + if not name then + client.socket:shutdown() + + return + end + + client.name = name + client.state = "connected" + + mud.print( "\n#s> Connected to %s@%s:%s", client.name, client.address, client.port ) + + local dataBuffer = "" + + while true do + dataBuffer = dataBuffer .. coroutine.yield() + + dataBuffer = dataBuffer:gsub( "(.)(.-)\255", function( command, args ) + if command == CommandBytes.all or command == CommandBytes.pm or command == CommandBytes.message then + local message = args:match( "^\n*(.-)\n*$" ) + + handleChat( message ) + end + + return "" + end ) + end +end + +local Client = { } + +function Client:new( socket, address, port ) + socket:setoption( "keepalive", true ) + + local client = { + socket = socket, + + address = address, + port = port, + + state = "connecting", + handler = coroutine.create( dataCoro ), + } + + assert( coroutine.resume( client.handler, client ) ) + + setmetatable( client, { __index = Client } ) + + table.insert( Clients, client ) + + socket:send( "CHAT:Hirve\n127.0.0.14050 " ) + + return client +end + +local function dataHandler( client, loop, watcher ) + local _, err, data = client.socket:receive( "*a" ) + + if err == "closed" then + client.socket:shutdown() + watcher:stop( loop ) + + return + end + + assert( coroutine.resume( client.handler, data ) ) + mud.handleXEvents() +end + +function mud.chat( form, ... ) + local named = "\nHirve " .. form:format( ... ) + local data = CommandBytes.all .. named:parseColours() .. "\n\255" + + for _, client in ipairs( Clients ) do + if client.state == "connected" then + client.socket:send( data ) + end + end + + mud.printb( "#lr%s", named ) + handleChat() +end + +local function call( address, port ) + mud.print( "\n#s> Calling %s:%d...", address, port ) + + local sock = socket.tcp() + sock:settimeout( 0 ) + + sock:connect( address, port ) + + ev.IO.new( function( loop, watcher ) + local _, err = sock:receive( "*a" ) + + if err ~= "connection refused" then + local client = Client:new( sock, address, port ) + + ev.IO.new( function( loop, watcher ) + dataHandler( client, loop, watcher ) + end, sock:getfd(), ev.READ ):start( loop ) + else + mud.print( "\n#s> Failed to call %s:%d", address, port ) + end + + watcher:stop( loop ) + end, sock:getfd(), ev.WRITE ):start( loop ) +end + +mud.alias( "/call", { + [ "^(%S+)$" ] = function( address ) + call( address, 4050 ) + end, + + [ "^(%S+)[%s:]+(%d+)$" ] = call, +}, "<address> [port]" ) + +mud.alias( "/pm", { + [ "^(%S+)%s+(.-)$" ] = function( name, message ) + local client = clientFromName( name ) + + if client then + local coloured = message:parseColours() + + local named = "\nHirve chats to you, '" .. coloured .. "'" + local data = CommandBytes.pm .. named .. "\n\255" + + client.socket:send( data ) + + local toPrint = ( "You chat to %s, '" % client.name ) .. coloured .. "'" + + handleChat( toPrint ) + end + end, +} ) + +mud.alias( "/emoteto", function( args ) +end ) + +return { + init = function( chatHandler ) + handleChat = chatHandler + end, +} diff --git a/connect.lua b/connect.lua @@ -0,0 +1,113 @@ +local DataHandler + +local LastAddress +local LastPort + +local loop = ev.Loop.default + +function mud.disconnect() + if not mud.connected then + mud.print( "\n#s> You're not connected..." ) + + return + end + + mud.connected = false + + mud.kill() +end + +function mud.connect( address, port ) + if mud.connected then + mud.print( "\n#s> Already connected! (%s:%d)", LastAddress, LastPort ) + + return + end + + if not address then + if not LastAddress then + mud.print( "\n#s> I need an address..." ) + + return + end + + address = LastAddress + port = LastPort + end + + local sock = socket.tcp() + sock:settimeout( 0 ) + sock:connect( address, port ) + + mud.print( "\n#s> Connecting to %s:%d...", address, port ) + + mud.connected = true + mud.lastInput = mud.now() + + mud.send = function( data ) + mud.lastInput = mud.now() + + sock:send( data ) + end + + mud.kill = function() + sock:shutdown() + end + + ev.IO.new( function( loop, watcher ) + local _, err = sock:receive( "*a" ) + + if err ~= "connection refused" then + LastAddress = address + LastPort = port + + ev.IO.new( function( loop, watcher ) + local _, err, data = sock:receive( "*a" ) + + if not data then + data = _ + end + + if data then + DataHandler( data ) + mud.handleXEvents() + end + + if err == "closed" then + mud.print( "\n#s> Disconnected!" ) + + watcher:stop( loop ) + sock:shutdown() + + mud.connected = false + + return + end + end, sock:getfd(), ev.READ ):start( loop ) + else + mud.print( "\n#s> Refused!" ) + + mud.connected = false + end + + watcher:stop( loop ) + end, sock:getfd(), ev.WRITE ):start( loop ) +end + +mud.alias( "/con", { + [ "^$" ] = function() + mud.connect() + end, + + [ "^(%S+)%s+(%d+)$" ] = function( address, port ) + mud.connect( address, port ) + end, +}, "<ip> <port>" ) + +mud.alias( "/dc", mud.disconnect ) + +return { + init = function( dataHandler ) + DataHandler = dataHandler + end, +} diff --git a/event.lua b/event.lua @@ -0,0 +1,39 @@ +local Events = { } + +function mud.listen( name, callback, disabled ) + enforce( name, "name", "string" ) + enforce( callback, "callback", "function" ) + + local event = { + callback = callback, + + enabled = not disabled, + + enable = function( self ) + self.enabled = true + end, + disable = function( self ) + self.enabled = false + end, + } + + if not Events[ name ] then + Events[ name ] = { } + end + + table.insert( Events[ name ], event ) + + return event +end + +function mud.event( name, ... ) + enforce( name, "name", "string" ) + + if Events[ name ] then + for _, event in ipairs( Events[ name ] ) do + if event.enabled then + event.callback( ... ) + end + end + end +end diff --git a/gag.lua b/gag.lua @@ -0,0 +1,59 @@ +local Gags = { } +local AnsiGags = { } + +local ChatGags = { } +local AnsiChatGags = { } + +local doGags +local doAnsiGags + +local doChatGags +local doAnsiChatGags + +local function genericGags( gags ) + return + function( pattern, disabled ) + local gag = { + pattern = pattern, + + enabled = not disabled, + + enable = function( self ) + self.enabled = true + end, + disable = function( self ) + self.enabled = false + end, + } + + table.insert( gags, gag ) + + return gag + end, + + function( line ) + for i = 1, #gags do + local gag = gags[ i ] + + if gag.enabled and line:find( gag.pattern ) then + return true + end + end + + return false + end +end + +mud.gag, doGags = genericGags( Gags ) +mud.gagAnsi, doAnsiGags = genericGags( AnsiGags ) + +mud.gagChat, doChatGags = genericGags( ChatGags ) +mud.gagAnsiChat, doChatAnsiGags = genericGags( ChatAnsiGags ) + +return { + doGags = doGags, + doAnsiGags = doAnsiGags, + + doChatGags = doChatGags, + doChatAnsiGags = doChatAnsiGags, +} diff --git a/handlers.lua b/handlers.lua @@ -0,0 +1,265 @@ +local action = require( "action" ) +local alias = require( "alias" ) +local gag = require( "gag" ) +local macro = require( "macro" ) +local sub = require( "sub" ) +local interval = require( "interval" ) + +local GA = "\255\249" + +local bold = false +local fg = 7 +local bg = 0 + +local lastWasChat = false + +local receiving = false +local showInput = true + +local dataBuffer = "" +local pendingInputs = { } + +local function echoOn() + showInput = true + + return "" +end + +local function echoOff() + showInput = true + + return "" +end + +local function setFG( colour ) + return function() + fg = colour + end +end + +local Escapes = { + m = { + [ "0" ] = function() + bold = false + fg = 7 + bg = 0 + end, + + [ "1" ] = function() + bold = true + end, + + [ "30" ] = setFG( 0 ), + [ "31" ] = setFG( 1 ), + [ "32" ] = setFG( 2 ), + [ "33" ] = setFG( 3 ), + [ "34" ] = setFG( 4 ), + [ "35" ] = setFG( 5 ), + [ "36" ] = setFG( 6 ), + [ "37" ] = setFG( 7 ), + }, +} + +local function printPendingInputs() + if lastWasChat then + mud.newlineMain() + + lastWasChat = false + end + + for i = 1, #pendingInputs do + mud.printr( pendingInputs[ i ] ) + end + + pendingInputs = { } +end + +local function handleChat( message ) + if not message then + lastWasChat = true + + return + end + + local oldFG = fg + local oldBG = bg + local oldBold = bold + + fg = 1 + bg = 0 + bold = true + + local noAnsi = message:gsub( "\27%[[%d;]*%a", "" ) + + action.doChatPreActions( noAnsi ) + action.doChatAnsiPreActions( message ) + + mud.newlineMain() + mud.newlineChat() + + for line, newLine in message:gmatch( "([^\n]*)(\n?)" ) do + for text, opts, escape in ( line .. "\27[m" ):gmatch( "(.-)\27%[([%d;]*)(%a)" ) do + if text ~= "" then + mud.printMain( text, fg, bg, bold ) + mud.printChat( text, fg, bg, bold ) + end + + for opt in opts:gmatch( "([^;]+)" ) do + if Escapes[ escape ][ opt ] then + Escapes[ escape ][ opt ]() + end + end + end + + if newLine == "\n" then + mud.newlineMain() + mud.newlineChat() + end + end + + mud.drawMain() + mud.drawChat() + + fg = oldFG + bg = oldBG + bold = oldBold + + lastWasChat = true +end + +local function handleData( data ) + receiving = true + + dataBuffer = dataBuffer .. data + + if data:match( GA ) then + dataBuffer = dataBuffer:gsub( "\r", "" ) + + dataBuffer = dataBuffer:gsub( "\255\252\1", echoOn ) + dataBuffer = dataBuffer:gsub( "\255\251\1", echoOff ) + + dataBuffer = dataBuffer:gsub( "\255\253\24", "" ) + dataBuffer = dataBuffer:gsub( "\255\253\31", "" ) + dataBuffer = dataBuffer:gsub( "\255\253\34", "" ) + + dataBuffer = dataBuffer .. "\n" + + for line in dataBuffer:gmatch( "([^\n]*)\n" ) do + if lastWasChat then + mud.newlineMain() + + lastWasChat = false + end + + local clean, subs = line:gsub( GA, "" ) + local hasGA = subs ~= 0 + + local noAnsi = clean:gsub( "\27%[%d*%a", "" ) + + local gagged = gag.doGags( noAnsi ) or gag.doAnsiGags( clean ) + + action.doPreActions( noAnsi ) + action.doAnsiPreActions( clean ) + + if not gagged then + local subbed = sub.doSubs( clean ) + + for text, opts, escape in ( subbed .. "\27[m" ):gmatch( "(.-)\27%[([%d;]*)(%a)" ) do + if text ~= "" then + mud.printMain( text, fg, bg, bold ) + end + + for opt in opts:gmatch( "([^;]+)" ) do + if Escapes[ escape ][ opt ] then + Escapes[ escape ][ opt ]() + end + end + end + + if not hasGA then + mud.newlineMain() + end + end + + if hasGA then + printPendingInputs() + end + + action.doActions( noAnsi ) + action.doAnsiActions( clean ) + end + + mud.drawMain() + + dataBuffer = "" + receiving = false + end +end + +local function handleCommand( input ) + if not alias.doAlias( input ) then + if not mud.connected then + mud.print( "\n#s> You're not connected..." ) + + return + end + + if input ~= "" then + local toShow = ( showInput and input or ( "*" ):rep( input:len() ) ) .. "\n" + + if receiving then + table.insert( pendingInputs, toShow ) + else + if lastWasChat then + mud.newlineMain() + + lastWasChat = false + end + + mud.printr( toShow ) + end + end + + mud.send( input .. "\n" ) + mud.drawMain() + end +end + +local function handleInput( input ) + for command in ( input .. ";" ):gmatch( "([^;]*);" ) do + handleCommand( command ) + end +end + +mud.input = handleInput + +local function handleMacro( key, shift, ctrl, alt ) + if shift then + key = "s" .. key + end + + if control then + key = "c" .. key + end + + if alt then + key = "a" .. key + end + + macro.doMacro( key ) +end + +local function handleClose() + ev.Loop.default:unloop() + + mud.event( "shutdown" ) +end + +return { + data = handleData, + chat = handleChat, + input = handleInput, + macro = handleMacro, + interval = interval.doIntervals, + close = handleClose, +} diff --git a/interval.lua b/interval.lua @@ -0,0 +1,79 @@ +local Intervals = { } + +local function doIntervals( loop, watcher ) + local now = loop:update_now() + + for i = 1, #Intervals do + local event = Intervals[ i ] + + if event.enabled then + event:checkTick( now ) + end + end +end + +function mud.now() + return ev.Loop.default:update_now() +end + +function mud.interval( callback, interval, disabled ) + enforce( callback, "callback", "function" ) + enforce( interval, "interval", "number" ) + + local event = { + callback = callback, + interval = interval, + nextTick = mud.now(), + + enabled = not disabled, + + enable = function( self ) + self.enabled = true + end, + disable = function( self ) + self.enabled = false + end, + + tick = function( self, now ) + callback( now ) + + self.nextTick = now + self.interval + end, + + checkTick = function( self, now ) + if self.nextTick == -1 then + self.nextTick = now + end + + if now >= self.nextTick then + self:tick( now ) + end + end, + + tend = function( self, tolerance, desired ) + tolerance = tolerance or 0 + desired = desired or mud.now() + + local toTick = ( self.nextTick - desired ) % self.interval + local sinceTick = self.interval - toTick + + if toTick <= tolerance then + self.nextTick = math.avg( self.nextTick, desired ) + elseif sinceTick <= tolerance then + self.nextTick = math.avg( self.nextTick, desired + self.interval ) + else + self.nextTick = desired + + self:checkTick( mud.now() ) + end + end, + } + + table.insert( Intervals, event ) + + return event +end + +return { + doIntervals = doIntervals, +} diff --git a/macro.lua b/macro.lua @@ -0,0 +1,31 @@ +local Macros = { } + +local function doMacro( key ) + local macro = Macros[ key ] + + if macro then + macro() + end +end + +function mud.macro( key, callback, disabled ) + enforce( key, "key", "string" ) + enforce( callback, "callback", "function", "string" ) + + if type( callback ) == "string" then + local command = callback + + callback = function() + mud.input( command ) + end + end + + assert( not Macros[ key ], "macro `%s' already registered" % key ) + + + Macros[ key ] = callback +end + +return { + doMacro = doMacro, +} diff --git a/main.lua b/main.lua @@ -0,0 +1,44 @@ +mud = { + connected = false, +} + +require( "utils" ) + +socket = require( "socket" ) +ev = require( "ev" ) + +require( "event" ) + +local script = require( "script" ) +local handlers = require( "handlers" ) + +require( "chat" ).init( handlers.chat ) +require( "connect" ).init( handlers.data ) + +local xFD, handleXEvents, + printMain, newlineMain, drawMain, + printChat, newlineChat, drawChat, + setHandlers = ... + +mud.printMain = printMain +mud.newlineMain = newlineMain +mud.drawMain = drawMain + +mud.printChat = printChat +mud.newlineChat = newlineChat +mud.drawChat = drawChat + +setHandlers( handlers.input, handlers.macro, handlers.close ) + +local loop = ev.Loop.default + +mud.handleXEvents = handleXEvents + +ev.IO.new( handleXEvents, xFD, ev.READ ):start( loop ) +ev.Timer.new( handlers.interval, 0.5, 0.5 ):start( loop ) + +script.load() + +loop:loop() + +script.close() diff --git a/script.lua b/script.lua @@ -0,0 +1,139 @@ +local lfs = require( "lfs" ) +local serialize = require( "serialize" ) + +local ScriptsDir = os.getenv( "HOME" ) .. "/.mudgangster/scripts" + +package.path = package.path .. ";" .. ScriptsDir .. "/?.lua" + +local function loadScript( name, path, padding ) + local function throw( err ) + if err:match( "^" .. path ) then + err = err:sub( path:len() + 2 ) + end + + mud.print( "#lr%-" .. padding .. "s", name ) + mud.print( "#s\n>\n> %s\n>", err ) + end + + mud.print( "#s\n> " ) + + local mainPath = path .. "/main.lua" + local readable, err = io.readable( mainPath ) + + if not readable then + throw( err ) + + return + end + + local script, err = loadfile( mainPath ) + + if not script then + throw( err ) + + return + end + + local env = setmetatable( + { + require = function( requirePath, ... ) + return require( "%s.%s" % { name, requirePath }, ... ) + end, + + saved = function( defaults ) + local settingsPath = path .. "/settings.lua" + + mud.listen( "shutdown", function() + local file = io.open( settingsPath, "w" ) + + file:write( serialize.unwrapped( defaults ) ) + + file:close() + end ) + + local settings = loadfile( settingsPath ) + + if not settings then + return defaults + end + + local settingsEnv = setmetatable( { }, { + __newindex = defaults, + } ) + + setfenv( settings, settingsEnv ) + + pcall( settings ) + + return defaults + end, + }, + { + __index = _G, + __newindex = _G, + } + ) + + setfenv( script, env ) + + local loaded, err = pcall( script ) + + if loaded then + mud.print( "#lg%s", name ) + else + throw( err ) + end +end + +local function loadScripts() + mud.print( "#s> Loading scripts..." ) + + local readable, err = io.readable( ScriptsDir ) + + if readable then + local attr = lfs.attributes( ScriptsDir ) + + if attr.mode == "directory" then + local scripts = { } + local maxLen = 0 + + for script in lfs.dir( ScriptsDir ) do + if not script:match( "^%." ) then + local path = ScriptsDir .. "/" .. script + local scriptAttr = lfs.attributes( path ) + + if scriptAttr.mode == "directory" then + maxLen = math.max( script:len(), maxLen ) + + table.insert( scripts, { + name = script, + path = path, + } ) + end + end + end + + table.sort( scripts, function( a, b ) + return a.name < b.name + end ) + + for _, script in ipairs( scripts ) do + loadScript( script.name, script.path, maxLen ) + end + else + mud.print( "#sfailed!\n> `%s' isn't a directory", ScriptsDir ) + end + else + mud.print( "#sfailed!\n> %s", err ) + end + + mud.event( "scriptsLoaded" ) +end + +local function closeScripts() +end + +return { + load = loadScripts, + close = closeScripts, +} diff --git a/serialize.lua b/serialize.lua @@ -0,0 +1,56 @@ +-- can't handle cycles, only works on strings/numbers/bools/tables + +local function formatKey( key ) + if type( key ) == "string" then + return "[ %q ]" % key + end + + return "[ %s ]" % tostring( key ) +end + +local function serializeObject( obj ) + local t = type( obj ) + + if t == "number" or t == "boolean" then + return tostring( obj ) + end + + if t == "string" then + return "%q" % obj + end + + if t == "table" then + local output = "{ " + + for k, v in pairs( obj ) do + output = output .. "%s = %s, " % { formatKey( k ), serializeObject( v ) } + end + + return output .. "}" + end + + error( "I don't know how to serialize type " .. t ) +end + +local function serialize( obj ) + if not obj then + return "return { }" + end + + return "return " .. serializeObject( obj ) +end + +local function serializeUnwrapped( obj ) + local output = "" + + for k, v in pairs( obj ) do + output = output .. "%s = %s\n" % { k, serializeObject( v ) } + end + + return output +end + +return { + wrapped = serialize, + unwrapped = serializeUnwrapped, +} diff --git a/src/common.h b/src/common.h @@ -0,0 +1,102 @@ +#ifndef _COMMON_H_ +#define _COMMON_H_ + +typedef enum { false, true } bool; + +#include <X11/Xlib.h> + +#include "textbox.h" + +struct +{ + Display* display; + int screen; + + GC gc; + Colormap colorMap; + + Window window; + + TextBox* textMain; + TextBox* textChat; + + int width; + int height; +} UI; + +typedef struct +{ + int ascent; + int descent; + + short lbearing; + short rbearing; + + int height; + int width; + + XFontStruct* font; +} MudFont; + +struct +{ + ulong bg; + ulong fg; + + XColor xBG; + XColor xFG; + + ulong statusBG; + ulong statusFG; + + ulong inputBG; + ulong inputFG; + ulong cursor; + + MudFont font; + MudFont fontBold; + + union + { + struct + { + ulong black; + ulong red; + ulong green; + ulong yellow; + ulong blue; + ulong magenta; + ulong cyan; + ulong white; + + ulong lblack; + ulong lred; + ulong lgreen; + ulong lyellow; + ulong lblue; + ulong lmagenta; + ulong lcyan; + ulong lwhite; + + ulong system; + } Colours; + + ulong colours[ 2 ][ 8 ]; + }; +} Style; + +#endif // _COMMON_H_ + +#define PRETEND_TO_USE( x ) ( void ) ( x ) +#define STRL( x ) ( x ), sizeof( x ) - 1 + +#define MIN( a, b ) ( ( a ) < ( b ) ? ( a ) : ( b ) ) +#define MAX( a, b ) ( ( a ) > ( b ) ? ( a ) : ( b ) ) + +#define PADDING 3 +#define SPACING 1 + +#define OUTPUT_MAX_LINES 131072 +#define CHAT_ROWS 7 + +#define MAX_INPUT_HISTORY 128 diff --git a/src/input.c b/src/input.c @@ -0,0 +1,210 @@ +#include <stdlib.h> +#include <string.h> + +#include "common.h" +#include "input.h" +#include "script.h" + +typedef struct +{ + char* text; + int len; +} InputHistory; + +static InputHistory inputHistory[ MAX_INPUT_HISTORY ]; +static int inputHistoryHead = 0; +static int inputHistoryCount = 0; +static int inputHistoryDelta = 0; + +static char* inputBuffer = NULL; +static char* starsBuffer = NULL; + +static int inputBufferSize = 256; + +static int inputLen = 0; +static int inputPos = 0; + +void input_send() +{ + if( inputLen > 0 ) + { + InputHistory* lastCmd = &inputHistory[ ( inputHistoryHead + inputHistoryCount - 1 ) % MAX_INPUT_HISTORY ]; + + if( inputLen != lastCmd->len || strncmp( inputBuffer, lastCmd->text, inputLen ) != 0 ) + { + int pos = ( inputHistoryHead + inputHistoryCount ) % MAX_INPUT_HISTORY; + + if( inputHistoryCount == MAX_INPUT_HISTORY ) + { + free( inputHistory[ pos ].text ); + + inputHistoryHead = ( inputHistoryHead + 1 ) % MAX_INPUT_HISTORY; + } + else + { + inputHistoryCount++; + } + + inputHistory[ pos ].text = malloc( inputLen ); + + memcpy( inputHistory[ pos ].text, inputBuffer, inputLen ); + inputHistory[ pos ].len = inputLen; + } + } + + script_handleInput( inputBuffer, inputLen ); + + inputHistoryDelta = 0; + + inputLen = 0; + inputPos = 0; + + input_draw(); +} + +void input_backspace() +{ + if( inputPos > 0 ) + { + memmove( inputBuffer + inputPos - 1, inputBuffer + inputPos, inputLen - inputPos ); + + inputLen--; + inputPos--; + } + + input_draw(); +} + +void input_delete() +{ + if( inputPos < inputLen ) + { + memmove( inputBuffer + inputPos, inputBuffer + inputPos + 1, inputLen - inputPos ); + + inputLen--; + } + + input_draw(); +} + +void input_up() +{ + if( inputHistoryDelta >= inputHistoryCount ) + { + return; + } + + inputHistoryDelta++; + int pos = ( inputHistoryHead + inputHistoryCount - inputHistoryDelta ) % MAX_INPUT_HISTORY; + + InputHistory cmd = inputHistory[ pos ]; + + memcpy( inputBuffer, cmd.text, cmd.len ); + + inputLen = cmd.len; + inputPos = cmd.len; + + input_draw(); +} + +void input_down() +{ + if( inputHistoryDelta == 0 ) + { + return; + } + + inputHistoryDelta--; + + if( inputHistoryDelta != 0 ) + { + int pos = ( inputHistoryHead + inputHistoryCount - inputHistoryDelta ) % MAX_INPUT_HISTORY; + + InputHistory cmd = inputHistory[ pos ]; + + memcpy( inputBuffer, cmd.text, cmd.len ); + + inputLen = cmd.len; + inputPos = cmd.len; + } + else + { + inputLen = 0; + inputPos = 0; + } + + input_draw(); +} + +void input_left() +{ + inputPos = MAX( inputPos - 1, 0 ); + + input_draw(); +} + +void input_right() +{ + inputPos = MIN( inputPos + 1, inputLen ); + + input_draw(); +} + +void input_add( char* buffer, int len ) +{ + if( inputLen + len >= inputBufferSize ) + { + inputBufferSize *= 2; + + inputBuffer = realloc( inputBuffer, inputBufferSize ); + starsBuffer = realloc( starsBuffer, inputBufferSize ); + + memset( starsBuffer + inputBufferSize / 2, '*', inputBufferSize / 2 ); + } + + if( inputPos < inputLen ) + { + memmove( inputBuffer + inputPos + len, inputBuffer + inputPos, inputLen - inputPos ); + } + + memcpy( inputBuffer + inputPos, buffer, len ); + + inputLen += len; + inputPos += len; + + input_draw(); +} + +void input_draw() +{ + XSetFont( UI.display, UI.gc, Style.font.font->fid ); + + XSetForeground( UI.display, UI.gc, Style.bg ); + XFillRectangle( UI.display, UI.window, UI.gc, PADDING, UI.height - ( PADDING + Style.font.height ), UI.width - 6, Style.font.height ); + + XSetForeground( UI.display, UI.gc, Style.fg ); + XDrawString( UI.display, UI.window, UI.gc, PADDING, UI.height - ( PADDING + Style.font.descent ), inputBuffer, inputLen ); + + XSetForeground( UI.display, UI.gc, Style.cursor ); + XFillRectangle( UI.display, UI.window, UI.gc, PADDING + Style.font.width * inputPos, UI.height - ( PADDING + Style.font.height ), Style.font.width, Style.font.height ); + + if( inputPos < inputLen ) + { + XSetForeground( UI.display, UI.gc, Style.bg ); + XDrawString( UI.display, UI.window, UI.gc, PADDING + Style.font.width * inputPos, UI.height - ( PADDING + Style.font.descent ), inputBuffer + inputPos, 1 ); + } +} + +void input_init() +{ + inputBuffer = malloc( inputBufferSize ); + starsBuffer = malloc( inputBufferSize ); + + memset( starsBuffer, '*', inputBufferSize ); +} + +void input_end() +{ + free( inputBuffer ); + free( starsBuffer ); +} diff --git a/src/input.h b/src/input.h @@ -0,0 +1,21 @@ +#ifndef _INPUT_H_ +#define _INPUT_H_ + +void input_send(); + +void input_backspace(); +void input_delete(); + +void input_up(); +void input_down(); +void input_left(); +void input_right(); + +void input_add( char* buffer, int len ); + +void input_draw(); + +void input_init(); +void input_end(); + +#endif // _INPUT_H_ diff --git a/src/main.c b/src/main.c @@ -0,0 +1,29 @@ +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> + +#include <X11/Xutil.h> +#include <X11/XKBlib.h> +#include <X11/cursorfont.h> +#include <X11/keysym.h> + +#include "common.h" +#include "script.h" +#include "input.h" +#include "ui.h" + +int main() +{ + ui_init(); + input_init(); + script_init(); + + // main loop is done in lua + + script_end(); + input_end(); + ui_end(); + + return EXIT_SUCCESS; +} diff --git a/src/script.c b/src/script.c @@ -0,0 +1,177 @@ +#include <stdlib.h> +#include <assert.h> + +#include <lua.h> +#include <lualib.h> +#include <lauxlib.h> + +#include "common.h" +#include "ui.h" + +static lua_State* L; + +static int inputHandlerIdx = LUA_NOREF; +static int macroHandlerIdx = LUA_NOREF; +static int closeHandlerIdx = LUA_NOREF; + +void script_handleInput( char* buffer, int len ) +{ + assert( inputHandlerIdx != LUA_NOREF ); + + lua_rawgeti( L, LUA_REGISTRYINDEX, inputHandlerIdx ); + lua_pushlstring( L, buffer, len ); + + lua_call( L, 1, 0 ); +} + +void script_doMacro( char* key, int len, bool shift, bool ctrl, bool alt ) +{ + lua_rawgeti( L, LUA_REGISTRYINDEX, macroHandlerIdx ); + + lua_pushlstring( L, key, len ); + + lua_pushboolean( L, shift ); + lua_pushboolean( L, ctrl ); + lua_pushboolean( L, alt ); + + lua_call( L, 4, 0 ); +} + +void script_handleClose() +{ + assert( closeHandlerIdx != LUA_NOREF ); + + lua_rawgeti( L, LUA_REGISTRYINDEX, closeHandlerIdx ); + + lua_call( L, 0, 0 ); +} + +static int mud_handleXEvents( lua_State* L ) +{ + PRETEND_TO_USE( L ); + + ui_handleXEvents(); + + return 0; +} + +static int mud_printMain( lua_State* L ) +{ + const char* str = luaL_checkstring( L, 1 ); + size_t len = lua_objlen( L, 1 ); + + Colour fg = luaL_checkint( L, 2 ); + Colour bg = luaL_checkint( L, 3 ); + bool bold = lua_toboolean( L, 4 ); + + textbox_add( UI.textMain, str, len, fg, bg, bold ); + + return 0; +} + +static int mud_newlineMain( lua_State* L ) +{ + PRETEND_TO_USE( L ); + + textbox_newline( UI.textMain ); + + return 0; +} + +static int mud_drawMain( lua_State* L ) +{ + PRETEND_TO_USE( L ); + + textbox_draw( UI.textMain ); + + return 0; +} + +static int mud_printChat( lua_State* L ) +{ + const char* str = luaL_checkstring( L, 1 ); + size_t len = lua_objlen( L, 1 ); + + Colour fg = luaL_checkint( L, 2 ); + Colour bg = luaL_checkint( L, 3 ); + bool bold = lua_toboolean( L, 4 ); + + textbox_add( UI.textChat, str, len, fg, bg, bold ); + + return 0; +} + +static int mud_newlineChat( lua_State* L ) +{ + PRETEND_TO_USE( L ); + + textbox_newline( UI.textChat ); + + return 0; +} + +static int mud_drawChat( lua_State* L ) +{ + PRETEND_TO_USE( L ); + + textbox_draw( UI.textChat ); + + return 0; +} + +static int mud_setHandlers( lua_State* L ) +{ + luaL_argcheck( L, lua_type( L, 1 ) == LUA_TFUNCTION, 1, "expected function" ); + luaL_argcheck( L, lua_type( L, 2 ) == LUA_TFUNCTION, 2, "expected function" ); + luaL_argcheck( L, lua_type( L, 3 ) == LUA_TFUNCTION, 3, "expected function" ); + + closeHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX ); + macroHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX ); + inputHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX ); + + return 0; +} + +void script_init() +{ + mud_handleXEvents( NULL ); + + L = lua_open(); + luaL_openlibs( L ); + + lua_getglobal( L, "debug" ); + lua_getfield( L, -1, "traceback" ); + lua_remove( L, -2 ); + + if( luaL_loadfile( L, "main.lua" ) ) + { + printf( "Error reading main.lua: %s\n", lua_tostring( L, -1 ) ); + + exit( 1 ); + } + + lua_pushinteger( L, ConnectionNumber( UI.display ) ); + lua_pushcfunction( L, mud_handleXEvents ); + + lua_pushcfunction( L, mud_printMain ); + lua_pushcfunction( L, mud_newlineMain ); + lua_pushcfunction( L, mud_drawMain ); + + lua_pushcfunction( L, mud_printChat ); + lua_pushcfunction( L, mud_newlineChat ); + lua_pushcfunction( L, mud_drawChat ); + + lua_pushcfunction( L, mud_setHandlers ); + + if( lua_pcall( L, 9, 0, -11 ) ) + { + printf( "Error running main.lua: %s\n", lua_tostring( L, -1 ) ); + + exit( 1 ); + } +} + +void script_end() +{ + lua_close( L ); +} diff --git a/src/script.h b/src/script.h @@ -0,0 +1,11 @@ +#ifndef _SCRIPT_H_ +#define _SCRIPT_H_ + +void script_handleInput( char* buffer, int len ); +void script_doMacro( char* key, int len, bool shift, bool ctrl, bool alt ); +void script_handleClose(); + +void script_init(); +void script_end(); + +#endif // _SCRIPT_H_ diff --git a/src/textbox.c b/src/textbox.c @@ -0,0 +1,200 @@ +#include <stdlib.h> +#include <string.h> +#include <assert.h> + +#include <X11/Xlib.h> + +#include "common.h" +#include <stdio.h> + +TextBox* textbox_new( unsigned int maxLines ) +{ + TextBox* textbox = calloc( sizeof( TextBox ), 1 ); + + assert( textbox != NULL ); + + textbox->lines = malloc( maxLines * sizeof( Line ) ); + textbox->numLines = -1; + textbox->maxLines = maxLines; + textbox->scrollDelta = 0; + + Line* line = &textbox->lines[ 0 ]; + + line->head = line->tail = calloc( sizeof( Text ), 1 ); + + return textbox; +} + +void textbox_freeline( TextBox* self, int idx ) +{ + Text* node = self->lines[ idx ].head; + + while( node != NULL ) + { + Text* next = node->next; + + free( node->buffer ); + free( node ); + + node = next; + } +} + +void textbox_free( TextBox* self ) +{ + for( int i = 0; i < self->numLines; i++ ) + { + textbox_freeline( self, i ); + } + + free( self ); +} + +void textbox_setpos( TextBox* self, int x, int y ) +{ + self->x = x; + self->y = y; +} + +void textbox_setsize( TextBox* self, int width, int height ) +{ + self->width = width; + self->height = height; + + self->rows = height / ( Style.font.height + SPACING ); + self->cols = width / Style.font.width; +} + +void textbox_add( TextBox* self, const char* str, unsigned int len, Colour fg, Colour bg, bool bold ) +{ + if( self->numLines == -1 ) + { + self->numLines = 0; + } + + Line* line = &self->lines[ ( self->head + self->numLines ) % self->maxLines ]; + + Text* node = malloc( sizeof( Text ) ); + node->buffer = malloc( len ); + + memcpy( node->buffer, str, len ); + node->len = len; + + node->fg = fg; + node->bg = bg; + node->bold = bold; + + node->next = NULL; + + line->tail->next = node; + line->tail = node; + + line->len += len; +} + +void textbox_newline( TextBox* self ) +{ + if( self->numLines < self->maxLines ) + { + self->numLines++; + + Line* line = &self->lines[ ( self->head + self->numLines ) % self->maxLines ]; + + line->head = line->tail = calloc( sizeof( Text ), 1 ); + + if( self->scrollDelta != 0 ) + { + self->scrollDelta++; + } + } + else + { + textbox_freeline( self, self->head ); + + Line* line = &self->lines[ self->head ]; + + line->head = line->tail = calloc( sizeof( Text ), 1 ); + + self->head = ( self->head + 1 ) % self->maxLines; + } +} + +void textbox_draw( TextBox* self ) +{ + XSetForeground( UI.display, UI.gc, Style.bg ); + XFillRectangle( UI.display, UI.window, UI.gc, self->x, self->y, self->width, self->height ); + + int rowsRemaining = self->rows; + int linesRemaining = self->numLines - self->scrollDelta + 1; + + int lineTop = self->y + self->height; + int linesPrinted = 0; + + int firstLine = self->head + self->numLines - self->scrollDelta; + + while( rowsRemaining > 0 && linesRemaining > 0 ) + { + Line* line = &self->lines[ ( firstLine - linesPrinted ) % self->maxLines ]; + + int lineHeight = ( line->len / self->cols ); + + if( line->len % self->cols != 0 || line->len == 0 ) + { + lineHeight++; + } + + lineTop -= lineHeight * ( Style.font.height + SPACING ); + + Text* node = line->head->next; + int row = 0; + int col = 0; + + while( node != NULL ) + { + if( node->len != 0 ) + { + XSetFont( UI.display, UI.gc, ( node->bold ? Style.fontBold : Style.font ).font->fid ); + XSetForeground( UI.display, UI.gc, + node->fg == SYSTEM ? Style.Colours.system : Style.colours[ node->bold ][ node->fg ] ); + + // TODO: draw bg + + int remainingChars = node->len; + int pos = 0; + + while( remainingChars > 0 ) + { + int charsToPrint = MIN( remainingChars, self->cols - col ); + + int x = self->x + ( Style.font.width * col ); + int y = lineTop + ( ( Style.font.height + SPACING ) * row ) + Style.font.ascent + SPACING; + + if( y >= self->y ) + { + XDrawString( UI.display, UI.window, UI.gc, x, y, node->buffer + pos, charsToPrint ); + } + + pos += charsToPrint; + remainingChars -= charsToPrint; + + if( remainingChars != 0 ) + { + col = 0; + row++; + } + else + { + col += charsToPrint; + } + } + } + + node = node->next; + } + + rowsRemaining -= lineHeight; + linesRemaining--; + + linesPrinted++; + } +} diff --git a/src/textbox.h b/src/textbox.h @@ -0,0 +1,66 @@ +#ifndef _TEXTBOX_H_ +#define _TEXTBOX_H_ + +typedef enum +{ + BLACK = 0, + RED, + GREEN, + YELLOW, + BLUE, + MAGENTA, + CYAN, + WHITE, + SYSTEM, + NONE, +} Colour; + +typedef struct Text Text; +struct Text +{ + char* buffer; + unsigned int len; + + Colour fg; + Colour bg; + bool bold; + + Text* next; +}; + +typedef struct +{ + Text* head; + Text* tail; + + unsigned int len; +} Line; + +typedef struct +{ + Line* lines; + + int maxLines; + int numLines; + int head; + + int x; + int y; + int width; + int height; + + int rows; + int cols; + + int scrollDelta; +} TextBox; + +TextBox* textbox_new( unsigned int maxLines ); +void textbox_free( TextBox* self ); +void textbox_setpos( TextBox* self, int x, int y ); +void textbox_setsize( TextBox* self, int width, int height ); +void textbox_add( TextBox* self, const char* str, unsigned int len, Colour fg, Colour bg, bool bold ); +void textbox_newline( TextBox* self ); +void textbox_draw( TextBox* self ); + +#endif // _TEXTBOX_H_ diff --git a/src/ui.c b/src/ui.c @@ -0,0 +1,383 @@ +#include <stdio.h> +#include <stdlib.h> +#include <assert.h> + +#include <X11/Xutil.h> +#include <X11/XKBlib.h> +#include <X11/cursorfont.h> +#include <X11/keysym.h> + +#include "common.h" +#include "input.h" +#include "script.h" + +Atom wmDeleteWindow; + +void statusDraw() +{ + XSetForeground( UI.display, UI.gc, Style.statusBG ); + XFillRectangle( UI.display, UI.window, UI.gc, 0, UI.height - ( PADDING * 4 ) - ( Style.font.height * 2 ), UI.width, Style.font.height + ( PADDING * 2 ) ); + + XSetForeground( UI.display, UI.gc, Style.statusFG ); + XDrawString( UI.display, UI.window, UI.gc, PADDING, UI.height - ( PADDING * 3 ) - Style.font.height - Style.font.descent, STRL( "legit statusbar" ) ); + + // TODO +} + +void ui_draw() +{ + XClearWindow( UI.display, UI.window ); + + input_draw(); + statusDraw(); + + textbox_draw( UI.textChat ); + textbox_draw( UI.textMain ); +} + +void eventButtonPress( XEvent* event ) +{ + PRETEND_TO_USE( event ); +} + +void eventButtonRelease( XEvent* event ) +{ + PRETEND_TO_USE( event ); +} + +void eventMessage( XEvent* event ) +{ + if( ( Atom ) event->xclient.data.l[ 0 ] == wmDeleteWindow ) + { + script_handleClose(); + } +} + +void eventResize( XEvent* event ) +{ + int newWidth = event->xconfigure.width; + int newHeight = event->xconfigure.height; + + if( newWidth == UI.width && newHeight == UI.height ) + { + return; + } + + UI.width = newWidth; + UI.height = newHeight; + + XSetForeground( UI.display, UI.gc, Style.bg ); + XFillRectangle( UI.display, UI.window, UI.gc, 0, 0, UI.width, UI.height ); + + textbox_setpos( UI.textChat, PADDING, PADDING ); + textbox_setsize( UI.textChat, UI.width - ( 2 * PADDING ), ( Style.font.height + SPACING ) * CHAT_ROWS ); + + textbox_setpos( UI.textMain, PADDING, ( PADDING * 2 ) + CHAT_ROWS * ( Style.font.height + SPACING ) ); + textbox_setsize( UI.textMain, UI.width - ( 2 * PADDING ), UI.height + - ( ( ( Style.font.height + SPACING ) * CHAT_ROWS ) + ( PADDING * 2 ) ) + - ( ( Style.font.height * 2 ) + ( PADDING * 5 ) ) + ); +} + +void eventExpose( XEvent* event ) +{ + PRETEND_TO_USE( event ); + + ui_draw(); +} + +void eventKeyPress( XEvent* event ) +{ + #define ADD_MACRO( key, name ) \ + case key: \ + script_doMacro( name, sizeof( name ) - 1, shift, ctrl, alt ); \ + break + + XKeyEvent* keyEvent = &event->xkey; + + char keyBuffer[ 32 ]; + KeySym key; + + bool shift = keyEvent->state & ShiftMask; + bool ctrl = keyEvent->state & ControlMask; + bool alt = keyEvent->state & Mod1Mask; + + int len = XLookupString( keyEvent, keyBuffer, sizeof( keyBuffer ), &key, NULL ); + + switch( key ) + { + case XK_Return: + { + input_send(); + + break; + } + + case XK_BackSpace: + input_backspace(); + + break; + + case XK_Delete: + input_delete(); + + break; + + case XK_Page_Up: + if( UI.textMain->scrollDelta < UI.textMain->numLines ) + { + int toScroll = shift ? 1 : ( UI.textMain->rows - 2 ); + + UI.textMain->scrollDelta = MIN( UI.textMain->scrollDelta + toScroll, UI.textMain->numLines ); + + textbox_draw( UI.textMain ); + } + + break; + + case XK_Page_Down: + if( UI.textMain->scrollDelta > 0 ) + { + int toScroll = shift ? 1 : ( UI.textMain->rows - 2 ); + + UI.textMain->scrollDelta = MAX( UI.textMain->scrollDelta - toScroll, 0 ); + + textbox_draw( UI.textMain ); + } + + break; + + case XK_Up: + input_up(); + + break; + case XK_Down: + input_down(); + + break; + + case XK_Left: + input_left(); + + break; + + case XK_Right: + input_right(); + + break; + + ADD_MACRO( XK_KP_1, "kp1" ); + ADD_MACRO( XK_KP_End, "kp1" ); + + ADD_MACRO( XK_KP_2, "kp2" ); + ADD_MACRO( XK_KP_Down, "kp2" ); + + ADD_MACRO( XK_KP_3, "kp3" ); + ADD_MACRO( XK_KP_Page_Down, "kp3" ); + + ADD_MACRO( XK_KP_4, "kp4" ); + ADD_MACRO( XK_KP_Left, "kp4" ); + + ADD_MACRO( XK_KP_5, "kp5" ); + ADD_MACRO( XK_KP_Begin, "kp5" ); + + ADD_MACRO( XK_KP_6, "kp6" ); + ADD_MACRO( XK_KP_Right, "kp6" ); + + ADD_MACRO( XK_KP_7, "kp7" ); + ADD_MACRO( XK_KP_Home, "kp7" ); + + ADD_MACRO( XK_KP_8, "kp8" ); + ADD_MACRO( XK_KP_Up, "kp8" ); + + ADD_MACRO( XK_KP_9, "kp9" ); + ADD_MACRO( XK_KP_Page_Up, "kp9" ); + + ADD_MACRO( XK_KP_0, "kp0" ); + ADD_MACRO( XK_KP_Insert, "kp0" ); + + ADD_MACRO( XK_KP_Multiply, "kp*" ); + ADD_MACRO( XK_KP_Divide, "kp/" ); + ADD_MACRO( XK_KP_Subtract, "kp-" ); + ADD_MACRO( XK_KP_Add, "kp+" ); + + ADD_MACRO( XK_KP_Delete, "kp." ); + ADD_MACRO( XK_KP_Decimal, "kp." ); + + ADD_MACRO( XK_F1, "f1" ); + ADD_MACRO( XK_F2, "f2" ); + ADD_MACRO( XK_F3, "f3" ); + ADD_MACRO( XK_F4, "f4" ); + ADD_MACRO( XK_F5, "f5" ); + ADD_MACRO( XK_F6, "f6" ); + ADD_MACRO( XK_F7, "f7" ); + ADD_MACRO( XK_F8, "f8" ); + ADD_MACRO( XK_F9, "f9" ); + ADD_MACRO( XK_F10, "f10" ); + ADD_MACRO( XK_F11, "f11" ); + ADD_MACRO( XK_F12, "f12" ); + + default: + if( ctrl || alt ) + { + script_doMacro( keyBuffer, len, shift, ctrl, alt ); + } + else + { + input_add( keyBuffer, len ); + } + + break; + } + + #undef ADD_MACRO +} + +void ( *EventHandler[ LASTEvent ] ) ( XEvent* ) = +{ + [ ButtonPress ] = eventButtonPress, + [ ButtonRelease ] = eventButtonRelease, + [ ClientMessage ] = eventMessage, + [ ConfigureNotify ] = eventResize, + [ Expose ] = eventExpose, + [ KeyPress ] = eventKeyPress, +}; + +void ui_handleXEvents() +{ + while( XPending( UI.display ) ) + { + XEvent event; + XNextEvent( UI.display, &event ); + + if( EventHandler[ event.type ] ) + { + EventHandler[ event.type ]( &event ); + } + } +} + +MudFont loadFont( char* fontStr ) +{ + MudFont font; + + font.font = XLoadQueryFont( UI.display, fontStr ); + + if( !font.font ) + { + printf( "could not load font %s\n", fontStr ); + + exit( 1 ); + } + + font.ascent = font.font->ascent; + font.descent = font.font->descent; + font.lbearing = font.font->min_bounds.lbearing; + font.rbearing = font.font->max_bounds.rbearing; + + font.width = font.rbearing - font.lbearing; + font.height = font.ascent + font.descent; + + return font; +} + +void initStyle() +{ + #define SETCOLOR( x, c ) \ + do { \ + XColor color; \ + XAllocNamedColor( UI.display, UI.colorMap, c, &color, &color ); \ + x = color.pixel; \ + } while( false ) + #define SETXCOLOR( x, y, c ) \ + do { \ + XColor color; \ + XAllocNamedColor( UI.display, UI.colorMap, c, &color, &color ); \ + x = color.pixel; \ + y = color; \ + } while( false ) + + SETXCOLOR( Style.bg, Style.xBG, "#1a1a1a" ); + SETXCOLOR( Style.fg, Style.xFG, "#b6c2c4" ); + + SETCOLOR( Style.cursor, "#00ff00" ); + + SETCOLOR( Style.statusBG, "#333333" ); + SETCOLOR( Style.statusFG, "#ffffff" ); + + SETCOLOR( Style.Colours.black, "#1a1a1a" ); + SETCOLOR( Style.Colours.red, "#ca4433" ); + SETCOLOR( Style.Colours.green, "#178a3a" ); + SETCOLOR( Style.Colours.yellow, "#dc7c2a" ); + SETCOLOR( Style.Colours.blue, "#415e87" ); + SETCOLOR( Style.Colours.magenta, "#5e468c" ); + SETCOLOR( Style.Colours.cyan, "#35789b" ); + SETCOLOR( Style.Colours.white, "#b6c2c4" ); + + SETCOLOR( Style.Colours.lblack, "#666666" ); + SETCOLOR( Style.Colours.lred, "#ff2954" ); + SETCOLOR( Style.Colours.lgreen, "#5dd030" ); + SETCOLOR( Style.Colours.lyellow, "#fafc4f" ); + SETCOLOR( Style.Colours.lblue, "#3581e1" ); + SETCOLOR( Style.Colours.lmagenta, "#875fff" ); + SETCOLOR( Style.Colours.lcyan, "#29fbff" ); + SETCOLOR( Style.Colours.lwhite, "#cedbde" ); + + SETCOLOR( Style.Colours.system, "#ffffff" ); + + #undef SETCOLOR + #undef SETXCOLOR + + Style.font = loadFont( "-windows-dina-medium-r-normal--12-120-75-75-c-0-microsoft-cp1252" ); + Style.fontBold = loadFont( "-windows-dina-bold-r-normal--12-120-75-75-c-0-microsoft-cp1252" ); +} + +void ui_init() +{ + UI.display = XOpenDisplay( NULL ); + UI.screen = XDefaultScreen( UI.display ); + UI.width = -1; + UI.height = -1; + + Window root = XRootWindow( UI.display, UI.screen ); + int depth = XDefaultDepth( UI.display, UI.screen ); + Visual* visual = XDefaultVisual( UI.display, UI.screen ); + UI.colorMap = XDefaultColormap( UI.display, UI.screen ); + + UI.textMain = textbox_new( OUTPUT_MAX_LINES ); + UI.textChat = textbox_new( OUTPUT_MAX_LINES ); + + initStyle(); + + XSetWindowAttributes attr = + { + .background_pixel = Style.bg, + .event_mask = ExposureMask | StructureNotifyMask | KeyPressMask | ButtonPressMask | ButtonReleaseMask, + .colormap = UI.colorMap, + }; + + UI.window = XCreateWindow( UI.display, root, 0, 0, 800, 600, 0, depth, InputOutput, visual, CWBackPixel | CWEventMask | CWColormap, &attr ); + UI.gc = XCreateGC( UI.display, UI.window, 0, NULL ); + + Cursor cursor = XCreateFontCursor( UI.display, XC_xterm ); + XDefineCursor( UI.display, UI.window, cursor ); + XRecolorCursor( UI.display, cursor, &Style.xFG, &Style.xBG ); + + XStoreName( UI.display, UI.window, "Mud Gangster" ); + XMapWindow( UI.display, UI.window ); + + wmDeleteWindow = XInternAtom( UI.display, "WM_DELETE_WINDOW", false ); + XSetWMProtocols( UI.display, UI.window, &wmDeleteWindow, 1 ); +} + +void ui_end() +{ + textbox_free( UI.textMain ); + + XFreeFont( UI.display, Style.font.font ); + XFreeFont( UI.display, Style.fontBold.font ); + + XFreeGC( UI.display, UI.gc ); + XDestroyWindow( UI.display, UI.window ); + XCloseDisplay( UI.display ); +} diff --git a/src/ui.h b/src/ui.h @@ -0,0 +1,11 @@ +#ifndef _UI_H_ +#define _UI_H_ + +void ui_handleXEvents(); + +void ui_draw(); + +void ui_init(); +void ui_end(); + +#endif // _UI_H_ diff --git a/sub.lua b/sub.lua @@ -0,0 +1,44 @@ +local Subs = { } + +local function doSubs( line ) + for i = 1, #Subs do + local sub = Subs[ i ] + + if sub.enabled then + local newLine, subs = line:gsub( sub.pattern, sub.replacement ) + + if subs ~= 0 then + return newLine + end + end + end + + return line +end + +function mud.sub( pattern, replacement, disabled ) + enforce( pattern, "pattern", "string" ) + enforce( replacement, "replacement", "string", "function" ) + + local sub = { + pattern = pattern, + replacement = replacement, + + enabled = not disabled, + + enable = function( self ) + self.enabled = true + end, + disable = function( self ) + self.enabled = false + end, + } + + table.insert( Subs, sub ) + + return sub +end + +return { + doSubs = doSubs, +} diff --git a/utils.lua b/utils.lua @@ -0,0 +1,192 @@ +getmetatable( "" ).__mod = function( self, form ) + if type( form ) == "table" then + return self:format( unpack( form ) ) + end + + return self:format( form ) +end + +function string.plural( count, plur, sing ) + return count == 1 and ( sing or "" ) or ( plur or "s" ) +end + +function string.startsWith( self, needle ) + return self:sub( 1, needle:len() ) == needle +end + +function string.commas( num ) + num = tonumber( num ) + + local out = "" + + while num > 1000 do + out = ( ",%03d%s" ):format( num % 1000, out ) + + num = math.floor( num / 1000 ) + end + + return tostring( num ) .. out +end + +function math.round( num ) + return math.floor( num + 0.5 ) +end + +function math.avg( a, b ) + return ( a + b ) / 2 +end + +function io.readable( path ) + local file, err = io.open( path, "r" ) + + if not file then + return false, err + end + + io.close( file ) + + return true +end + +function enforce( var, name, ... ) + local acceptable = { ... } + local ok = false + + for _, accept in ipairs( acceptable ) do + if type( var ) == accept then + ok = true + + break + end + end + + if not ok then + error( "argument `%s' to %s should be of type %s (got %s)" % { name, debug.getinfo( 2, "n" ).name, table.concat( acceptable, " or " ), type( var ) }, 3 ) + end +end + +local function genericPrint( message, main, chat ) + local bold = false + local fg = 7 + local bg = 0 + + local function setFG( colour ) + return function() + fg = colour + end + end + + local Sequences = { + d = setFG( 7 ), + r = setFG( 1 ), + g = setFG( 2 ), + y = setFG( 3 ), + b = setFG( 4 ), + m = setFG( 5 ), + c = setFG( 6 ), + w = setFG( 7 ), + s = setFG( 8 ), + } + + for line, newLine in message:gmatch( "([^\n]*)(\n?)" ) do + for text, hashPos, light, colour, colourPos in ( line .. "#a" ):gmatch( "(.-)()#(l?)(%l)()" ) do + if main then + mud.printMain( text:gsub( "##", "#" ), fg, bg, bold ) + end + + if chat then + mud.printChat( text:gsub( "##", "#" ), fg, bg, bold ) + end + + if line:sub( hashPos - 1, hashPos - 1 ) ~= "#" then + if colourPos ~= line:len() + 3 then + assert( Sequences[ colour ], "invalid colour sequence: #" .. light .. colour ) + + bold = light == "l" + Sequences[ colour ]() + end + else + if main then + mud.printMain( light .. colour, fg, bg, bold ) + end + + if chat then + mud.printChat( light .. colour, fg, bg, bold ) + end + end + end + + if newLine ~= "" then + if main then + mud.newlineMain() + end + + if chat then + mud.newlineChat() + end + end + end + + if main then + mud.drawMain() + end + + if chat then + mud.drawChat() + end +end + +function mud.print( form, ... ) + genericPrint( form:format( ... ), true, false ) +end + +function mud.printc( form, ... ) + genericPrint( form:format( ... ), false, true ) +end + +function mud.printb( form, ... ) + genericPrint( form:format( ... ), true, true ) +end + +function mud.printr( str, fg, bg, bold ) + fg = fg or 7 + bg = bg or 0 + bold = bold or false + + for line, newLine in str:gmatch( "([^\n]*)(\n?)" ) do + mud.printMain( line, fg, bg, bold ) + + if newLine ~= "" then + mud.newlineMain() + end + end + + mud.drawMain() +end + +local ColourSequences = { + d = 0, + r = 31, + g = 32, + y = 33, + b = 34, + m = 35, + c = 36, + w = 37, +} + +function string.parseColours( message ) + message = assert( tostring( message ) ) + + return ( message:gsub( "()#(l?)(%l)", function( patternPos, bold, sequence ) + if message:sub( patternPos - 1, patternPos - 1 ) == "#" then + return + end + + if bold == "l" then + return "\27[1m\27[%dm" % { ColourSequences[ sequence ] } + end + + return "\27[%sm\27[0m" % { ColourSequences[ sequence ] } + end ):gsub( "##", "#" ) ) +end