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