--[[
lama is a MUD server made in Lua.
Copyright (C) 2013 Curtis Erickson
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
]]
--- Singleton that provides the necessary operations for running the game.
-- @author milkmanjack
local Event = require("obj.Event")
local Scheduler = require("obj.Scheduler")
local Server = require("obj.Server")
local Client = require("obj.Client")
local Player = require("obj.Player")
local Mob = require("obj.Mob")
local Map = require("obj.Map")
local CommandParser = require("obj.CommandParser")
--- Singleton that provides the necessary operations for running the game.
-- @class table
-- @name Game
-- @field name The name we would like to be called.
-- @field version The version of this release.
-- @field defaultPort The port we'll use if one isn't provided.
-- @field state Our state. Always a member of the GameState table.
-- @field playerID Unique ID to be assigned to the next new Player.
-- @field server Server we're using.
-- @field scheduler Scheduler we're using.
-- @field parser CommandParser we're using.
-- @field map The Map we'll be using.
-- @field logger Logger that prints to the standard output.
-- @field fileLogger Logger that prints to the standard log file for this session.
-- @field commandLogger Logger that prints all command input to the standard command log file for this session.
Game = {}
-- game data
Game.name = "lama"
Game.version = "v0.8a"
Game.developers = {"milkmanjack"}
-- current game info
Game.state = GameState.NEW
Game.server = nil
Game.scheduler = nil
Game.map = nil
Game.parser = nil
--- Contains all Players connected to the game.
-- @class table
-- @name Game.players
Game.playerID = 0
Game.players = {}
-- loggers for the game
Game.logger = logging.console()
Game.fileLogger = logging.file("log/%s.log", "%m%d%y")
Game.commandLogger = logging.file("log/%s-commands.log", "%m%d%y")
Game.fileLogger:setLevel(logging.DEBUG)
Game.commandLogger:setLevel(logging.DEBUG)
--- Open the game for play on the given port.
-- @param port The port to be hosted on. Defaults to Game.defaultPort{@link Game.defaultPort}.
-- @return true on success.<br/>false followed by an error otherwise.
function Game.openOnPort(port)
Game.info(string.format("Preparing to host game server on port %d...", port))
Game.server = Server:new()
local _, err = Game.server:host(port)
if not _ then
return false, err
end
Game.onOpen()
return true
end
--- Open the game for play on the given server.
-- @param server Server to be used by the Game as opposed to setting up a new one. Used when hotbooting.
-- @return true on success.<br/>false followed by an error otherwise.
function Game.openOnServer(server)
Game.info("Preparing to host game on existing server.")
Game.server = server
Game.onOpen()
return true
end
-- Specifies further action after opening the game.
function Game.onOpen()
-- load the scheduler
Game.scheduler = Scheduler:new()
Game.info("Preparing scheduler...")
Game.queue(Game.AcceptEvent:new(Game.time()))
Game.queue(Game.PollEvent:new(Game.time()))
-- load the map
Game.info("Loading map...")
Game.map = dofile("data/map/worldmap.lua")
-- load the parser
Game.parser = CommandParser:new()
Game.info("Generating commands...")
Game.generateCommands()
-- load other database stuff
Game.info("Loading races...")
DatabaseManager.loadRaces()
Game.info("Loading classes...")
DatabaseManager.loadClasses()
Game.setState(GameState.READY)
Game.info("Game is ready for business...")
end
--- Shutdown the game.
-- @return true on success.<br/>false followed by an error otherwise.
function Game.shutdown()
if not Game.isReady() then
return false, "Game not running"
end
Game.info("Shutting down game...")
for i,v in table.safeIPairs(Game:getPlayers()) do
Game.disconnectPlayer(v)
end
Game.setState(GameState.SHUTDOWN)
Game.server:close()
Game.info("Game is shutdown!")
return true
end
--- Hotboot the game.
-- The Game and all of the pertinent data will be recycled during a hotboot, so don't worry too much.
-- The main focus is on preserving the server socket, client sockets, and player mobs.
-- @return true on success.<br/>false followed by an error otherwise.
function Game.hotboot()
Game.info("*** Preparing for hotboot...")
Game.setState(GameState.HOTBOOT)
return true
end
--- Recovers old players after a hotboot.
-- @param preservedData A table containing formatted tables that
-- are used to reconstitute old players. Most importantly, their
-- sockets are included in this table, but also things like their
-- client options, and even mob IDs for loading temporary player
-- files.
function Game.recoverFromHotboot(preservedData)
for i,v in ipairs(preservedData) do
-- load new client and restore options.
local client = Client:new(v.socket, false)
client.options = v.options
-- load new player
local player = Player:new(client)
player:setID(v.id)
Game.connectPlayer(player, true)
local mob, location = DatabaseManager.loadCharacter(v.name)
player:setMob(mob)
mob:moveToMap(Game.map)
mob:move(location)
Game.info(string.format("*** Character loaded: %s->%s", tostring(mob), tostring(player)))
end
end
--- Updates the game as necessary. Things like updating the scheduler and such.
function Game.update()
Game.scheduler:poll(Game.time())
end
--- Connects a Player.<br/>
-- Calls Game.onPlayerConnect(player) before adding to players list.
-- @param player The Player to be connected.
-- @param hotboot If true, player is reconnecting after hotboot.
function Game.connectPlayer(player, hotboot)
Game.onPlayerConnect(player, hotboot)
table.insert(Game.players, player)
end
--- Specifies further actions for a connecting Player.
-- @param player The Player connecting.
-- @param hotboot If true, player is reconnecting after hotboot.
function Game.onPlayerConnect(player, hotboot)
if hotboot then
Game.info(string.format("*** Reconnecting %s after hotboot.", tostring(player:getClient())))
else
Game.info(string.format("Connected %s!", tostring(player:getClient())))
end
Nanny.greet(player, hotboot)
end
--- Disconnect a Player.<br/>
-- Calls Game.onPlayerDisconnect(player) before removing from players list or destroying client.
-- @param player The Player to be disconnected.
function Game.disconnectPlayer(player)
Game.onPlayerDisconnect(player) -- disconnect the player
table.removeValue(Game.players, player) -- remove from players list (no longer recognized as a player)
-- if they're not playing yet, then disconnect them.
-- otherwise, only disconnect if it's not a hotboot.
if player:getState() ~= PlayerState.PLAYING then
Game.server:disconnectClient(player:getClient()) -- kill the client
end
end
--- Specifies further actions for a disconnecting Player.
-- @param player The Player disconnecting.
function Game.onPlayerDisconnect(player)
Game.info(string.format("Disconnected %s!", tostring(player:getClient())))
if player:getState() == PlayerState.PLAYING then
Nanny.logout(player)
end
end
--- Determine what to do with input given by a Player.
-- @param player The Player providing the input.
-- @param input The input to be processed.
function Game.onPlayerInput(player, input)
Game.logCommand(player, input)
-- this assumes that there will be echoing at some stage
-- in the future, only do this if we know there will be echoing
player:clearMessageMode()
-- in-between states
if player:getState() ~= PlayerState.PLAYING then
Nanny.process(player, input)
return
end
-- command parsing for in-game players
Game.parser:parse(player, player:getMob(), input)
end
--- Generates a list of every Command for the CommandParser.<br/>
-- This probably makes more sense as part of the database manager. Not sure yet.
function Game.generateCommands()
for i in lfs.dir("src/obj/command") do
if i ~= "." and i ~= ".." then
local file = string.match(i, "(.+)%.lua")
if file then -- it's an lua file!
local package = string.format("obj.command.%s", file)
if package ~= "obj.command.Movement" then
local command = require(package)
Game.parser:addCommand(command:new())
end
end
end
end
end
--- Announce something to connecting Players.<br/>
-- This is mostly temporary, but I'm leaving it in now for testing purposes.<br/>
-- More reasonable implementation later.
-- @param message The message to be announced.
-- @param mode The mode of the message.<br/>Must be a valid member of MessageMode.
-- @param minState The state a Player must be at to see the message (or "higher").
function Game.announce(message, mode, minState)
for i,v in ipairs(Game.getPlayers()) do
if not minState or v:getState() >= minState then
v:sendMessage(message, mode)
end
end
end
--- Shortcut for Game.scheduler:queue(event)
-- @param event Event to queue.
function Game.queue(event)
Game.scheduler:queue(event)
end
--- Shortcut for Game.scheduler:deque(event)
-- @param event Event to deque.
function Game.deque(event)
Game.scheduler:deque(event)
end
--- Shortcut to Game.logger:log(level,message)
-- @param level Level of this log.
-- @param message Message to be logged.
function Game.log(level, message)
Game.logger:log(level, message)
Game.fileLogger:log(level, message)
end
--- Shortcut to Game.logger:debug(message)
-- @param message Message to be logged as debug.
function Game.debug(message)
Game.logger:debug(message)
Game.fileLogger:debug(message)
end
--- Shortcut to Game.logger:info(message)
-- @param message Message to be logged as info.
function Game.info(message)
Game.logger:info(message)
Game.fileLogger:info(message)
end
--- Shortcut to Game.logger:warn(message)
-- @param message Message to be logged as warn.
function Game.warn(message)
Game.logger:warn(message)
Game.fileLogger:warn(message)
end
--- Shortcut to Game.logger:error(message)
-- @param message Message to be logged as error.
function Game.error(message)
Game.logger:error(message)
Game.fileLogger:error(message)
end
--- Shortcut to Game.logger:fatal(message)
-- @param message Message to be logged as fatal.
function Game.fatal(message)
Game.logger:fatal(message)
game.fileLogger:fatal(message)
end
--- Logs Player input.
-- @param player The Player giving input.
-- @param input The input the Player gave.
function Game.logCommand(player, input)
Game.commandLogger:info(tostring(player) .. ": '" .. input .. "'")
end
--- Set the Game's state.
-- @param state The state to be assigned.<br/>Must be a valid member of GameState.
function Game.setState(state)
Game.state = state
end
--- Returns a timestamp used for most Game operations.
-- @return A timestamp reflecting the current time.
function Game.time()
return socket.gettime()
end
--- Check if the Game is ready to be played.
-- return true if the game's state is at GameState.READY.<br/>false otherwise.
function Game.isReady()
return Game.state == GameState.READY
end
--- Retreive the Game's name.
-- @return The name of the Game.
function Game.getName()
return Game.name
end
--- Retreive the Game's version.
-- @return The version of the Game.
function Game.getVersion()
return Game.version
end
--- Retreive the Game's list of developers.
-- @return A tuple of each developer
function Game.getDevelopers()
return Game.developers
end
--- Retreive the Game's state.
-- @return The Game's state.<br/>Must be a valid member of GameState.
function Game.getState()
return Game.state
end
--- Get a unique player ID.
-- @return A unique player ID.
function Game.getNextPlayerID()
local id = Game.playerID
Game.playerID = Game.playerID+1
return id
end
--- Retreive the Game's players list.
-- @return Player list.
function Game.getPlayers()
return Game.players
end
--- This Event acts as the middle ground for client connections, accepting clients on behalf of the server, and informing the game about it.<br/>
-- Game.PollEvent and Game.AcceptEvent are mandatory events that must be part of the Game scheduler.
-- @class table
-- @name Game.AcceptEvent
Game.AcceptEvent = Event:clone()
Game.AcceptEvent.shouldRepeat = true
Game.AcceptEvent.repeatMax = 0
Game.AcceptEvent.repeatInterval = 0.1
function Game.AcceptEvent:run()
if not Game.isReady() or not Game.server:isHosted() then
return
end
local client, err = Game.server:accept()
if not client then
return
end
local player = Player:new(client)
Game.connectPlayer(player)
end
--- This Event acts as the middle ground for client input, polling clients from the server for input and informing the game about it.<br/>
-- Also the starting point for destroying clients that have disconnected and cannot be reached.<br/>
-- Game.PollEvent and Game.AcceptEvent are mandatory events that must be part of the Game scheduler.
-- @class table
-- @name Game.PollEvent
Game.PollEvent = Event:clone()
Game.PollEvent.shouldRepeat = true
Game.PollEvent.repeatMax = 0
Game.PollEvent.repeatInterval = 0.1
function Game.PollEvent:run()
if not Game.isReady() or not Game.server:isHosted() then
return
end
if #Game.players < 1 then
return
end
local i = 1
while i <= #Game.players do
local player = Game.players[i]
local client = player:getClient()
local input, err = client:receive("*a")
if err == 'closed' then
Game.disconnectPlayer(player)
i = i - 1
elseif input and string.len(input) > 0 then
local multiple = string.gmatch(input, "(.-)\n")
local first = multiple()
if first then
Game.onPlayerInput(player, first)
for cmd in multiple do
Game.onPlayerInput(player, cmd)
end
else
Game.debug(string.format("bad input from %s: {%s}", tostring(player:getClient()), input))
end
end
i = i + 1
end
end
return Game