aboutsummaryrefslogtreecommitdiff
path: root/gamemode/core
diff options
context:
space:
mode:
authorAlexander Pickering <alexandermpickering@gmail.com>2017-02-18 21:55:55 -0500
committerAlexander Pickering <alexandermpickering@gmail.com>2017-02-18 21:55:55 -0500
commita22cbeddc5f8fb61e87a30aa14ba354de5cf4431 (patch)
tree297c1dbfb23185c5246e1dd7bdec52253a24ba60 /gamemode/core
parentf4ee62bb0725a3ae94477b2818071f506e4dfd9f (diff)
downloadartery-a22cbeddc5f8fb61e87a30aa14ba354de5cf4431.tar.gz
artery-a22cbeddc5f8fb61e87a30aa14ba354de5cf4431.tar.bz2
artery-a22cbeddc5f8fb61e87a30aa14ba354de5cf4431.zip
Updates
Diffstat (limited to 'gamemode/core')
-rw-r--r--gamemode/core/database/sv_mysqlite.lua387
-rw-r--r--gamemode/core/database/sv_setup.lua160
-rw-r--r--gamemode/core/inventory/inventory.lua30
-rw-r--r--gamemode/core/inventory/item.lua5
-rw-r--r--gamemode/core/inventory/sv_invtracker.lua84
-rw-r--r--gamemode/core/npc/sv_npcsystem.lua93
-rw-r--r--gamemode/core/npc/sv_shop.lua17
-rw-r--r--gamemode/core/pac/cl_pac.lua90
-rw-r--r--gamemode/core/pac/sv_pac.lua167
9 files changed, 994 insertions, 39 deletions
diff --git a/gamemode/core/database/sv_mysqlite.lua b/gamemode/core/database/sv_mysqlite.lua
new file mode 100644
index 0000000..82a2830
--- /dev/null
+++ b/gamemode/core/database/sv_mysqlite.lua
@@ -0,0 +1,387 @@
+--[[
+ MySQLite - Abstraction mechanism for SQLite and MySQL
+
+ Why use this?
+ - Easy to use interface for MySQL
+ - No need to modify code when switching between SQLite and MySQL
+ - Queued queries: execute a bunch of queries in order an run the callback when all queries are done
+
+ License: LGPL V2.1 (read here: https://www.gnu.org/licenses/lgpl-2.1.html)
+
+ Supported MySQL modules:
+ - MySQLOO
+ - tmysql4
+
+ Note: When both MySQLOO and tmysql4 modules are installed, MySQLOO is used by default.
+
+ /*---------------------------------------------------------------------------
+ Documentation
+ ---------------------------------------------------------------------------*/
+
+ MySQLite.initialize([config :: table]) :: No value
+ Initialize MySQLite. Loads the config from either the config parameter OR the MySQLite_config global.
+ This loads the module (if necessary) and connects to the MySQL database (if set up).
+ The config must have this layout:
+ {
+ EnableMySQL :: Bool - set to true to use MySQL, false for SQLite
+ Host :: String - database hostname
+ Username :: String - database username
+ Password :: String - database password (keep away from clients!)
+ Database_name :: String - name of the database
+ Database_port :: Number - connection port (3306 by default)
+ Preferred_module :: String - Preferred module, case sensitive, must be either "mysqloo" or "tmysql4"
+ MultiStatements :: Bool - Only available in tmysql4: allow multiple SQL statements per query
+ }
+
+ ----------------------------- Utility functions -----------------------------
+ MySQLite.isMySQL() :: Bool
+ Returns whether MySQLite is set up to use MySQL. True for MySQL, false for SQLite.
+ Use this when the query syntax between SQLite and MySQL differs (example: AUTOINCREMENT vs AUTO_INCREMENT)
+
+ MySQLite.SQLStr(str :: String) :: String
+ Escapes the string and puts it in quotes.
+ It uses the escaping method of the module that is currently being used.
+
+ MySQLite.tableExists(tbl :: String, callback :: function, errorCallback :: function)
+ Checks whether table tbl exists.
+
+ callback format: function(res :: Bool)
+ res is a boolean indicating whether the table exists.
+
+ The errorCallback format is the same as in MySQLite.query.
+
+ ----------------------------- Running queries -----------------------------
+ MySQLite.query(sqlText :: String, callback :: function, errorCallback :: function) :: No value
+ Runs a query. Calls the callback parameter when finished, calls errorCallback when an error occurs.
+
+ callback format:
+ function(result :: table, lastInsert :: number)
+ Result is the table with results (nil when there are no results or when the result list is empty)
+ lastInsert is the row number of the last inserted value (use with AUTOINCREMENT)
+
+ Note: lastInsert is NOT supported when using SQLite.
+
+ errorCallback format:
+ function(error :: String, query :: String) :: Bool
+ error is the error given by the database module.
+ query is the query that triggered the error.
+
+ Return true to suppress the error!
+
+ MySQLite.queryValue(sqlText :: String, callback :: function, errorCallback :: function) :: No value
+ Runs a query and returns the first value it comes across.
+
+ callback format:
+ function(result :: any)
+ where the result is either a string or a number, depending on the requested database field.
+
+ The errorCallback format is the same as in MySQLite.query.
+
+ ----------------------------- Transactions -----------------------------
+ MySQLite.begin() :: No value
+ Starts a transaction. Use in combination with MySQLite.queueQuery and MySQLite.commit.
+
+ MySQLite.queueQuery(sqlText :: String, callback :: function, errorCallback :: function) :: No value
+ Queues a query in the transaction. Note: a transaction must be started with MySQLite.begin() for this to work.
+ The callback will be called when this specific query has been executed successfully.
+ The errorCallback function will be called when an error occurs in this specific query.
+
+ See MySQLite.query for the callback and errorCallback format.
+
+ MySQLite.commit(onFinished)
+ Commits a transaction and calls onFinished when EVERY queued query has finished.
+ onFinished is NOT called when an error occurs in one of the queued queries.
+
+ onFinished is called without arguments.
+
+ ----------------------------- Hooks -----------------------------
+ DatabaseInitialized
+ Called when a successful connection to the database has been made.
+]]
+
+local bit = bit
+local debug = debug
+local error = error
+local ErrorNoHalt = ErrorNoHalt
+local hook = hook
+local include = include
+local pairs = pairs
+local require = require
+local sql = sql
+local string = string
+local table = table
+local timer = timer
+local tostring = tostring
+local GAMEMODE = GM or GAMEMODE
+local mysqlOO
+local TMySQL
+local _G = _G
+
+local multistatements
+
+local MySQLite_config = MySQLite_config or RP_MySQLConfig or FPP_MySQLConfig
+local moduleLoaded
+
+local function loadMySQLModule()
+ if moduleLoaded or not MySQLite_config or not MySQLite_config.EnableMySQL then return end
+
+ local moo, tmsql = file.Exists("bin/gmsv_mysqloo_*.dll", "LUA"), file.Exists("bin/gmsv_tmysql4_*.dll", "LUA")
+
+ if not moo and not tmsql then
+ error("Could not find a suitable MySQL module. Supported modules are MySQLOO and tmysql4.")
+ end
+ moduleLoaded = true
+
+ require(moo and tmsql and MySQLite_config.Preferred_module or
+ moo and "mysqloo" or
+ "tmysql4")
+
+ multistatements = CLIENT_MULTI_STATEMENTS
+
+ mysqlOO = mysqloo
+ TMySQL = tmysql
+end
+loadMySQLModule()
+
+module("MySQLite")
+
+
+function initialize(config)
+ MySQLite_config = config or MySQLite_config
+
+ if not MySQLite_config then
+ ErrorNoHalt("Warning: No MySQL config!")
+ end
+
+ loadMySQLModule()
+
+ if MySQLite_config.EnableMySQL then
+ connectToMySQL(MySQLite_config.Host, MySQLite_config.Username, MySQLite_config.Password, MySQLite_config.Database_name, MySQLite_config.Database_port)
+ else
+ timer.Simple(0, function()
+ GAMEMODE.DatabaseInitialized = GAMEMODE.DatabaseInitialized or function() end
+ hook.Call("DatabaseInitialized", GAMEMODE)
+ end)
+ end
+end
+
+local CONNECTED_TO_MYSQL = false
+local msOOConnect
+databaseObject = nil
+
+local queuedQueries
+local cachedQueries
+
+function isMySQL()
+ return CONNECTED_TO_MYSQL
+end
+
+function begin()
+ if not CONNECTED_TO_MYSQL then
+ sql.Begin()
+ else
+ if queuedQueries then
+ debug.Trace()
+ error("Transaction ongoing!")
+ end
+ queuedQueries = {}
+ end
+end
+
+function commit(onFinished)
+ if not CONNECTED_TO_MYSQL then
+ sql.Commit()
+ if onFinished then onFinished() end
+ return
+ end
+
+ if not queuedQueries then
+ error("No queued queries! Call begin() first!")
+ end
+
+ if #queuedQueries == 0 then
+ queuedQueries = nil
+ if onFinished then onFinished() end
+ return
+ end
+
+ -- Copy the table so other scripts can create their own queue
+ local queue = table.Copy(queuedQueries)
+ queuedQueries = nil
+
+ -- Handle queued queries in order
+ local queuePos = 0
+ local call
+
+ -- Recursion invariant: queuePos > 0 and queue[queuePos] <= #queue
+ call = function(...)
+ queuePos = queuePos + 1
+
+ if queue[queuePos].callback then
+ queue[queuePos].callback(...)
+ end
+
+ -- Base case, end of the queue
+ if queuePos + 1 > #queue then
+ if onFinished then onFinished() end -- All queries have finished
+ return
+ end
+
+ -- Recursion
+ local nextQuery = queue[queuePos + 1]
+ query(nextQuery.query, call, nextQuery.onError)
+ end
+
+ query(queue[1].query, call, queue[1].onError)
+end
+
+function queueQuery(sqlText, callback, errorCallback)
+ if CONNECTED_TO_MYSQL then
+ table.insert(queuedQueries, {query = sqlText, callback = callback, onError = errorCallback})
+ return
+ end
+ -- SQLite is instantaneous, simply running the query is equal to queueing it
+ query(sqlText, callback, errorCallback)
+end
+
+local function msOOQuery(sqlText, callback, errorCallback, queryValue)
+ local query = databaseObject:query(sqlText)
+ local data
+ query.onData = function(Q, D)
+ data = data or {}
+ data[#data + 1] = D
+ end
+
+ query.onError = function(Q, E)
+ if databaseObject:status() == mysqlOO.DATABASE_NOT_CONNECTED then
+ table.insert(cachedQueries, {sqlText, callback, queryValue})
+
+ -- Immediately try reconnecting
+ msOOConnect(MySQLite_config.Host, MySQLite_config.Username, MySQLite_config.Password, MySQLite_config.Database_name, MySQLite_config.Database_port)
+ return
+ end
+
+ local supp = errorCallback and errorCallback(E, sqlText)
+ if not supp then error(E .. " (" .. sqlText .. ")") end
+ end
+
+ query.onSuccess = function()
+ local res = queryValue and data and data[1] and table.GetFirstValue(data[1]) or not queryValue and data or nil
+ if callback then callback(res, query:lastInsert()) end
+ end
+ query:start()
+end
+
+local function tmsqlQuery(sqlText, callback, errorCallback, queryValue)
+ local call = function(res)
+ res = res[1] -- For now only support one result set
+ if not res.status then
+ local supp = errorCallback and errorCallback(res.error, sqlText)
+ if not supp then error(res.error .. " (" .. sqlText .. ")") end
+ return
+ end
+
+ if not res.data or #res.data == 0 then res.data = nil end -- compatibility with other backends
+ if queryValue and callback then return callback(res.data and res.data[1] and table.GetFirstValue(res.data[1]) or nil) end
+ if callback then callback(res.data, res.lastid) end
+ end
+
+ databaseObject:Query(sqlText, call)
+end
+
+local function SQLiteQuery(sqlText, callback, errorCallback, queryValue)
+ sql.m_strError = "" -- reset last error
+
+ local lastError = sql.LastError()
+ local Result = queryValue and sql.QueryValue(sqlText) or sql.Query(sqlText)
+
+ if sql.LastError() and sql.LastError() ~= lastError then
+ local err = sql.LastError()
+ local supp = errorCallback and errorCallback(err, sqlText)
+ if supp == false then error(err .. " (" .. sqlText .. ")", 2) end
+ return
+ end
+
+ if callback then callback(Result) end
+ return Result
+end
+
+function query(sqlText, callback, errorCallback)
+ local qFunc = (CONNECTED_TO_MYSQL and ((mysqlOO and msOOQuery) or (TMySQL and tmsqlQuery))) or SQLiteQuery
+ return qFunc(sqlText, callback, errorCallback, false)
+end
+
+function queryValue(sqlText, callback, errorCallback)
+ local qFunc = (CONNECTED_TO_MYSQL and ((mysqlOO and msOOQuery) or (TMySQL and tmsqlQuery))) or SQLiteQuery
+ return qFunc(sqlText, callback, errorCallback, true)
+end
+
+local function onConnected()
+ CONNECTED_TO_MYSQL = true
+
+ -- Run the queries that were called before the connection was made
+ for k, v in pairs(cachedQueries or {}) do
+ cachedQueries[k] = nil
+ if v[3] then
+ queryValue(v[1], v[2])
+ else
+ query(v[1], v[2])
+ end
+ end
+ cachedQueries = {}
+
+ hook.Call("DatabaseInitialized", GAMEMODE.DatabaseInitialized and GAMEMODE or nil)
+end
+
+msOOConnect = function(host, username, password, database_name, database_port)
+ databaseObject = mysqlOO.connect(host, username, password, database_name, database_port)
+
+ if timer.Exists("darkrp_check_mysql_status") then timer.Remove("darkrp_check_mysql_status") end
+
+ databaseObject.onConnectionFailed = function(_, msg)
+ timer.Simple(5, function()
+ msOOConnect(MySQLite_config.Host, MySQLite_config.Username, MySQLite_config.Password, MySQLite_config.Database_name, MySQLite_config.Database_port)
+ end)
+ error("Connection failed! " .. tostring(msg) .. "\nTrying again in 5 seconds.")
+ end
+
+ databaseObject.onConnected = onConnected
+
+ databaseObject:connect()
+end
+
+local function tmsqlConnect(host, username, password, database_name, database_port)
+ local db, err = TMySQL.initialize(host, username, password, database_name, database_port, nil, MySQLite_config.MultiStatements and multistatements or nil)
+ if err then error("Connection failed! " .. err .. "\n") end
+
+ databaseObject = db
+ onConnected()
+end
+
+function connectToMySQL(host, username, password, database_name, database_port)
+ database_port = database_port or 3306
+ local func = mysqlOO and msOOConnect or TMySQL and tmsqlConnect or function() end
+ func(host, username, password, database_name, database_port)
+end
+
+function SQLStr(str)
+ local escape =
+ not CONNECTED_TO_MYSQL and sql.SQLStr or
+ mysqlOO and function(str) return "\"" .. databaseObject:escape(tostring(str)) .. "\"" end or
+ TMySQL and function(str) return "\"" .. databaseObject:Escape(tostring(str)) .. "\"" end
+
+ return escape(str)
+end
+
+function tableExists(tbl, callback, errorCallback)
+ if not CONNECTED_TO_MYSQL then
+ local exists = sql.TableExists(tbl)
+ callback(exists)
+
+ return exists
+ end
+
+ queryValue(string.format("SHOW TABLES LIKE %s", SQLStr(tbl)), function(v)
+ callback(v ~= nil)
+ end, errorCallback)
+end
diff --git a/gamemode/core/database/sv_setup.lua b/gamemode/core/database/sv_setup.lua
new file mode 100644
index 0000000..0d0eb74
--- /dev/null
+++ b/gamemode/core/database/sv_setup.lua
@@ -0,0 +1,160 @@
+--Adds the MySQLite global
+nrequire("sv_mysqlite.lua")
+local config = nrequire("config/sv_sql.lua")
+local data = nrequire("config/sv_newplayer.lua")
+local fn = nrequire("fn.lua")
+local col = nrequire("colortheme.lua")
+local inv = nrequire("inventory/inventory.lua")
+local track = nrequire("sv_invtracker.lua")
+local sql = {}
+
+--Setup the database if it's not already
+local setup_db = [[
+CREATE TABLE IF NOT EXISTS playerdata(SteamID bigint primary key, PlayerData json, MetaData json)]]
+
+--Create a new player
+local create_player_query = [[
+INSERT INTO playerdata (`SteamID`,`PlayerData`,`MetaData`) VALUES(%.0f,%q,%q)]]
+
+--Get a player's data from the database
+local fetch_player_query = [[
+SELECT PlayerData, MetaData FROM playerdata WHERE SteamID=%.0f
+]]
+
+local save_player_query = [[
+UPDATE playerdata SET MetaData=%q PlayerData=%q WHERE SteamID=%.0f
+]]
+
+local s_fmt = function(fmt,...)
+ local args = {...}
+ fn.map(args,MySQLite.SQLStr)
+ return string.format(fmt,unpack(args))
+end
+
+local function q_fai(err,query)
+ MsgC(col.console.red,string.format("Error executing %q, error:%s",query,err))
+end
+
+local function serialize_player(ply)
+ local sdata = {}
+ local invs = {}
+ for k,v in pairs(ply.data.inventories) do
+ invs[k] = {v.Name,v:Serialize()}
+ end
+ sdata.inventories = invs
+ sdata.skills = ply.data.skills
+ sdata.quests = ply.data.quests
+ sdata.prayers = ply.data.prayers
+ return util.TableToJSON(sdata)
+end
+
+local function deserialize_player(ply,str)
+ print("Deseriailizeing player",ply," with ", str)
+ track.ClearInventories(ply)
+ local tbl = util.JSONToTable(str)
+ local invs = tbl.inventories
+ print("Inventories was", invs)
+ PrintTable(invs)
+ for k,v in pairs(invs) do
+ print("Giveing inventory",v[1],v[2])
+ track.GiveInventoryWithData(ply,v[1],v[2])
+ end
+ ply.data.skills = tbl.skills or {}
+ ply.data.quests = tbl.quests or {}
+ ply.data.prayers = tbl.prayers or {}
+ track.SendPlayerData(ply)
+end
+
+local function connect()
+ print("Connecting to the database...")
+ MySQLite.initialize(config)
+end
+hook.Add("DatabaseInitialized","setup_table",function()
+ local setup_success = function(res,li)
+ print("Set up connection to db")
+ end
+ print("Setup query:",setup_db)
+ MySQLite.query(setup_db,setup_success,q_fai)
+end)
+connect()
+
+--Retruns (PlayerData, MetaData) or nil
+function sql.GetPlayerData(ply)
+ local s64 = ply:SteamID64()
+ local q_str = s_fmt(fetch_player_query,s64)
+ local q_suc = function(res,li)
+ print("Got player's data:",res,type(res))
+ if res == nil then
+ print("Was nil, createing player data")
+ sql.CreatePlayerTable(ply)
+ else
+ PrintTable(res)
+ assert(#res == 1,"Not unique!")
+ print("Was unique!")
+ local meta = res[1].MetaData
+ local plyd = res[1].PlayerData
+ local mtbl = util.JSONToTable(meta)
+ print("About to check if we are on the right server")
+ if mtbl.lastserver ~= game.GetIPAddress() then
+ print("Connecting player to ", mtbl.lastserver, " was on ", game.GetIPAddress())
+ ply:ConCommand("connect " .. mtbl.lastserver)
+ return
+ end
+ print("We were on the right server")
+ local _,_,x,y,z = mtbl.lastlocation:find("([%d%.]+) ([%d%.]+) ([%d%.]+)")
+ local vec = {x,y,z}
+ for k,v in pairs(vec) do vec[k] = tonumber(v) end
+ print("setting player pos to")
+ PrintTable(vec)
+ ply:SetPos(Vector(unpack(vec)))
+ deserialize_player(ply,plyd)
+ end
+ end
+ print("doing query",q_str)
+ MySQLite.query(q_str,q_suc,q_fai)
+end
+
+function sql.CreatePlayerTable(ply)
+ print("Createing player table....")
+ local s64 = ply:SteamID64()
+ print("steamid was", s64)
+ local plytbl = data.newdata()
+ local plymet = data.newmeta()
+ local plydata = util.TableToJSON(plytbl)
+ local metdata = util.TableToJSON(plymet)
+ local q_str = s_fmt(create_player_query,s64,plydata,metdata)
+ local q_suc = function(res,li)
+ print("Inserted new player",ply)
+ sql.GetPlayerData(ply)
+ end
+ print("doing query", q_str)
+ MySQLite.query(q_str,q_suc,q_fai)
+end
+
+function sql.SendPlayerToInstance(ply,ls,ll)
+ local s64 = ply:SteamID64()
+ local plydata = serialize_player(ply)
+ local plymeta = util.TableToJSON({
+ lastserver = ls,
+ lastlocation = ll
+ })
+ local q_str = s_fmt(save_player_query,plymeta,plydata,s64)
+ local q_suc = function(res,li)
+ print("Successfully saved player data")
+ end
+ MySQLite.query(q_str,q_suc,q_fai)
+end
+
+concommand.Add("DoQuery",function(ply,cmd,args)
+ if args[1] == "create" then
+ sql.CreatePlayerTable(ply)
+ elseif args[1] == "get" then
+ sql.GetPlayerData(ply)
+ elseif args[1] == "send" then
+ sql.SendPlayerToInstance(ply,args[2],args[3])
+ else
+ error("Command not understood:" .. args[1] .. "!")
+ end
+end)
+
+return sql
diff --git a/gamemode/core/inventory/inventory.lua b/gamemode/core/inventory/inventory.lua
index 47a7eb3..b4c025c 100644
--- a/gamemode/core/inventory/inventory.lua
+++ b/gamemode/core/inventory/inventory.lua
@@ -5,20 +5,21 @@
CreateInventoryFromData(string_name,string_data)::table_inventory)
DeriveInventory(string_name) ::table_inventory
Inventories have the following structure
- inv.Name ::string
- inv:FindPlaceFor(item) ::table_position or nil
- inv:CanFitIn(table_position,item) ::true or string_explanation
- inv:Put(table_position,item) ::nil
- inv:Has(string_or_compare_func) ::table_position or nil
- inv:Remove(position) ::table_item
- inv:Get(position) ::table_item
- inv:Serialize() ::string
- inv:DeSerialize(str) ::table_inventory
+ field returns description
+ inv.Name ::string The name!
+ inv:FindPlaceFor(item) ::table_position or nil Finds a place for the item
+ inv:CanFitIn(table_position,item) ::boolean Check if the item can fit in the position
+ inv:Put(table_position,item) ::nil Put an item in at the position
+ inv:Has(string_or_compare_func) ::table_position or nil find an item in the inventory
+ inv:Remove(position) ::table_item Remove an item from the position
+ inv:Get(position) ::table_item Get the item at a position
+ inv:Serialize() ::string Serialize the item to store it in a db
+ inv:DeSerialize(str) ::table_inventory recreate the item from data in serialize
The above fields must be defined for new inventories.
-----------------------------------------------------
The below are automatically made if they do not exist.
- inv:AddObserver(tbl_other) ::number_id
- inv:RemoveObserver(number_id) ::nil
+ inv:AddObserver(tbl_other) ::number_id Whenever put or remove is called on this inventory, tbl_other's put() and remove() is also called, for easy networking to whoever needs it
+ inv:RemoveObserver(number_id) ::nil Removes an observer from the inventory
------------------------------------------------------
These fields should be defined when an inventory is created, before it can be used
inv.Owner ::entity
@@ -31,9 +32,6 @@
Serialize() should take this inventories contents and return a string that it can recreate this inventory from. DeSerialize should create a self.Copy() with the appropriate fields set. Take advantage of the fact that items must also have Serialize() and DeSerialize() methods.
]]
-local thm = nrequire("colortheme.lua")
-local log = nrequire("log.lua")
-
local inv = {}
--Creates a partial copy of a table(tables are copied, functions are not)
@@ -64,7 +62,7 @@ local function DefaultAddObserver(self,tbl)
end
local function DefaultRemoveObserver(self,observer_id)
for i = observer_id, #self.observers do
- self.observers[i] = self.observers + 1
+ self.observers[i] = self.observers[i + 1]
end
end
local function SetDefaultObservers(tbl)
@@ -117,7 +115,7 @@ function inv.RegisterInventory(tbl)
SetDefaultObservers(tbl)
end
inventories[tbl.Name] = tbl
- log.debug("Registered inventory: " .. tbl.Name .. "\n")
+ print("Registered inventory: " .. tbl.Name)
end
--Create an inventory
diff --git a/gamemode/core/inventory/item.lua b/gamemode/core/inventory/item.lua
index 354472a..dd788d6 100644
--- a/gamemode/core/inventory/item.lua
+++ b/gamemode/core/inventory/item.lua
@@ -17,9 +17,6 @@
The above must be defined for every item
Items may also have methods from one or more interfaces registered with RegisterInterface
]]
-local log = nrequire("log.lua")
-print("in item.lua, log is:")
-PrintTable(log)
local itm = {}
local required_fields = {
@@ -33,7 +30,7 @@ function itm.RegisterItem(tbl)
end
assert(items[tbl.Name] == nil, string.format("Attempted to register 2 items with the same name %q",tbl.Name))
items[tbl.Name] = tbl
- log.debug("Registered item: " .. tbl.Name .. "\n")
+ print("Registered item: " .. tbl.Name)
end
function itm.GetItemByName(name)
diff --git a/gamemode/core/inventory/sv_invtracker.lua b/gamemode/core/inventory/sv_invtracker.lua
index 98e0268..5331261 100644
--- a/gamemode/core/inventory/sv_invtracker.lua
+++ b/gamemode/core/inventory/sv_invtracker.lua
@@ -4,6 +4,7 @@
local inv = nrequire("inventory/inventory.lua")
local itm = nrequire("item.lua")
+local track = {}
for k,v in pairs({
"art_ObserveInventory",
@@ -41,8 +42,15 @@ net.Receive("art_RequestInvMove",function(len,ply)
print("froment",froment)
print("froment.data:",froment.data)
print("froment.data.inventories",froment.data.inventories)
+ PrintTable(froment.data.inventories)
print("invid:",froment.data.inventories[frominvid])
assert(froment.data ~= nil and froment.data.inventories ~= nil and froment.data.inventories[frominvid] ~= nil, "From entity did not have that inventory!")
+ print("toent",toent)
+ print("toent.data",toent.data)
+ print("toent.data.inventories",toent.data.inventories)
+ PrintTable(toent.data.inventories)
+ print("toinvid",toinvid)
+ print("toent.data.inventories[invid]",toent.data.inventories[toinvid])
assert(toent.data ~= nil and toent.data.inventories ~= nil and toent.data.inventories[toinvid] ~= nil, "To entity did not have that inventory!")
local frominv = froment.data.inventories[frominvid]
local toinv = toent.data.inventories[toinvid]
@@ -54,13 +62,13 @@ net.Receive("art_RequestInvMove",function(len,ply)
toinv:Put(topos,item)
end)
-local function ClearInventories(ply)
+function track.ClearInventories(ply)
ply.data = {}
ply.data.inventories = {}
end
--Updates the client side inventory whenever the inventory is updated server side
-local function MakeInventoryObserver(ply,invid)
+function track.MakeInventoryObserver(ply,invid)
local observer = {}
observer.Put = function(self,pos,item)
print("In observer, item was", item)
@@ -86,13 +94,25 @@ local function MakeInventoryObserver(ply,invid)
return observer
end
-local function GiveInventoryTo(ply,name)
+function track.NotifyPlayerOfInventory(ply,inv)
+ local initaldat = inv:Serialize()
+ net.Start("art_ObserveInventory")
+ net.WriteUInt(inv.id,32)
+ net.WriteString(inv.Name)
+ net.WriteUInt(#initaldat,32)
+ net.WriteData(initaldat,#initaldat)
+ print("Before sending, inv owner is", inv.Owner)
+ net.WriteEntity(inv.Owner)
+ net.Send(ply)
+end
+
+function track.GiveInventoryTo(ply,name)
local i = inv.CreateInventory(name)
- i.owner = ply
+ i.Owner = ply
local nid = #ply.data.inventories + 1
i.id = nid
local dat = i:Serialize()
- local observer = MakeInventoryObserver(ply,nid)
+ local observer = track.MakeInventoryObserver(ply,nid)
i:AddObserver(observer)
ply.data.inventories[nid] = i
@@ -101,13 +121,59 @@ local function GiveInventoryTo(ply,name)
net.WriteString(name)
net.WriteUInt(#dat,32)
net.WriteData(dat,#dat)
+ net.WriteEntity(i.Owner)
+ net.Send(ply)
+end
+
+function track.GiveInventoryWithData(ply,name,data)
+ print("Giveing inventory with data")
+ local i = inv.CreateInventoryFromData(name,data)
+ local nid = #ply.data.inventories + 1
+ local observer = track.MakeInventoryObserver(ply,nid)
+ i.Owner = ply
+ i.id = nid
+ i:AddObserver(observer)
+ ply.data.inventories[nid] = i
+
+ net.Start("art_ObserveInventory")
+ net.WriteUInt(nid,32)
+ net.WriteString(name)
+ net.WriteUInt(#data,32)
+ net.WriteData(data,#data)
net.WriteEntity(ply)
net.Send(ply)
end
+
+
+--A shortcut for finding if a player has an item
+local plymeta = FindMetaTable("Player")
+
+function plymeta:HasItem(str)
+ for k,v in pairs(self.inventories) do
+ local p = v:Has(str)
+ if type(p) == "table" then
+ return {k,p}
+ end
+ end
+ return false
+end
+
+function plymeta:RemoveItem(tbl)
+ local nid = tbl[1]
+ local pos = tbl[2]
+ self.inventories[nid]:Remove(pos)
+end
+
+function track.SendPlayerData(ply)
+ net.Start("art_load_player_data")
+ net.WriteTable({})
+ net.Send(ply)
+end
+
concommand.Add("SendMeData",function(ply,cmd,args)
- ClearInventories(ply)
- GiveInventoryTo(ply,"Equipment")
+ track.ClearInventories(ply)
+ track.GiveInventoryTo(ply,"Equipment")
net.Start("art_load_player_data")
net.WriteTable({})
net.Send(ply)
@@ -118,7 +184,7 @@ concommand.Add("ShowMyInventories",function(ply,cmd,args)
end)
concommand.Add("AddInventory",function(ply,cmd,args)
- GiveInventoryTo(ply,args[1])
+ track.GiveInventoryTo(ply,args[1])
end)
concommand.Add("GiveItem",function(ply,cmd,args)
@@ -139,3 +205,5 @@ concommand.Add("GiveItem",function(ply,cmd,args)
print("I couldn't find a place to put it!")
end
end)
+
+return track
diff --git a/gamemode/core/npc/sv_npcsystem.lua b/gamemode/core/npc/sv_npcsystem.lua
index b41f4e6..1abbc67 100644
--- a/gamemode/core/npc/sv_npcsystem.lua
+++ b/gamemode/core/npc/sv_npcsystem.lua
@@ -1,4 +1,3 @@
-
local f = nrequire("concommands.lua")
local n = {}
local npcs = {} --Master table of npcs
@@ -12,28 +11,100 @@ function n.RegisterNPC(npc)
end
function n.CreateNPCByName(npcname, pos)
- print("Createing a " ,npcname ," at ", pos)
+ print("Createing a ", npcname, " at ", pos)
local npctbl = npcs[npcname]
local npc = ents.Create("npc_huntable")
npc:SetPos(pos)
- for k,v in pairs(npctbl) do
+
+ for k, v in pairs(npctbl) do
npc[k] = v
end
+
npc:Spawn()
+
return npc
end
+--Creates a shop npc with this tbl
+function n.CreateShop(npc)
+ print("Createing shop npc")
+ local npcent = ents.Create("npc_shop")
+ for k,v in pairs(npc) do
+ npcent[k] = v
+ end
+ npcent:Spawn()
+ print("Called spawn")
+end
-if SERVER then
- autocompletef = nil
-else
- autocompletef = f.AutocompleteFunction(npcs)
+--Creates a townie npc with this tbl
+function n.CreateTownie(tbl)
+ local npcent = ents.Create("npc_townie")
+ for k, v in pairs(tbl) do
+ npcent[k] = v
+ end
+ npcent:Spawn()
+end
+
+--Creates a new navigation node for npc's
+function n.CreateNavNode(tbl)
+ local nodeent = ents.Create("info_townienode")
+ assert(tbl ~= nil, "Tried to create a nil navnode")
+ for k, v in pairs(tbl) do
+ nodeent[k] = v
+ end
+ nodeent:Spawn()
+end
+
+--Ents to remove when refreshing the npc map
+local removeents = {"npc_townie", "info_townienode", "npc_shop"}
+
+-- "art_chest",
+for k, v in pairs(removeents) do
+ local eot = ents.FindByClass(v)
+ for i, j in pairs(eot) do
+ j:Remove()
+ end
+end
+
+local mapfields = {"navnodes", "npcs"}
+
+-- "chests",
+local function loadMap()
+ for k, v in ipairs(mapfields) do
+ local mapname = game.GetMap()
+ local fpath = string.format("artery/maps/%s/%s/*", mapname, v)
+ local files, dirs = file.Find(fpath, "DATA")
+
+ for i, j in pairs(files) do
+ if string.GetExtensionFromFilename(j) ~= "lua" then continue end
+ local itempath = string.format("artery/maps/%s/%s/%s", mapname, v, j)
+ local itemtxt = file.Read(itempath, "DATA")
+ assert(itemtxt ~= nil, "Found a file, but it looks like it can't be compiled:" .. itempath)
+ CompileString(itemtxt, itempath)()
+ end
+ end
end
-concommand.Add("artery_makenpc",function(ply,cmd,args)
+
+hook.Add("InitPostEntity", "artery_spawnmapnpcs", function()
+ loadMap()
+end)
+
+concommand.Add("artery_reloadmap", function()
+ for k, v in pairs(removeents) do
+ local eot = ents.FindByClass(v)
+
+ for i, j in pairs(eot) do
+ j:Remove()
+ end
+ end
+
+ loadMap()
+end)
+
+concommand.Add("artery_makenpc", function(ply, cmd, args)
if not ply:IsAdmin() then return end
local na = args[1]
- n.CreateNPCByName(na,ply:GetEyeTrace().HitPos)
-end,
-autocompletef)
+ n.CreateNPCByName(na, ply:GetEyeTrace().HitPos)
+end, autocompletef)
return n
diff --git a/gamemode/core/npc/sv_shop.lua b/gamemode/core/npc/sv_shop.lua
new file mode 100644
index 0000000..2e54f7e
--- /dev/null
+++ b/gamemode/core/npc/sv_shop.lua
@@ -0,0 +1,17 @@
+--[[
+ Create a shop npc
+]]
+
+local shop = {}
+
+function shop.CreateShop(npc)
+ print("Createing shop npc")
+ local npcent = ents.Create("npc_shop")
+ for k,v in pairs(npc) do
+ npcent[k] = v
+ end
+ npcent:Spawn()
+ print("Called spawn")
+end
+
+return shop
diff --git a/gamemode/core/pac/cl_pac.lua b/gamemode/core/pac/cl_pac.lua
new file mode 100644
index 0000000..9163d82
--- /dev/null
+++ b/gamemode/core/pac/cl_pac.lua
@@ -0,0 +1,90 @@
+--[[
+ A lazy loader for pac3 costumes, used for weapons/armor/ect.
+]]
+
+--As soon as the client connects, request the names of all the pac's on the server
+
+--If the player dosen't have PAC3 installed, then overwrite all pac-related network events to display an error.
+if pac == nil then
+ local function no_pac_panic()
+ error("This gamemode require PAC3 to display armor/cloths, please be sure clients are downloading this addon from somewhere (perferably the workshop!)")
+ end
+ local networkmsgs = {
+ "artery_downloadpac",
+ "artery_applypac",
+ "artery_giveworldpacs",
+ "artery_removepac"
+ }
+ for k,v in pairs(networkmsgs) do
+ net.Receive(v,no_pac_panic)
+ end
+ no_pac_panic()
+ return --Don't execute this file!
+end
+
+timer.Simple(0,function()
+ net.Start("artery_getworldpacs")
+ net.SendToServer()
+end)
+
+file.CreateDir("artery/pacs")
+
+local function loadpac(ent,name,hash)
+ print("Told to apply pac", name, "to ent", ent)
+ local filepath = string.format("artery/pacs/%s.txt",name)
+ local filetext = file.Read(filepath,"DATA")
+ if ent.AttachPACPart == nil then
+ pac.SetupENT(ent)
+ assert(ent.AttachPACPart ~= nil,"Failed to set up ent",ent)
+ end
+ if filetext and (tonumber(util.CRC(filetext)) == hash) then
+ local pactbl = CompileString(string.format("return {%s}",filetext),name)()
+ ent:AttachPACPart(pactbl)
+ else--Cache is old, download the new pac!
+ net.Start("artery_requestpac")
+ net.WriteString(name)
+ net.SendToServer()
+ --1 second is probably long enough to download a pac, right?
+ timer.Simple(1,function()
+ loadpac(ent,name,hash)
+ end)
+ end
+end
+
+local function unloadpac(ent,name,hash)
+ local filepath = string.format("artery/pacs/%s.txt",name)
+ local filetext = file.Read(filepath,"DATA")
+ local pactbl = CompileString(string.format("return {%s}",filetext),name)()
+ ent:RemovePACPart(pactbl)
+end
+
+net.Receive("artery_downloadpac",function()
+ local pac_name = net.ReadString()
+ local pac_txt = net.ReadString()
+ local pac_hash = net.ReadUInt(32)
+ local filepath = string.format("artery/pacs/%s.txt",pac_name)
+ file.Write(filepath,pac_txt)
+end)
+
+net.Receive("artery_applypac",function()
+ local pac_ent = net.ReadEntity()
+ local pac_name = net.ReadString()
+ local pac_hash = net.ReadUInt(32)
+ loadpac(pac_ent,pac_name,pac_hash)
+end)
+
+net.Receive("artery_giveworldpacs",function()
+ local pactbl = net.ReadTable()
+ for ent,pacnames in pairs(pactbl) do
+ for name, hash in pairs(pacnames) do
+ loadpac(ent,name,hash)
+ end
+ end
+end)
+
+net.Receive("artery_removepac",function()
+ local pac_ent = net.ReadEntity()
+ local pac_name = net.ReadString()
+ local pac_hash = net.ReadUInt(32)
+ unloadpac(pac_ent,pac_name,pac_hash)
+end)
diff --git a/gamemode/core/pac/sv_pac.lua b/gamemode/core/pac/sv_pac.lua
new file mode 100644
index 0000000..5a82607
--- /dev/null
+++ b/gamemode/core/pac/sv_pac.lua
@@ -0,0 +1,167 @@
+--[[
+ The server side lazy loader for pac3 costumes
+ PAC3 outfits are not downloaded until they are needed, we can keep the inital download to join the server pretty small this way.
+ The downside is that the game might lag a little when someone wears something that is rare/new and everyone has to download it.
+
+ Console Commands:
+ artery_reload_pacs
+ The server will cache PAC's so it dosen't need to read form disk every time. If you're live editing pac's on the server, and are wondering why your changes aren't showing, use this command. There is no command to clear client cache, because applying a pac also sends a hash of the pac to the client, and the client re-downloads if the hashes are different.
+
+
+ Functions:
+ entity:ApplyPac(String costume) :: nil
+ Find the file /data/pacs/<costume>.pac (on the server) and tell clients to apply it to ent. This will automatically download the pac to any clients nessessary.
+
+ entity:RemovePac(String costume) :: nil
+ Remove the pac from the entity.
+
+ entity:GetPacs() :: table
+ Retreives a list of the pacs an entity is wearing, as strings
+
+ Network Strings:
+ "artery_getworldpacs",
+ "artery_giveworldpacs",
+ "artery_applypac",
+ "artery_removepac",
+ "artery_requestpac",
+ "artery_downloadpac"
+]]
+
+local p3 = {}
+
+local nwstrings = {
+ "artery_getworldpacs",
+ "artery_giveworldpacs",
+ "artery_applypac",
+ "artery_removepac",
+ "artery_requestpac",
+ "artery_downloadpac"
+}
+for _,v in pairs(nwstrings) do
+ util.AddNetworkString(v)
+end
+
+--If the server has pac installed, restrict player's from putting on their own pacs
+hook.Add("PrePACConfigApply", "stoppacs", function(ply, outfit_data)
+ if not ply:IsAdmin() then
+ return false, "You don't have permission to do that!"
+ end
+end)
+
+--When the server starts, get all the pacs and calculate their hashes so we can index them quickly without haveing to read from disk each time.
+local pachashes = {}
+local function loadhashes()
+ local files,_ = file.Find("artery/pacs/*","DATA")
+ for _,v in ipairs(files) do
+ local filepath = string.format("artery/pacs/%s",v)
+ local filetext = file.Read(filepath,"DATA")
+ local filehash = util.CRC(filetext)
+ pachashes[string.StripExtension(v)] = tonumber(filehash)
+ end
+end
+loadhashes()
+
+local appliedpacs = {}
+
+function p3.ApplyPac(what, name)
+ appliedpacs[what] = appliedpacs[what] or {}
+ appliedpacs[what][name] = pachashes[name]
+ net.Start("artery_applypac")
+ net.WriteEntity(what)
+ net.WriteString(name)
+ net.WriteUInt(pachashes[name],32)
+ net.Broadcast()
+end
+
+function p3.RemovePac(what, name)
+ assert(appliedpacs[what][name],"Attempted to remove a pac that an entity is not wearing!")
+ appliedpacs[what][name] = nil
+ if #appliedpacs[what] == 0 then
+ appliedpacs[what] = nil
+ end
+ net.Start("artery_removepac")
+ net.WriteEntity(what)
+ net.WriteString(name)
+ net.WriteUInt(pachashes[name],32)
+ net.Broadcast()
+end
+
+function p3.GetPacs(what)
+ return appliedpacs[what] or {}
+end
+
+--If a player joins the server, tell them all about the pacs that are applied
+net.Receive("artery_getworldpacs",function(ln,ply)
+ net.Start("artery_giveworldpacs")
+ net.WriteTable(appliedpacs)
+ net.Send(ply)
+end)
+
+local max_pacs_in_cache = 10
+local pacs_in_cache = 0
+local pac_cache = {}
+
+--Load something from our cache
+local function cacheload(key)
+ --If it's already in the cache, just update the time it was last used and return the pac.
+ if pac_cache[key] ~= nil then
+ pac_cache[key].time = CurTime()
+ return pac_cache[key].pac
+ end
+
+ --Otherwise, we need to load it.
+ local pacpath = string.format("artery/pacs/%s.txt",key)
+ local pacfile = file.Read(pacpath,"LUA")
+
+ --If we haven't reached max cache yet, just put it in
+ if pacs_in_cache < max_pacs_in_cache then
+ pac_cache[key] = {
+ ["pac"] = pacfile,
+ ["time"] = CurTime()
+ }
+ pacs_in_cache = pacs_in_cache + 1
+ return pacfile
+ else
+ --We have max pac's, delete the oldest one, and put the new one in.
+ local oldest,oldstr = CurTime(),""
+ for k,v in pairs(pac_cache) do
+ if v.time < oldest then
+ oldest = v.time
+ oldstr = k
+ end
+ end
+ pac_cache[oldstr] = nil
+ pac_cache[key] = {
+ ["pac"] = pacfile,
+ ["time"] = CurTime()
+ }
+ return pacfile
+ end
+end
+
+net.Receive("artery_requestpac",function(ln,ply)
+ local pac_name = net.ReadString()
+
+ --Double check that we're not executing a directory traversal attack https://www.owasp.org/index.php/Path_Traversal
+ if string.find(pac_name,"..",1,true) then
+ Report(string.format("Directory traversal attack attempted by %s:%s using artery_requestpac string %q",ply:Nick(),ply:SteamID64(),pac_name))
+ end
+
+ local pac_txt = cacheload(pac_name)
+
+ net.Start("artery_downloadpac")
+ net.WriteString(pac_name)
+ net.WriteString(pac_txt)
+ net.WriteUInt(pachashes[pac_name],32)
+ net.Send(ply)
+
+end)
+
+--Does all the things needed to edit pac's live
+concommand.Add("artery_reload_pacs",function()
+ pac_cache = {}
+ pacs_in_cache = 0
+ loadhashes()
+end)
+
+return p3