├── .gitignore ├── LICENSE ├── README.md ├── Tutorial.md ├── data ├── banlist.json ├── cell │ └── .gitkeep ├── custom │ └── .gitkeep ├── map │ └── .gitkeep ├── player │ └── .gitkeep ├── recordstore │ └── .gitkeep ├── requiredDataFiles.json └── world │ └── .gitkeep ├── lib ├── .gitkeep └── lua │ ├── classy.lua │ ├── dkjson.lua │ ├── fileHelper.lua │ ├── jsonInterface.lua │ ├── patterns.lua │ ├── tableHelper.lua │ ├── time.lua │ └── utils.lua └── scripts ├── animHelper.lua ├── cell ├── base.lua ├── json.lua └── sql.lua ├── clientVariableScopes.lua ├── color.lua ├── commandHandler.lua ├── config.lua ├── contentFixer.lua ├── custom └── .gitkeep ├── customCommandHooks.lua ├── customEventHooks.lua ├── customScripts.lua ├── dataTableBuilder.lua ├── database.lua ├── defaultCommands.lua ├── enumerations.lua ├── eventHandler.lua ├── guiHelper.lua ├── inventoryHelper.lua ├── logicHandler.lua ├── menu ├── advancedExample.lua ├── defaultCrafting.lua └── help.lua ├── menuHelper.lua ├── packetBuilder.lua ├── packetReader.lua ├── player ├── base.lua ├── json.lua └── sql.lua ├── recordstore ├── base.lua ├── json.lua └── sql.lua ├── serverCore.lua ├── speechCollections.lua ├── speechHelper.lua ├── stateHelper.lua └── world ├── base.lua ├── json.lua └── sql.lua /.gitignore: -------------------------------------------------------------------------------- 1 | data/* 2 | lib/* 3 | scripts/custom/* 4 | !/lib/lua 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2022 TES3MP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoreScripts 2 | 3 | These are the scripts used to implement most of the essential server logic in TES3MP, including gameplay adjustments for multiplayer as well as state saving and loading. 4 | 5 | * TES3MP version: 0.8.1 6 | -------------------------------------------------------------------------------- /Tutorial.md: -------------------------------------------------------------------------------- 1 | Using the customEventHooks API: 2 | === 3 | 4 | Handling events: 5 | --- 6 | 7 | To handle various events you will need to use two functions: `customEventHooks.registerValidator` and `customEventHooks.registerHandler`. 8 | Validators are called before any default logic for the event is executed, Handlers are called after such (whether default behaviour was peformed or not). 9 | 10 | Both of these functions accept an event string (you can find a table below) and a callback function as their arguments. 11 | The callback function will be called with a guaranteed argument of eventStatus and a few arguments (potentially none) depending on the particular event. 12 | 13 | eventStatus is a table that defines the way handlers should behave. It has two fields: `validDefaultHandler` and `validCustomHandlers`. By default both of these are `true`. 14 | First defines if default behaviour should be performed, the second signals custom handlers that they should not run. 15 | However, their callbacks are still ran, and it is scripts' responsibility to handle `eventStatus.validCustomHandlers` being `false`. 16 | 17 | Validators can change the current eventStatus. If your validators returns nothing, it stays the same, however if you return a non-`nil` value for either of the two fields, it will override the previous one. You can use `customEventHooks.makeEventStatus(validDefaultHandler, validCustomHandlers)` for this. 18 | 19 | Examples: 20 | --- 21 | Imagine you want to taunt a player whenever they die. 22 | 23 | ```Lua 24 | customEventHooks.registerHandler("OnPlayerDeath", function(eventStatus, pid) 25 | if eventStatus.validCustomHandlers then --check if some other script made this event obsolete 26 | tes3mp.SendMessage(pid, "Don't worry, he'll be gentle!\n") 27 | end 28 | end) 29 | ``` 30 | 31 | Now let's do something more practical: limiting players' level: 32 | 33 | ```Lua 34 | local maxLevel = 20 35 | customEventHooks.registerValidator("OnPlayerLevel", function(eventStatus, pid) 36 | local player = Players[pid] 37 | if player.data.stats.level >= maxLevel then 38 | player.data.stats.level = maxLevel 39 | player.data.stats.levelProgress = 0 40 | player:LoadLevel() 41 | --cancel the level increase on the server side 42 | --there have been no level up anymore, so don't run custom handlers for it either 43 | return customEventHooks.makeEventStatus(false,false) 44 | end 45 | end) 46 | ``` 47 | 48 | Custom events 49 | --- 50 | 51 | You can also use this API to allow other scripts to interact with yours. For that you will need to add `customEventHooks.triggerValidators(event, args)` and `customEventHooks.triggerHandlers(event, eventStatus, args)` to your code. `event` is a string labeling the event, `eventStatus` should be whatever was returned by `triggerValidators` and `args` is a list or arguments relevant callbacks will receive. 52 | 53 | Here's an example from `eventHandler.lua`: 54 | ```Lua 55 | local eventStatus = customEventHooks.triggerValidators("OnPlayerLevel", {pid}) 56 | if eventStatus.validDefaultHandler then 57 | Players[pid]:SaveLevel() 58 | Players[pid]:SaveStatsDynamic() 59 | end 60 | customEventHooks.triggerHandlers("OnPlayerLevel", eventStatus, {pid}) 61 | ``` 62 | 63 | If you don't want other scripts replacing logic from yours, you can provide just the handlers: 64 | ```Lua 65 | customEventHooks.triggerHandlers("OnServerExit", customEventHooks.makeEventStatus(true, true), {}) 66 | ``` 67 | 68 | Using the customCommandHooks API: 69 | === 70 | 71 | To add a command, simply run `customCommandHooks.registerCommand(cmd, callback)`. Here `cmd` is the word after `/` which you want to trigger your command (e.g. "help" for `/help`) and callback is a function which will be ran when someone sends a message starting with "/" and `cmd`. 72 | 73 | Callback will receive as its arguments a player's `pid` and an array of all command parts (their message is split into parts by spaces, after removing the leading '/', same as in the old `commandHandler.lua`). 74 | 75 | You can then perform staff rank checks by calling `Players[pid]:IsAdmin()` etc. 76 | 77 | Event table 78 | === 79 | 80 | This table will follow this format: `event(args)`, where `event` and `args` are as described in *Using the customEventHooks API:* 81 | 82 | Most of the events are the same as `eventHandler.lua` functions, with some extra arguments: 83 | 84 | * OnPlayerConnect(pid) 85 | * OnPlayerDisconnect(pid) 86 | * OnGUIAction(pid, idGui, data) 87 | * OnPlayerSendMessage(pid, message) 88 | * OnPlayerDeath(pid) 89 | * OnDeathTimeExpiration(pid) 90 | * OnPlayerAttribute(pid) 91 | * OnPlayerSkill(pid) 92 | * OnPlayerLevel(pid) 93 | * OnPlayerShapeshift(pid) 94 | * OnPlayerCellChange(pid) 95 | * OnPlayerEndCharGen(pid) 96 | * OnPlayerEquipment(pid) 97 | * OnPlayerInventory(pid) 98 | * OnPlayerSpellbook(pid) 99 | * OnPlayerQuickKeys(pid) 100 | * OnPlayerJournal(pid) 101 | * OnPlayerFaction(pid, action) 102 | `action` is the result of `tes3mp.GetFactionChangesAction(pid)` (0 for RANK, 1 for EXPULSION, 2 for REPUTATION) 103 | * OnPlayerTopic(pid) 104 | * OnPlayerBounty(pid) 105 | * OnPlayerReputation(pid) 106 | * OnPlayerBook(pid) 107 | * OnPlayerItemUse(pid, itemRefId) 108 | * OnPlayerMiscellaneous(pid) 109 | * OnCellLoad(pid, cellDescription) 110 | * OnCellUnload(pid, cellDescription) 111 | * OnCellDeletion(cellDescription) 112 | * OnActorList(pid, cellDescription) 113 | * OnActorEquipment(pid, cellDescription) 114 | * OnActorAI(pid, cellDescription) 115 | * OnActorDeath(pid, cellDescription) 116 | * OnActorCellChange(pid, cellDescription) 117 | * OnObjectActivate(pid, cellDescription, objects, players) 118 | `objects` and `players` container lists of activated objects and players respectively. 119 | 120 | `objects` elements have form 121 | ` 122 | { 123 | uniqueIndex = ..., 124 | refId = ... 125 | } 126 | ` 127 | 128 | `players` elements have form 129 | ` 130 | { 131 | pid = ... 132 | } 133 | ` 134 | * OnObjectPlace(pid, cellDescription, objects) 135 | `objects` has the same structure as in `OnObjectActivate` 136 | * OnObjectSpawn(pid, cellDescription, objects) 137 | `objects` has the same structure as in `OnObjectActivate` 138 | * OnObjectDelete(pid, cellDescription, objects) 139 | `objects` has the same structure as in `OnObjectActivate` 140 | * OnObjectLock(pid, cellDescription, objects) 141 | `objects` has the same structure as in `OnObjectActivate` 142 | * OnObjectTrap(pid, cellDescription, objects) 143 | `objects` has the same structure as in `OnObjectActivate` 144 | * OnObjectScale(pid, cellDescription, objects) 145 | `objects` has the same structure as in `OnObjectActivate` 146 | * OnObjectState(pid, cellDescription, objects) 147 | `objects` has the same structure as in `OnObjectActivate` 148 | * OnDoorState(pid, cellDescription, objects) 149 | `objects` has the same structure as in `OnObjectActivate` 150 | * OnContainer(pid, cellDescription, objects) 151 | `objects` has the same structure as in `OnObjectActivate` 152 | * OnVideoPlay(pid, videos) 153 | `videos` is a list of video filenames 154 | * OnRecordDynamic(pid, recordTable, storeType) 155 | * OnWorldKillCount(pid) 156 | * OnWorldMap(pid) 157 | * OnWorldWeather(pid) 158 | * OnObjectLoopTimeExpiration(pid, loopIndex) 159 | `pid` is the loop's `targetPid` 160 | 161 | There are also some events not present in `eventHandler` before: 162 | 163 | * OnServerInit() 164 | * OnServerPostInit() 165 | * OnServerExit() 166 | Only has a handler trigger and no default behaviour to cancel. 167 | * OnLoginTimeExpiration(pid) 168 | * OnPlayerResurrect(pid) 169 | Only has a handler trigger and no default behaviour to cancel. 170 | * OnPlayerFinishLogin(pid) 171 | Only has a handler trigger and no default behaviour to cancel. 172 | * OnPlayerAuthentified(pid) 173 | Only has a handler trigger and no default behaviour to cancel. 174 | 175 | Is triggered after a player has finished login it, whether it was by making a new character (`OnPlayerEndCharGen`) or by logging in (`OnPlayerFinishLogin`) 176 | 177 | -------------------------------------------------------------------------------- /data/banlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "playerNames":[], 3 | "ipAddresses":[] 4 | } -------------------------------------------------------------------------------- /data/cell/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TES3MP/CoreScripts/d7d71d635cd0aa10dfa6b089a999e4113cd516c3/data/cell/.gitkeep -------------------------------------------------------------------------------- /data/custom/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TES3MP/CoreScripts/d7d71d635cd0aa10dfa6b089a999e4113cd516c3/data/custom/.gitkeep -------------------------------------------------------------------------------- /data/map/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TES3MP/CoreScripts/d7d71d635cd0aa10dfa6b089a999e4113cd516c3/data/map/.gitkeep -------------------------------------------------------------------------------- /data/player/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TES3MP/CoreScripts/d7d71d635cd0aa10dfa6b089a999e4113cd516c3/data/player/.gitkeep -------------------------------------------------------------------------------- /data/recordstore/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TES3MP/CoreScripts/d7d71d635cd0aa10dfa6b089a999e4113cd516c3/data/recordstore/.gitkeep -------------------------------------------------------------------------------- /data/requiredDataFiles.json: -------------------------------------------------------------------------------- 1 | // This file lets you enforce a certain plugin list and order for all clients 2 | // attempting to join this server 3 | // 4 | // By default, only the English and Russian editions of Morrowind are allowed, 5 | // because the German and French editions have hardcoded translations, whereas 6 | // the Russian edition has localization files that make it compatible with the 7 | // English edition 8 | 9 | [ 10 | {"Morrowind.esm": ["0x7B6AF5B9", "0x34282D67"]}, 11 | {"Tribunal.esm": ["0xF481F334", "0x211329EF"]}, 12 | {"Bloodmoon.esm": ["0x43DD2132", "0x9EB62F26"]} 13 | ] 14 | -------------------------------------------------------------------------------- /data/world/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TES3MP/CoreScripts/d7d71d635cd0aa10dfa6b089a999e4113cd516c3/data/world/.gitkeep -------------------------------------------------------------------------------- /lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TES3MP/CoreScripts/d7d71d635cd0aa10dfa6b089a999e4113cd516c3/lib/.gitkeep -------------------------------------------------------------------------------- /lib/lua/classy.lua: -------------------------------------------------------------------------------- 1 | -- class-based OO module for Lua 2 | 3 | -- cache globals 4 | local assert = assert 5 | local V = assert( _VERSION ) 6 | local setmetatable = assert( setmetatable ) 7 | local select = assert( select ) 8 | local pairs = assert( pairs ) 9 | local ipairs = assert( ipairs ) 10 | local type = assert( type ) 11 | local error = assert( error ) 12 | local load = assert( load ) 13 | local s_rep = assert( string.rep ) 14 | local t_unpack = assert( V == "Lua 5.1" and unpack or table.unpack ) 15 | 16 | 17 | -- list of all metamethods that a user of this library is allowed to 18 | -- add to a class 19 | local allowed_metamethods = { 20 | __add = true, __sub = true, __mul = true, __div = true, 21 | __mod = true, __pow = true, __unm = true, __concat = true, 22 | __len = true, __eq = true, __lt = true, __le = true, __call = true, 23 | __tostring = true, __pairs = true, __ipairs = true, __gc = true, 24 | __newindex = true, __metatable = true, __idiv = true, __band = true, 25 | __bor = true, __bxor = true, __bnot = true, __shl = true, 26 | __shr = true, 27 | } 28 | 29 | -- this metatable is (re-)used often: 30 | local mode_k_meta = { __mode = "k" } 31 | 32 | -- store information for every registered class (still in use) 33 | -- [ cls ] = { 34 | -- -- the name of the class 35 | -- name = "clsname", 36 | -- -- an array of superclasses in an order suitable for method 37 | -- -- lookup, the first n are direct superclasses (parents) 38 | -- super = { n = 2, super1, super2, super1_1, super1_2 }, 39 | -- -- a set of subclasses (value is "inheritance difference") 40 | -- sub = { [ subcls1 ] = 1, [ subcls2 ] = 2 }, -- mode="k" 41 | -- -- direct member functions/variables for this class 42 | -- members = {}, 43 | -- -- the metatable for objects of this class 44 | -- o_meta = { __index = {} }, 45 | -- -- the metatable for the class itself 46 | -- c_meta = { __index = ..., __call = ..., __newindex = ... }, 47 | -- } 48 | local classinfo = setmetatable( {}, mode_k_meta ) 49 | 50 | 51 | -- object constructor for the class if no custom __init function is 52 | -- defined 53 | local function default_constructor( meta ) 54 | return function() 55 | return setmetatable( {}, meta ) 56 | end 57 | end 58 | 59 | -- object constructor for the class if a custom __init function is 60 | -- available 61 | local function init_constructor( meta, init ) 62 | return function( _, ... ) 63 | local o = setmetatable( {}, meta ) 64 | init( o, ... ) 65 | return o 66 | end 67 | end 68 | 69 | 70 | -- propagate a changed method to a sub class 71 | local function propagate_update( cls, key ) 72 | local info = classinfo[ cls ] 73 | if info.members[ key ] ~= nil then 74 | info.o_meta.__index[ key ] = info.members[ key ] 75 | else 76 | for i = 1, #info.super do 77 | local val = classinfo[ info.super[ i ] ].members[ key ] 78 | if val ~= nil then 79 | info.o_meta.__index[ key ] = val 80 | return 81 | end 82 | end 83 | info.o_meta.__index[ key ] = nil 84 | end 85 | end 86 | 87 | 88 | -- __newindex handler for class proxy tables, allowing to set certain 89 | -- metamethods, initializers, and normal members. updates sub classes! 90 | local function class_newindex( cls, key, val ) 91 | local info = classinfo[ cls ] 92 | if allowed_metamethods[ key ] then 93 | assert( info.o_meta[ key ] == nil, 94 | "overwriting metamethods not allowed" ) 95 | info.o_meta[ key ] = val 96 | elseif key == "__init" then 97 | info.members.__init = val 98 | info.o_meta.__index.__init = val 99 | if type( val ) == "function" then 100 | info.c_meta.__call = init_constructor( info.o_meta, val ) 101 | else 102 | info.c_meta.__call = default_constructor( info.o_meta ) 103 | end 104 | else 105 | assert( key ~= "__class", "key '__class' is reserved" ) 106 | info.members[ key ] = val 107 | propagate_update( cls, key ) 108 | for sub in pairs( info.sub ) do 109 | propagate_update( sub, key ) 110 | end 111 | end 112 | end 113 | 114 | 115 | -- __pairs/__ipairs metamethods for iterating members of classes 116 | local function class_pairs( cls ) 117 | return pairs( classinfo[ cls ].o_meta.__index ) 118 | end 119 | 120 | local function class_ipairs( cls ) 121 | return ipairs( classinfo[ cls ].o_meta.__index ) 122 | end 123 | 124 | 125 | -- put the inheritance tree into a flat array using a width-first 126 | -- iteration (similar to a binary heap); also set the "inheritance 127 | -- difference" in superclasses 128 | local function linearize_ancestors( cls, super, ... ) 129 | local n = select( '#', ... ) 130 | for i = 1, n do 131 | local pcls = select( i, ... ) 132 | assert( classinfo[ pcls ], "invalid class" ) 133 | super[ i ] = pcls 134 | end 135 | super.n = n 136 | local diff, newn = 1, n 137 | for i,p in ipairs( super ) do 138 | local pinfo = classinfo[ p ] 139 | local psuper, psub = pinfo.super, pinfo.sub 140 | if not psub[ cls ] then psub[ cls ] = diff end 141 | for i = 1, psuper.n do 142 | super[ #super+1 ] = psuper[ i ] 143 | end 144 | newn = newn + psuper.n 145 | if i == n then 146 | n, diff = newn, diff+1 147 | end 148 | end 149 | end 150 | 151 | 152 | -- create the necessary metadata for the class, setup the inheritance 153 | -- hierarchy, set a suitable metatable, and return the class 154 | local function create_class( _, name, ... ) 155 | assert( type( name ) == "string", "class name must be a string" ) 156 | local cls, index = {}, {} 157 | local o_meta = { 158 | __index = index, 159 | __name = name, 160 | } 161 | local info = { 162 | name = name, 163 | super = { n = 0 }, 164 | sub = setmetatable( {}, mode_k_meta ), 165 | members = {}, 166 | o_meta = o_meta, 167 | c_meta = { 168 | __index = index, 169 | __newindex = class_newindex, 170 | __call = default_constructor( o_meta ), 171 | __pairs = class_pairs, 172 | __ipairs = class_ipairs, 173 | __name = "class", 174 | __metatable = false, 175 | }, 176 | } 177 | linearize_ancestors( cls, info.super, ... ) 178 | for i = #info.super, 1, -1 do 179 | for k,v in pairs( classinfo[ info.super[ i ] ].members ) do 180 | if k ~= "__init" then index[ k ] = v end 181 | end 182 | end 183 | index.__class = cls 184 | classinfo[ cls ] = info 185 | return setmetatable( cls, info.c_meta ) 186 | end 187 | 188 | 189 | -- the exported class module 190 | local M = {} 191 | setmetatable( M, { __call = create_class } ) 192 | 193 | 194 | -- returns the class of an object 195 | function M.of( o ) 196 | return type( o ) == "table" and o.__class or nil 197 | end 198 | 199 | 200 | -- returns the class name of an object or class 201 | function M.name( oc ) 202 | if oc == nil then return nil end 203 | oc = type( oc ) == "table" and oc.__class or oc 204 | local info = classinfo[ oc ] 205 | return info and info.name 206 | end 207 | 208 | 209 | -- checks if an object or class is in an inheritance 210 | -- relationship with a given class 211 | function M.is_a( oc, cls ) 212 | if oc == nil then return nil end 213 | local info = assert( classinfo[ cls ], "invalid class" ) 214 | oc = type( oc ) == "table" and oc.__class or oc 215 | if oc == cls then return 0 end 216 | return info.sub[ oc ] 217 | end 218 | 219 | 220 | -- change the type of an object to the new class 221 | function M.cast( o, newcls ) 222 | local info = classinfo[ newcls ] 223 | if not info then 224 | error( "invalid class" ) 225 | end 226 | setmetatable( o, info.o_meta ) 227 | return o 228 | end 229 | 230 | 231 | local function make_delegate( cls, field, method ) 232 | cls[ method ] = function( self, ... ) 233 | local obj = self[ field ] 234 | return obj[ method ]( obj, ... ) 235 | end 236 | end 237 | 238 | -- create delegation methods 239 | function M.delegate( cls, fieldname, ... ) 240 | if type( (...) ) == "table" then 241 | for k,v in pairs( (...) ) do 242 | if cls[ k ] == nil and k ~= "__init" and 243 | type( v ) == "function" then 244 | make_delegate( cls, fieldname, k ) 245 | end 246 | end 247 | else 248 | for i = 1, select( '#', ... ) do 249 | local k = select( i, ... ) 250 | if cls[ k ] == nil and k ~= "__init" then 251 | make_delegate( cls, fieldname, k ) 252 | end 253 | end 254 | end 255 | return cls 256 | end 257 | 258 | 259 | -- multimethod stuff 260 | do 261 | -- store multimethods and map them to the meta-data 262 | local mminfo = setmetatable( {}, mode_k_meta ) 263 | 264 | local erroffset = 0 265 | if V == "Lua 5.1" then erroffset = 1 end 266 | 267 | local function no_match2() 268 | error( "no matching multimethod overload", 2+erroffset ) 269 | end 270 | 271 | local function no_match3() 272 | error( "no matching multimethod overload", 3+erroffset ) 273 | end 274 | 275 | local function amb_call() 276 | error( "ambiguous multimethod call", 3+erroffset ) 277 | end 278 | 279 | local empty = {} -- just an empty table used as dummy 280 | local FIRST_OL = 4 -- index of first overload specification 281 | 282 | 283 | -- create a multimethod using the parameter indices given 284 | -- as arguments for dynamic dispatch 285 | function M.multimethod( ... ) 286 | local t, n = { ... }, select( '#', ... ) 287 | assert( n >= 1, "no polymorphic parameter for multimethod" ) 288 | local max = 0 289 | for i = 1, n do 290 | local x = t[ i ] 291 | max = assert( x > max and x % 1 == 0 and x, 292 | "invalid parameter overload specification" ) 293 | end 294 | local mm_impl = { no_match2, t, max } 295 | local function mm( ... ) 296 | return mm_impl[ 1 ]( mm_impl, ... ) 297 | end 298 | mminfo[ mm ] = mm_impl 299 | return mm 300 | end 301 | 302 | 303 | local function make_weak() 304 | return setmetatable( {}, mode_k_meta ) 305 | end 306 | 307 | 308 | local function calculate_cost( ol, ... ) 309 | local c = 0 310 | for i = 1, select( '#', ... ) do 311 | local a, pt = ol[ i ], select( i, ... ) 312 | if type( a ) == "table" then -- class table 313 | local info = classinfo[ a ] 314 | local diff = (pt == a) and 0 or info and info.sub[ pt ] 315 | if not diff then return nil end 316 | c = c + diff 317 | else -- type name 318 | if pt ~= a then return nil end 319 | end 320 | end 321 | return c 322 | end 323 | 324 | 325 | local function select_impl( cost, f, amb, ol, ... ) 326 | local c = calculate_cost( ol, ... ) 327 | if c then 328 | if cost then 329 | if c < cost then 330 | cost, f, amb = c, ol.func, false 331 | elseif c == cost then 332 | amb = true 333 | end 334 | else 335 | cost, f, amb = c, ol.func, false 336 | end 337 | end 338 | return cost, f, amb 339 | end 340 | 341 | 342 | local function collect_type_checkers( mm, a ) 343 | local funcs = {}, {} 344 | for i = FIRST_OL, #mm do 345 | local ol = mm[ i ] 346 | for k,v in pairs( ol ) do 347 | if type( k ) == "function" and 348 | (a == nil or v[ a ]) and 349 | not funcs[ k ] then 350 | local j = #funcs+1 351 | funcs[ j ] = k 352 | funcs[ k ] = j 353 | end 354 | end 355 | end 356 | return funcs 357 | end 358 | 359 | 360 | local function c_varlist( t, m, prefix ) 361 | local n = #t 362 | if m >= 1 then 363 | t[ n+1 ] = prefix 364 | t[ n+2 ] = 1 365 | end 366 | for i = 2, m do 367 | local j = i*3+n 368 | t[ j-3 ] = "," 369 | t[ j-2 ] = prefix 370 | t[ j-1 ] = i 371 | end 372 | end 373 | 374 | local function c_typecheck( t, mm, funcs, j ) 375 | local n, ai = #t, mm[ 2 ][ j ] 376 | t[ n+1 ] = " t=type(_" 377 | t[ n+2 ] = ai 378 | t[ n+3 ] = ")\n local t" 379 | t[ n+4 ] = j 380 | t[ n+5 ] = "=(t=='table' and _" 381 | t[ n+6 ] = ai 382 | t[ n+7 ] = ".__class) or " 383 | local ltcs = collect_type_checkers( mm, j ) 384 | local m = #ltcs 385 | for i = 1, m do 386 | local k = i*5+n+3 387 | t[ k ] = "tc" 388 | t[ k+1 ] = funcs[ ltcs[ i ] ] 389 | t[ k+2 ] = "(_" 390 | t[ k+3 ] = ai 391 | t[ k+4 ] = ") or " 392 | end 393 | t[ m*5+n+8 ] = "t\n" 394 | end 395 | 396 | local function c_cache( t, mm ) 397 | local c = #mm[ 2 ] 398 | local n = #t 399 | t[ n+1 ] = s_rep( "(", c-1 ) 400 | t[ n+2 ] = "cache" 401 | for i = 1, c-1 do 402 | local j = i*3+n 403 | t[ j ] = "[t" 404 | t[ j+1 ] = i 405 | t[ j+2 ] = "] or empty)" 406 | end 407 | local j = c*3+n 408 | t[ j ] = "[t" 409 | t[ j+1 ] = c 410 | t[ j+2 ] = "]\n" 411 | end 412 | 413 | local function c_costcheck( t, i, j ) 414 | local n = #t 415 | t[ n+1 ] = " cost,f,is_amb=sel_impl(cost,f,is_amb,mm[" 416 | t[ n+2 ] = j+FIRST_OL-1 417 | t[ n+3 ] = "]," 418 | c_varlist( t, i, "t" ) 419 | t[ #t+1 ] = ")\n" 420 | end 421 | 422 | local function c_updatecache( t, i ) 423 | local n = #t 424 | t[ n+1 ] = " if not t[t" 425 | t[ n+2 ] = i 426 | t[ n+3 ] = "] then t[t" 427 | t[ n+4 ] = i 428 | t[ n+5 ] = "]=mk_weak() end\n t=t[t" 429 | t[ n+6 ] = i 430 | t[ n+7 ] = "]\n" 431 | end 432 | 433 | 434 | local function recompile_and_call( mm, ... ) 435 | local n = #mm[ 2 ] -- number of polymorphic parameters 436 | local tcs = collect_type_checkers( mm ) 437 | local code = { 438 | "local type,cache,empty,mk_weak,sel_impl,no_match,amb_call" 439 | } 440 | if #tcs >= 1 then 441 | code[ #code+1 ] = "," 442 | end 443 | c_varlist( code, #tcs, "tc" ) 444 | code[ #code+1 ] = "=...\nreturn function(mm," 445 | c_varlist( code, mm[ 3 ], "_" ) 446 | code[ #code+1 ] = ",...)\n local t\n" 447 | for i = 1, n do 448 | c_typecheck( code, mm, tcs, i ) 449 | end 450 | code[ #code+1 ] = " local f=" 451 | c_cache( code, mm ) 452 | code[ #code+1 ] = [=[ 453 | if f==nil then 454 | local is_amb,cost 455 | ]=] 456 | for i = 1, #mm-FIRST_OL+1 do 457 | c_costcheck( code, n, i ) 458 | end 459 | code[ #code+1 ] = [=[ 460 | if f==nil then 461 | no_match() 462 | elseif is_amb then 463 | amb_call() 464 | end 465 | t=cache 466 | ]=] 467 | for i = 1, n-1 do 468 | c_updatecache( code, i ) 469 | end 470 | code[ #code+1 ] = " t[t" 471 | code[ #code+1 ] = n 472 | code[ #code+1 ] = "]=f\n end\n return f(" 473 | c_varlist( code, mm[ 3 ], "_" ) 474 | code[ #code+1 ] = ",...)\nend\n" 475 | local i = 0 476 | local function ld() 477 | i = i + 1 478 | return code[ i ] 479 | end 480 | --print( table.concat( code ) ) -- XXX 481 | local f = assert( load( ld, "=[multimethod]" ) )( 482 | type, make_weak(), empty, make_weak, select_impl, no_match3, 483 | amb_call, t_unpack( tcs ) 484 | ) 485 | mm[ 1 ] = f 486 | return f( mm, ... ) 487 | end 488 | 489 | 490 | -- register a new overload for this multimethod 491 | function M.overload( f, ... ) 492 | local mm = assert( type( f ) == "function" and mminfo[ f ], 493 | "argument is not a multimethod" ) 494 | local i, n = 1, select( '#', ... ) 495 | local ol = {} 496 | local func = assert( n >= 1 and select( n, ... ), 497 | "missing function in overload specification" ) 498 | while i < n do 499 | local a = select( i, ... ) 500 | local t = type( a ) 501 | if t == "string" then 502 | ol[ #ol+1 ] = a 503 | elseif t == "table" then 504 | assert( classinfo[ a ], "invalid class" ) 505 | ol[ #ol+1 ] = a 506 | else 507 | assert( t == "function", "invalid overload specification" ) 508 | i = i + 1 509 | assert( i < n, "missing function in overload specification" ) 510 | ol[ a ] = ol[ a ] or {} 511 | ol[ #ol+1 ] = select( i, ... ) 512 | ol[ a ][ #ol ] = true 513 | end 514 | i = i + 1 515 | end 516 | assert( #mm[ 2 ] == #ol, "wrong number of overloaded parameters" ) 517 | ol.func = func 518 | mm[ #mm+1 ] = ol 519 | mm[ 1 ] = recompile_and_call 520 | end 521 | 522 | end 523 | 524 | 525 | -- return module table 526 | return M 527 | 528 | -------------------------------------------------------------------------------- /lib/lua/fileHelper.lua: -------------------------------------------------------------------------------- 1 | require("patterns") 2 | tableHelper = require("tableHelper") 3 | 4 | local fileHelper = {} 5 | 6 | -- Avoid using the following filenames because of their reserved status on operating systems 7 | fileHelper.invalidFilenames = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", 8 | "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", 9 | "LPT8", "LPT9" } 10 | 11 | -- Turn an invalid filename into a valid one 12 | function fileHelper.fixFilename(filename) 13 | 14 | -- Trim spaces at the start and end of the filename 15 | filename = filename:trim() 16 | 17 | -- Replace characters not allowed in filenames 18 | filename = string.gsub(filename, ":", ";") 19 | filename = string.gsub(filename, patterns.invalidFileCharacters, "_") 20 | 21 | -- Also replace periods because of their special meaning, i.e. a file named 22 | -- AUX.test.json would not get fixed in the loop below, but would still be 23 | -- invalid on Windows 24 | filename = string.gsub(filename, "%.", ",") 25 | 26 | -- If the filename itself is invalid, add an underline at the start 27 | if tableHelper.containsValue(fileHelper.invalidFilenames, filename) then 28 | filename = "_" .. filename 29 | end 30 | 31 | return filename 32 | end 33 | 34 | return fileHelper 35 | -------------------------------------------------------------------------------- /lib/lua/jsonInterface.lua: -------------------------------------------------------------------------------- 1 | local dkjson = require("dkjson") 2 | local cjson 3 | local cjsonExists = doesModuleExist("cjson") 4 | 5 | if cjsonExists then 6 | cjson = require("cjson") 7 | cjson.encode_sparse_array(true) 8 | cjson.encode_invalid_numbers("null") 9 | cjson.encode_empty_table_as_object(false) 10 | cjson.decode_null_as_lightuserdata(false) 11 | else 12 | tes3mp.LogMessage(enumerations.log.ERROR, "Could not find Lua CJSON! The decoding and encoding of JSON files will always use dkjson and be slower as a result.") 13 | end 14 | 15 | local jsonInterface = {} 16 | 17 | jsonInterface.libraryMissingMessage = "No input/output library selected for JSON interface!" 18 | 19 | function jsonInterface.setLibrary(ioLibrary) 20 | jsonInterface.ioLibrary = ioLibrary 21 | end 22 | 23 | -- Remove all text from before the actual JSON content starts 24 | function jsonInterface.removeHeader(content) 25 | 26 | local closestBracketIndex 27 | 28 | local bracketIndex1 = content:find("\n%[") 29 | local bracketIndex2 = content:find("\n{") 30 | 31 | if bracketIndex1 and bracketIndex2 then 32 | closestBracketIndex = math.min(bracketIndex1, bracketIndex2) 33 | else 34 | closestBracketIndex = bracketIndex1 or bracketIndex2 35 | end 36 | 37 | return content:sub(closestBracketIndex) 38 | end 39 | 40 | function jsonInterface.load(fileName) 41 | 42 | if jsonInterface.ioLibrary == nil then 43 | tes3mp.LogMessage(enumerations.log.ERROR, jsonInterface.libraryMissingMessage) 44 | return nil 45 | end 46 | 47 | local home = config.dataPath .. "/" 48 | local file = jsonInterface.ioLibrary.open(home .. fileName, 'r') 49 | 50 | if file ~= nil then 51 | local content = file:read("*all") 52 | file:close() 53 | 54 | if cjsonExists then 55 | -- Lua CJSON does not support comments before the JSON data, so remove them if 56 | -- they are present 57 | if content:sub(1, 2) == "//" then 58 | content = jsonInterface.removeHeader(content) 59 | end 60 | 61 | local decodedContent 62 | local status, result = pcall(function() decodedContent = cjson.decode(content) end) 63 | 64 | if status then 65 | return decodedContent 66 | else 67 | tes3mp.LogMessage(enumerations.log.ERROR, "Could not load " .. fileName .. " using Lua CJSON " .. 68 | "due to improperly formatted JSON! Error:\n" .. result .. "\n" .. fileName .. " is being read " .. 69 | "via the slower dkjson instead.") 70 | end 71 | end 72 | 73 | return dkjson.decode(content) 74 | else 75 | return nil 76 | end 77 | end 78 | 79 | 80 | function jsonInterface.writeToFile(fileName, content) 81 | 82 | if jsonInterface.ioLibrary == nil then 83 | tes3mp.LogMessage(enumerations.log.ERROR, jsonInterface.libraryMissingMessage) 84 | return false 85 | end 86 | 87 | local home = config.dataPath .. "/" 88 | local file = jsonInterface.ioLibrary.open(home .. fileName, 'w+b') 89 | 90 | if file ~= nil then 91 | file:write(content) 92 | file:close() 93 | return true 94 | else 95 | return false 96 | end 97 | end 98 | 99 | -- Save data to JSON in a slower but human-readable way, with identation and a specific order 100 | -- to the keys, provided via dkjson 101 | function jsonInterface.save(fileName, data, keyOrderArray) 102 | 103 | local content = dkjson.encode(data, { indent = true, keyorder = keyOrderArray }) 104 | 105 | return jsonInterface.writeToFile(fileName, content) 106 | end 107 | 108 | -- Save data to JSON in a fast but minimized way, provided via Lua CJSON, ideal for large files 109 | -- that need to be saved over and over 110 | function jsonInterface.quicksave(fileName, data) 111 | 112 | if cjsonExists then 113 | local content = cjson.encode(data) 114 | return jsonInterface.writeToFile(fileName, content) 115 | else 116 | return jsonInterface.save(fileName, data) 117 | end 118 | end 119 | 120 | return jsonInterface 121 | -------------------------------------------------------------------------------- /lib/lua/patterns.lua: -------------------------------------------------------------------------------- 1 | patterns = {} 2 | patterns.invalidFileCharacters = '[<>:"/\\|*?\r\n]' -- characters not allowed in filenames 3 | patterns.commaSplit = "%s*([^,]+)" -- strings separated by commas, with spaces immediately after the commas ignored 4 | patterns.periodSplit = "%s*([^%.]+)" -- as in commaSplit, but with periods 5 | patterns.quoteSplit = '".-"' -- strings separated by quotation marks 6 | patterns.exteriorCell = "(%-?%d+), ?(%-?%d+)$" -- X coordinate, Y coordinate 7 | patterns.item = "(.+), (%d+), (%-?%d+)$" -- refId, count, charge 8 | patterns.coordinates = "(%-?%d+%.?%d*), (%-?%d+%.?%d*), (%-?%d+%.?%d*)$" -- X coordinate, Y coordinate, Z coordinate 9 | 10 | return patterns 11 | -------------------------------------------------------------------------------- /lib/lua/tableHelper.lua: -------------------------------------------------------------------------------- 1 | local tableHelper = {} 2 | 3 | -- Swap keys with their values in a table, allowing for the easy creation of tables similar to enums 4 | function tableHelper.enum(inputTable) 5 | local newTable = {} 6 | for key, value in ipairs(inputTable) do 7 | newTable[value] = key 8 | end 9 | return newTable 10 | end 11 | 12 | function tableHelper.getCount(inputTable) 13 | local count = 0 14 | for key in pairs(inputTable) do count = count + 1 end 15 | return count 16 | end 17 | 18 | -- Iterate through a table's indexes and put them into an array table 19 | function tableHelper.getArrayFromIndexes(inputTable) 20 | 21 | local newTable = {} 22 | 23 | for key, _ in pairs(inputTable) do 24 | table.insert(newTable, key) 25 | end 26 | 27 | return newTable 28 | end 29 | 30 | -- Return an array with the values that two input tables have in common 31 | function tableHelper.getValueOverlap(firstTable, secondTable) 32 | 33 | local newTable = {} 34 | 35 | for _, value in pairs(firstTable) do 36 | if tableHelper.containsValue(secondTable, value) then 37 | table.insert(newTable, value) 38 | end 39 | end 40 | 41 | return newTable 42 | end 43 | 44 | -- Iterate through values matching a pattern in a string and turn them into table values 45 | function tableHelper.getTableFromSplit(inputString, pattern) 46 | 47 | local newTable = {} 48 | 49 | for value in string.gmatch(inputString, pattern) do 50 | table.insert(newTable, value) 51 | end 52 | 53 | return newTable 54 | end 55 | 56 | -- Iterate through comma-separated values in a string and turn them into table values 57 | function tableHelper.getTableFromCommaSplit(inputString) 58 | return tableHelper.getTableFromSplit(inputString, patterns.commaSplit) 59 | end 60 | 61 | -- Concatenate the indexes in a table, useful for printing out all the valid 62 | -- indexes 63 | function tableHelper.concatenateTableIndexes(inputTable, delimiter) 64 | 65 | local resultString = "" 66 | local tableCount = tableHelper.getCount(inputTable) 67 | local indexesSoFar = 1 68 | 69 | if delimiter == nil then 70 | delimiter = " " 71 | end 72 | 73 | for index, value in pairs(inputTable) do 74 | 75 | resultString = resultString .. index 76 | 77 | if indexesSoFar < tableCount then 78 | resultString = resultString .. delimiter 79 | end 80 | 81 | indexesSoFar = indexesSoFar + 1 82 | end 83 | 84 | return resultString 85 | end 86 | 87 | -- Concatenate the values in an array, useful for printing out the array's 88 | -- contents, with an optional delimiter between values 89 | function tableHelper.concatenateArrayValues(inputTable, startIndex, delimiter) 90 | 91 | local resultString = "" 92 | 93 | if startIndex == nil then 94 | startIndex = 1 95 | end 96 | 97 | if delimiter == nil then 98 | delimiter = " " 99 | end 100 | 101 | for i = startIndex, #inputTable do 102 | resultString = resultString .. inputTable[i] 103 | 104 | if i ~= #inputTable then 105 | resultString = resultString .. delimiter 106 | end 107 | end 108 | 109 | return resultString 110 | end 111 | 112 | function tableHelper.concatenateFromIndex(inputTable, startIndex, delimiter) 113 | 114 | return tableHelper.concatenateArrayValues(inputTable, startIndex, delimiter) 115 | end 116 | 117 | -- Check whether a table contains a key/value pair, optionally checking inside 118 | -- nested tables 119 | function tableHelper.containsKeyValue(inputTable, keyToFind, valueToFind, checkNestedTables) 120 | 121 | if inputTable[keyToFind] ~= nil then 122 | if inputTable[keyToFind] == valueToFind then 123 | return true 124 | end 125 | end 126 | 127 | if checkNestedTables then 128 | for key, value in pairs(inputTable) do 129 | if type(value) == "table" and tableHelper.containsKeyValue(value, keyToFind, valueToFind, true) then 130 | return true 131 | end 132 | end 133 | end 134 | 135 | return false 136 | end 137 | 138 | -- Check whether a table contains a set of key/value pairs, optionally checking inside 139 | -- tables nested in the original one 140 | function tableHelper.containsKeyValuePairs(inputTable, keyValuePairsTable, checkNestedTables) 141 | 142 | local foundMatches = true 143 | 144 | for keyToFind, valueToFind in pairs(keyValuePairsTable) do 145 | if inputTable[keyToFind] == nil or inputTable[keyToFind] ~= valueToFind then 146 | foundMatches = false 147 | break 148 | end 149 | end 150 | 151 | if foundMatches then 152 | return true 153 | elseif checkNestedTables then 154 | for key, value in pairs(inputTable) do 155 | if type(value) == "table" and tableHelper.containsKeyValuePairs(value, keyValuePairsTable, true) then 156 | return true 157 | end 158 | end 159 | end 160 | 161 | return false 162 | end 163 | 164 | -- Check whether a table contains a certain value, optionally checking inside 165 | -- nested tables 166 | function tableHelper.containsValue(inputTable, valueToFind, checkNestedTables) 167 | for key, value in pairs(inputTable) do 168 | if checkNestedTables and type(value) == "table" then 169 | if tableHelper.containsValue(value, valueToFind, true) then 170 | return true 171 | end 172 | elseif value == valueToFind then 173 | return true 174 | end 175 | end 176 | return false 177 | end 178 | 179 | -- Check whether a table contains a certain case insensitive string, optionally 180 | -- checking inside nested tables 181 | function tableHelper.containsCaseInsensitiveString(inputTable, stringToFind, checkNestedTables) 182 | 183 | if type(stringToFind) ~= "string" then return false end 184 | 185 | for key, value in pairs(inputTable) do 186 | if checkNestedTables and type(value) == "table" then 187 | if tableHelper.containsCaseInsensitiveString(value, stringToFind, true) then 188 | return true 189 | end 190 | elseif type(value) == "string" and string.lower(value) == string.lower(stringToFind) then 191 | return true 192 | end 193 | end 194 | return false 195 | end 196 | 197 | function tableHelper.insertValueIfMissing(inputTable, value) 198 | if tableHelper.containsValue(inputTable, value, false) == false then 199 | table.insert(inputTable, value) 200 | end 201 | end 202 | 203 | function tableHelper.getAnyValue(inputTable) 204 | for key, value in pairs(inputTable) do 205 | return value 206 | end 207 | end 208 | 209 | function tableHelper.getUnusedNumericalIndex(inputTable) 210 | local i = 1 211 | 212 | while inputTable[i] ~= nil do 213 | i = i + 1 214 | end 215 | 216 | return i 217 | end 218 | 219 | function tableHelper.getIndexByPattern(inputTable, patternToFind) 220 | for key, value in pairs(inputTable) do 221 | if string.match(value, patternToFind) ~= nil then 222 | return key 223 | end 224 | end 225 | return nil 226 | end 227 | 228 | function tableHelper.getIndexByNestedKeyValue(inputTable, keyToFind, valueToFind) 229 | for key, value in pairs(inputTable) do 230 | if type(value) == "table" then 231 | if tableHelper.containsKeyValue(value, keyToFind, valueToFind) == true then 232 | return key 233 | end 234 | end 235 | end 236 | return nil 237 | end 238 | 239 | function tableHelper.getIndexByValue(inputTable, valueToFind) 240 | for key, value in pairs(inputTable) do 241 | if value == valueToFind then 242 | return key 243 | end 244 | end 245 | 246 | return nil 247 | end 248 | 249 | -- Iterate through a table and return a new table based on it that has no nil values 250 | -- (useful for numerical arrays because they retain nil values) 251 | -- 252 | -- Based on http://stackoverflow.com/a/28302975 253 | function tableHelper.cleanNils(inputTable) 254 | 255 | local newTable = {} 256 | 257 | for key, value in pairs(inputTable) do 258 | if type(value) == "table" then 259 | tableHelper.cleanNils(value) 260 | end 261 | 262 | if type(key) == "number" then 263 | newTable[#newTable + 1] = value 264 | inputTable[key] = nil 265 | end 266 | end 267 | 268 | tableHelper.merge(inputTable, newTable) 269 | end 270 | 271 | -- Set values to nil here instead of using table.remove(), so this method can be used on 272 | -- a table while iterating through it 273 | function tableHelper.removeValue(inputTable, valueToFind) 274 | 275 | tableHelper.replaceValue(inputTable, valueToFind, nil) 276 | end 277 | 278 | function tableHelper.replaceValue(inputTable, valueToFind, newValue) 279 | for key, value in pairs(inputTable) do 280 | if type(value) == "table" then 281 | tableHelper.replaceValue(value, valueToFind, newValue) 282 | elseif value == valueToFind then 283 | inputTable[key] = newValue 284 | end 285 | end 286 | end 287 | 288 | -- Add a 2nd table's key/value pairs to the 1st table 289 | -- 290 | -- Note: If they share keys, the values of the 2nd table will overwrite the ones 291 | -- from the 1st table, unless both tables are arrays and combineArrays is true, 292 | -- in which case the non-duplicate values in the 2nd table will be added to the 1st 293 | function tableHelper.merge(mainTable, addedTable, combineArrays) 294 | 295 | if tableHelper.isArray(mainTable) and tableHelper.isArray(addedTable) and combineArrays then 296 | tableHelper.insertValues(mainTable, addedTable, true) 297 | else 298 | for key, value in pairs(addedTable) do 299 | if mainTable[key] == nil then 300 | if type(value) == "table" then 301 | mainTable[key] = tableHelper.shallowCopy(value) 302 | else 303 | mainTable[key] = value 304 | end 305 | elseif type(value) == "table" then 306 | tableHelper.merge(mainTable[key], value, combineArrays) 307 | else 308 | mainTable[key] = value 309 | end 310 | end 311 | end 312 | end 313 | 314 | -- Insert all the values from the 2nd table into the 1st table 315 | function tableHelper.insertValues(mainTable, addedTable, skipDuplicates) 316 | 317 | for _, value in pairs(addedTable) do 318 | if not skipDuplicates or not tableHelper.containsValue(mainTable, value) then 319 | table.insert(mainTable, value) 320 | end 321 | end 322 | end 323 | 324 | -- Convert string keys containing numbers into numerical keys, 325 | -- useful for JSON tables 326 | -- 327 | -- Because Lua arrays start from index 1, the fixZeroStart argument 328 | -- can be set to true to increment all of the keys by 1 in tables that 329 | -- start from 0 330 | function tableHelper.fixNumericalKeys(inputTable, fixZeroStart) 331 | 332 | local newTable = {} 333 | local incrementKeys = false 334 | 335 | if inputTable["0"] ~= nil and fixZeroStart then 336 | incrementKeys = true 337 | end 338 | 339 | for key, value in pairs(inputTable) do 340 | 341 | if type(value) == "table" then 342 | tableHelper.fixNumericalKeys(value) 343 | end 344 | 345 | if type(key) ~= "number" and type(tonumber(key)) == "number" then 346 | 347 | local newKey = tonumber(key) 348 | 349 | if incrementKeys then 350 | newKey = newKey + 1 351 | end 352 | 353 | newTable[newKey] = value 354 | inputTable[key] = nil 355 | end 356 | end 357 | 358 | tableHelper.merge(inputTable, newTable) 359 | end 360 | 361 | -- Check whether the table contains only numerical keys, though they 362 | -- don't have to be consecutive 363 | function tableHelper.usesNumericalKeys(inputTable) 364 | 365 | if tableHelper.getCount(inputTable) == 0 then 366 | return false 367 | end 368 | 369 | for key, value in pairs(inputTable) do 370 | if type(key) ~= "number" then 371 | return false 372 | end 373 | end 374 | 375 | return true 376 | end 377 | 378 | -- Check whether the table contains only numerical values 379 | function tableHelper.usesNumericalValues(inputTable) 380 | 381 | if tableHelper.getCount(inputTable) == 0 then 382 | return false 383 | end 384 | 385 | for key, value in pairs(inputTable) do 386 | if type(value) ~= "number" then 387 | return false 388 | end 389 | end 390 | 391 | return true 392 | end 393 | 394 | -- Check whether there are any items in the table 395 | function tableHelper.isEmpty(inputTable) 396 | if next(inputTable) == nil then 397 | return true 398 | end 399 | 400 | return false 401 | end 402 | 403 | -- Check whether the table is an array with only consecutive numerical keys, 404 | -- i.e. without any gaps between keys 405 | -- Based on http://stackoverflow.com/a/6080274 406 | function tableHelper.isArray(inputTable) 407 | 408 | local index = 0 409 | 410 | for _ in pairs(inputTable) do 411 | index = index + 1 412 | if inputTable[index] == nil then return false end 413 | end 414 | 415 | return true 416 | end 417 | 418 | -- Check whether the table has the same keys and values as another table, optionally 419 | -- ignoring certain keys 420 | function tableHelper.isEqualTo(firstTable, secondTable, ignoredKeys) 421 | 422 | local hasIgnoredKeys = ignoredKeys ~= nil and not tableHelper.isEmpty(ignoredKeys) 423 | 424 | -- Is this the exact same table? 425 | if firstTable == secondTable then 426 | return true 427 | end 428 | 429 | if not hasIgnoredKeys and tableHelper.getCount(firstTable) ~= tableHelper.getCount(secondTable) then 430 | return false 431 | end 432 | 433 | for key, value in pairs(firstTable) do 434 | 435 | if not hasIgnoredKeys or not tableHelper.containsValue(ignoredKeys, key) then 436 | 437 | if secondTable[key] == nil then 438 | return false 439 | elseif type(value) == "table" and type(secondTable[key]) == "table" then 440 | if not tableHelper.isEqualTo(value, secondTable[key]) then 441 | return false 442 | end 443 | elseif value ~= secondTable[key] then 444 | return false 445 | end 446 | end 447 | end 448 | 449 | return true 450 | end 451 | 452 | -- Copy the value of a variable in a naive and simple way, useful for copying a table's top 453 | -- level values and direct children to another table, but still assigning references 454 | -- for deeper children which can cause unexpected behavior 455 | -- 456 | -- Note: This is only kept here for the sake of backwards compatibility, with use of the 457 | -- deepCopy() method from below being preferable in any new scripts. 458 | -- 459 | -- Based on http://lua-users.org/wiki/CopyTable 460 | function tableHelper.shallowCopy(inputValue) 461 | 462 | local inputType = type(inputValue) 463 | 464 | local newValue 465 | 466 | if inputType == "table" then 467 | newValue = {} 468 | for innerKey, innerValue in pairs(inputValue) do 469 | newValue[innerKey] = innerValue 470 | end 471 | else -- number, string, boolean, etc 472 | newValue = inputValue 473 | end 474 | 475 | return newValue 476 | end 477 | 478 | -- Copy the value of a variable in a deep way, useful for copying a table's top level values 479 | -- and direct children to another table safely, also handling metatables 480 | -- 481 | -- Based on http://lua-users.org/wiki/CopyTable 482 | function tableHelper.deepCopy(inputValue) 483 | 484 | local inputType = type(inputValue) 485 | 486 | local newValue 487 | 488 | if inputType == "table" then 489 | newValue = {} 490 | for innerKey, innerValue in next, inputValue, nil do 491 | newValue[tableHelper.deepCopy(innerKey)] = tableHelper.deepCopy(innerValue) 492 | end 493 | setmetatable(newValue, tableHelper.deepCopy(getmetatable(inputValue))) 494 | else -- number, string, boolean, etc 495 | newValue = inputValue 496 | end 497 | 498 | return newValue 499 | end 500 | 501 | -- Get a compact string with a table's contents 502 | function tableHelper.getSimplePrintableTable(inputTable) 503 | 504 | local text = "" 505 | local shouldPrintComma = false 506 | 507 | for index, value in pairs(inputTable) do 508 | if shouldPrintComma then 509 | text = text .. ", " 510 | end 511 | 512 | if type(value) == "table" then 513 | text = text .. "[" .. tableHelper.getSimplePrintableTable(value) .. "]" 514 | else 515 | text = text .. index .. ": " .. tostring(value) 516 | end 517 | 518 | shouldPrintComma = true 519 | end 520 | 521 | return text 522 | end 523 | 524 | -- Get a string with a table's contents where every value is on its own row 525 | -- 526 | -- Based on http://stackoverflow.com/a/13398936 527 | function tableHelper.getPrintableTable(inputTable, maxDepth, indentStr, indentLevel) 528 | 529 | if type(inputTable) ~= "table" then 530 | return type(inputTable) 531 | end 532 | 533 | local str = "" 534 | local currentIndent = "" 535 | 536 | if indentLevel == nil then indentLevel = 0 end 537 | if indentStr == nil then indentStr = "\t" end 538 | if maxDepth == nil then maxDepth = 50 end 539 | 540 | for i = 0, indentLevel do 541 | currentIndent = currentIndent .. indentStr 542 | end 543 | 544 | for index, value in pairs(inputTable) do 545 | 546 | if type(value) == "table" and maxDepth > 0 then 547 | value = "\n" .. tableHelper.getPrintableTable(value, maxDepth - 1, indentStr, indentLevel + 1) 548 | else 549 | value = tostring(value) .. "\n" 550 | end 551 | 552 | str = str .. currentIndent .. index .. ": " .. value 553 | end 554 | 555 | return str 556 | end 557 | 558 | function tableHelper.print(inputTable, maxDepth, indentStr, indentLevel) 559 | local text = tableHelper.getPrintableTable(inputTable, maxDepth, indentStr, indentLevel) 560 | tes3mp.LogMessage(2, text) 561 | end 562 | 563 | return tableHelper 564 | -------------------------------------------------------------------------------- /lib/lua/time.lua: -------------------------------------------------------------------------------- 1 | time = {} 2 | time.seconds = function(sec) 3 | return sec * 1000 4 | end 5 | 6 | time.minutes = function(min) 7 | return min * 60000 8 | end 9 | 10 | time.hours = function(hours) 11 | return hours * 3600000 12 | end 13 | 14 | time.days = function(day) 15 | return day * 86400000 16 | end 17 | 18 | time.toSeconds = function(msec) 19 | return msec / 1000 20 | end 21 | 22 | time.toMinutes = function(msec) 23 | return msec / 60000 24 | end 25 | 26 | time.toHours = function(msec) 27 | return msec / 3600000 28 | end 29 | 30 | time.toDays = function(msec) 31 | return msec / 86400000 32 | end 33 | 34 | return time 35 | -------------------------------------------------------------------------------- /lib/lua/utils.lua: -------------------------------------------------------------------------------- 1 | function string:split(sep) 2 | local sep, fields = sep or ":", {} 3 | local pattern = string.format("([^%s]+)", sep) 4 | self:gsub(pattern, function(c) fields[#fields+1] = c end) 5 | return fields 6 | end 7 | 8 | function string:capitalizeFirstLetter() 9 | return (self:gsub("^%l", string.upper)) 10 | end 11 | 12 | -- Check for case-insensitive equality 13 | function string:ciEqual(otherString) 14 | 15 | if type(otherString) ~= "string" then return false end 16 | 17 | return self:lower() == otherString:lower() 18 | end 19 | 20 | function string:trim() 21 | return (self:gsub("^%s*(.-)%s*$", "%1")) 22 | end 23 | 24 | function prefixZeroes(inputString, desiredLength) 25 | 26 | local length = string.len(inputString) 27 | 28 | while length < desiredLength do 29 | inputString = "0" .. inputString 30 | length = length + 1 31 | end 32 | 33 | return inputString 34 | end 35 | 36 | -- Based on https://stackoverflow.com/a/34965917 37 | function prequire(...) 38 | local status, lib = pcall(require, ...) 39 | if status then return lib end 40 | 41 | return nil 42 | end 43 | 44 | function doesModuleExist(name) 45 | if package.loaded[name] then 46 | return true 47 | else 48 | for _, searcher in ipairs(package.searchers or package.loaders) do 49 | local loader = searcher(name) 50 | if type(loader) == "function" then 51 | package.preload[name] = loader 52 | return true 53 | end 54 | end 55 | return false 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /scripts/animHelper.lua: -------------------------------------------------------------------------------- 1 | tableHelper = require("tableHelper") 2 | 3 | local animHelper = {} 4 | 5 | local defaultAnimNames = { "hit1", "hit2", "hit3", "hit4", "hit5", "idle2", "idle3", "idle4", 6 | "idle5", "idle6", "idle7", "idle8", "idle9", "pickprobe" } 7 | 8 | local generalAnimAliases = { act_impatient = "idle6", check_missing_item = "idle9", examine_hand = "idle7", 9 | look_behind = "idle3", shift_feet = "idle2", scratch_neck = "idle4", touch_chin = "idle8", 10 | touch_shoulder = "idle5" } 11 | local femaleAnimAliases = { adjust_hair = "idle4", touch_hip = "idle5" } 12 | local beastAnimAliases = { act_confused = "idle9", look_around = "idle2", touch_hands = "idle6" } 13 | 14 | function animHelper.GetAnimation(pid, animAlias) 15 | 16 | -- Is this animation included in the default animation names? 17 | if tableHelper.containsValue(defaultAnimNames, animAlias) then 18 | return animAlias 19 | else 20 | local race = string.lower(Players[pid].data.character.race) 21 | local gender = Players[pid].data.character.gender 22 | 23 | local isBeast = false 24 | local isFemale = false 25 | 26 | if race == "khajiit" or race == "argonian" then 27 | isBeast = true 28 | elseif gender == 0 then 29 | isFemale = true 30 | end 31 | 32 | if generalAnimAliases[animAlias] ~= nil then 33 | -- Did we use a general alias for something named differently for beasts? 34 | if isBeast and tableHelper.containsValue(beastAnimAliases, generalAnimAliases[animAlias]) then 35 | return "invalid" 36 | -- Did we use a general alias for something named differently for females? 37 | elseif isFemale and tableHelper.containsValue(femaleAnimAliases, generalAnimAliases[animAlias]) then 38 | return "invalid" 39 | else 40 | return generalAnimAliases[animAlias] 41 | end 42 | elseif isBeast and beastAnimAliases[animAlias] ~= nil then 43 | return beastAnimAliases[animAlias] 44 | elseif isFemale and femaleAnimAliases[animAlias] ~= nil then 45 | return femaleAnimAliases[animAlias] 46 | end 47 | end 48 | 49 | return "invalid" 50 | end 51 | 52 | function animHelper.GetValidList(pid) 53 | 54 | local validList = {} 55 | 56 | local race = string.lower(Players[pid].data.character.race) 57 | local gender = Players[pid].data.character.gender 58 | 59 | local isBeast = false 60 | local isFemale = false 61 | 62 | if race == "khajiit" or race == "argonian" then 63 | isBeast = true 64 | elseif gender == 0 then 65 | isFemale = true 66 | end 67 | 68 | for generalAlias, defaultAnim in pairs(generalAnimAliases) do 69 | 70 | if (not isBeast and not isFemale) or 71 | (isBeast and not tableHelper.containsValue(beastAnimAliases, defaultAnim)) or 72 | (isFemale and not tableHelper.containsValue(femaleAnimAliases, defaultAnim)) then 73 | table.insert(validList, generalAlias) 74 | end 75 | end 76 | 77 | if isBeast then 78 | for beastAlias, defaultAnim in pairs(beastAnimAliases) do 79 | table.insert(validList, beastAlias) 80 | end 81 | end 82 | 83 | if isFemale then 84 | for femaleAlias, defaultAnim in pairs(femaleAnimAliases) do 85 | table.insert(validList, femaleAlias) 86 | end 87 | end 88 | 89 | return tableHelper.concatenateFromIndex(validList, 1, ", ") 90 | end 91 | 92 | function animHelper.PlayAnimation(pid, animAlias) 93 | 94 | local defaultAnim = animHelper.GetAnimation(pid, animAlias) 95 | 96 | if defaultAnim ~= "invalid" then 97 | tes3mp.PlayAnimation(pid, defaultAnim, 0, 1, false) 98 | return true 99 | end 100 | 101 | return false 102 | end 103 | 104 | return animHelper 105 | -------------------------------------------------------------------------------- /scripts/cell/json.lua: -------------------------------------------------------------------------------- 1 | require("config") 2 | fileHelper = require("fileHelper") 3 | tableHelper = require("tableHelper") 4 | local BaseCell = require("cell.base") 5 | 6 | local Cell = class("Cell", BaseCell) 7 | 8 | function Cell:__init(cellDescription) 9 | BaseCell.__init(self, cellDescription) 10 | 11 | -- Ensure filename is valid 12 | self.entryName = fileHelper.fixFilename(cellDescription) 13 | 14 | self.entryFile = tes3mp.GetCaseInsensitiveFilename(config.dataPath .. "/cell/", self.entryName .. ".json") 15 | 16 | if self.entryFile == "invalid" then 17 | self.hasEntry = false 18 | self.entryFile = self.entryName .. ".json" 19 | else 20 | self.hasEntry = true 21 | end 22 | end 23 | 24 | function Cell:CreateEntry() 25 | self.hasEntry = jsonInterface.save("cell/" .. self.entryFile, self.data) 26 | 27 | if self.hasEntry then 28 | tes3mp.LogMessage(enumerations.log.INFO, "Successfully created JSON file for cell " .. self.entryName) 29 | else 30 | local message = "Failed to create JSON file for " .. self.entryName 31 | tes3mp.SendMessage(self.pid, message, true) 32 | end 33 | end 34 | 35 | function Cell:SaveToDrive() 36 | if self.hasEntry then 37 | tableHelper.cleanNils(self.data.packets) 38 | jsonInterface.save("cell/" .. self.entryFile, self.data, config.cellKeyOrder) 39 | end 40 | end 41 | 42 | function Cell:QuicksaveToDrive() 43 | if self.hasEntry then 44 | jsonInterface.quicksave("cell/" .. self.entryFile, self.data) 45 | end 46 | end 47 | 48 | function Cell:LoadFromDrive() 49 | self.data = jsonInterface.load("cell/" .. self.entryFile) 50 | 51 | if self.data == nil then 52 | tes3mp.LogMessage(enumerations.log.ERROR, "cell/" .. self.entryFile .. " cannot be read!") 53 | tes3mp.StopServer(2) 54 | else 55 | -- JSON doesn't allow numerical keys, but we use them, so convert 56 | -- all string number keys into numerical keys 57 | tableHelper.fixNumericalKeys(self.data) 58 | end 59 | end 60 | 61 | -- Deprecated function with confusing name, kept around for backwards compatibility 62 | function Cell:Save() 63 | self:SaveToDrive() 64 | end 65 | 66 | function Cell:Load() 67 | self:LoadFromDrive() 68 | end 69 | 70 | return Cell 71 | -------------------------------------------------------------------------------- /scripts/cell/sql.lua: -------------------------------------------------------------------------------- 1 | Database = require("database") 2 | local BaseCell = require("cell.base") 3 | 4 | local Cell = class("Cell", BaseCell) 5 | 6 | function Cell:__init(cellDescription) 7 | BaseCell.__init(self, cellDescription) 8 | 9 | if self.hasEntry == nil then 10 | 11 | -- Not implemented yet 12 | end 13 | end 14 | 15 | function Cell:CreateEntry() 16 | -- Not implemented yet 17 | end 18 | 19 | function Cell:SaveToDrive() 20 | -- Not implemented yet 21 | end 22 | 23 | function Cell:LoadFromDrive() 24 | -- Not implemented yet 25 | end 26 | 27 | return Cell 28 | -------------------------------------------------------------------------------- /scripts/color.lua: -------------------------------------------------------------------------------- 1 | color = {} 2 | 3 | color.AliceBlue = "#F0F8FF" 4 | color.AntiqueWhite = "#FAEBD7" 5 | color.Aqua = "#00FFFF" 6 | color.Aquamarine = "#7FFFD4" 7 | color.Azure = "#F0FFFF" 8 | color.Beige = "#F5F5DC" 9 | color.Bisque = "#FFE4C4" 10 | color.Black = "#000000" 11 | color.BlanchedAlmond = "#FFEBCD" 12 | color.Blue = "#0000FF" 13 | color.BlueViolet = "#8A2BE2" 14 | color.Brown = "#A52A2A" 15 | color.BurlyWood = "#DEB887" 16 | color.CadetBlue = "#5F9EA0" 17 | color.Chartreuse = "#7FFF00" 18 | color.Chocolate = "#D2691E" 19 | color.Coral = "#FF7F50" 20 | color.CornflowerBlue = "#6495ED" 21 | color.Cornsilk = "#FFF8DC" 22 | color.Crimson = "#DC143C" 23 | color.Cyan = "#00FFFF" 24 | color.DarkBlue = "#00008B" 25 | color.DarkCyan = "#008B8B" 26 | color.DarkGoldenRod = "#B8860B" 27 | color.DarkGray = "#A9A9A9" 28 | color.DarkGrey = "#A9A9A9" 29 | color.DarkGreen = "#006400" 30 | color.DarkKhaki = "#BDB76B" 31 | color.DarkMagenta = "#8B008B" 32 | color.DarkOliveGreen = "#556B2F" 33 | color.DarkOrange = "#FF8C00" 34 | color.DarkOrchid = "#9932CC" 35 | color.DarkRed = "#8B0000" 36 | color.DarkSalmon = "#E9967A" 37 | color.DarkSeaGreen = "#8FBC8F" 38 | color.DarkSlateBlue = "#483D8B" 39 | color.DarkSlateGray = "#2F4F4F" 40 | color.DarkSlateGrey = "#2F4F4F" 41 | color.DarkTurquoise = "#00CED1" 42 | color.DarkViolet = "#9400D3" 43 | color.DeepPink = "#FF1493" 44 | color.DeepSkyBlue = "#00BFFF" 45 | color.DimGray = "#696969" 46 | color.DimGrey = "#696969" 47 | color.DodgerBlue = "#1E90FF" 48 | color.FireBrick = "#B22222" 49 | color.FloralWhite = "#FFFAF0" 50 | color.ForestGreen = "#228B22" 51 | color.Fuchsia = "#FF00FF" 52 | color.Gainsboro = "#DCDCDC" 53 | color.GhostWhite = "#F8F8FF" 54 | color.Gold = "#FFD700" 55 | color.GoldenRod = "#DAA520" 56 | color.Gray = "#808080" 57 | color.Grey = "#808080" 58 | color.Green = "#008000" 59 | color.GreenYellow = "#ADFF2F" 60 | color.HoneyDew = "#F0FFF0" 61 | color.HotPink = "#FF69B4" 62 | color.IndianRed = "#CD5C5C" 63 | color.Indigo = "#4B0082" 64 | color.Ivory = "#FFFFF0" 65 | color.Khaki = "#F0E68C" 66 | color.Lavender = "#E6E6FA" 67 | color.LavenderBlush = "#FFF0F5" 68 | color.LawnGreen = "#7CFC00" 69 | color.LemonChiffon = "#FFFACD" 70 | color.LightBlue = "#ADD8E6" 71 | color.LightCoral = "#F08080" 72 | color.LightCyan = "#E0FFFF" 73 | color.LightGoldenRodYellow = "#FAFAD2" 74 | color.LightGray = "#D3D3D3" 75 | color.LightGrey = "#D3D3D3" 76 | color.LightGreen = "#90EE90" 77 | color.LightPink = "#FFB6C1" 78 | color.LightSalmon = "#FFA07A" 79 | color.LightSeaGreen = "#20B2AA" 80 | color.LightSkyBlue = "#87CEFA" 81 | color.LightSlateGray = "#778899" 82 | color.LightSlateGrey = "#778899" 83 | color.LightSteelBlue = "#B0C4DE" 84 | color.LightYellow = "#FFFFE0" 85 | color.Lime = "#00FF00" 86 | color.LimeGreen = "#32CD32" 87 | color.Linen = "#FAF0E6" 88 | color.Magenta = "#FF00FF" 89 | color.Maroon = "#800000" 90 | color.MediumAquaMarine = "#66CDAA" 91 | color.MediumBlue = "#0000CD" 92 | color.MediumOrchid = "#BA55D3" 93 | color.MediumPurple = "#9370DB" 94 | color.MediumSeaGreen = "#3CB371" 95 | color.MediumSlateBlue = "#7B68EE" 96 | color.MediumSpringGreen = "#00FA9A" 97 | color.MediumTurquoise = "#48D1CC" 98 | color.MediumVioletRed = "#C71585" 99 | color.MidnightBlue = "#191970" 100 | color.MintCream = "#F5FFFA" 101 | color.MistyRose = "#FFE4E1" 102 | color.Moccasin = "#FFE4B5" 103 | color.NavajoWhite = "#FFDEAD" 104 | color.Navy = "#000080" 105 | color.OldLace = "#FDF5E6" 106 | color.Olive = "#808000" 107 | color.OliveDrab = "#6B8E23" 108 | color.Orange = "#FFA500" 109 | color.OrangeRed = "#FF4500" 110 | color.Orchid = "#DA70D6" 111 | color.PaleGoldenRod = "#EEE8AA" 112 | color.PaleGreen = "#98FB98" 113 | color.PaleTurquoise = "#AFEEEE" 114 | color.PaleVioletRed = "#DB7093" 115 | color.PapayaWhip = "#FFEFD5" 116 | color.PeachPuff = "#FFDAB9" 117 | color.Peru = "#CD853F" 118 | color.Pink = "#FFC0CB" 119 | color.Plum = "#DDA0DD" 120 | color.PowderBlue = "#B0E0E6" 121 | color.Purple = "#800080" 122 | color.RebeccaPurple = "#663399" 123 | color.Red = "#FF0000" 124 | color.RosyBrown = "#BC8F8F" 125 | color.RoyalBlue = "#4169E1" 126 | color.SaddleBrown = "#8B4513" 127 | color.Salmon = "#FA8072" 128 | color.SandyBrown = "#F4A460" 129 | color.SeaGreen = "#2E8B57" 130 | color.SeaShell = "#FFF5EE" 131 | color.Sienna = "#A0522D" 132 | color.Silver = "#C0C0C0" 133 | color.SkyBlue = "#87CEEB" 134 | color.SlateBlue = "#6A5ACD" 135 | color.SlateGray = "#708090" 136 | color.SlateGrey = "#708090" 137 | color.Snow = "#FFFAFA" 138 | color.SpringGreen = "#00FF7F" 139 | color.SteelBlue = "#4682B4" 140 | color.Tan = "#D2B48C" 141 | color.Teal = "#008080" 142 | color.Thistle = "#D8BFD8" 143 | color.Tomato = "#FF6347" 144 | color.Turquoise = "#40E0D0" 145 | color.Violet = "#EE82EE" 146 | color.Wheat = "#F5DEB3" 147 | color.White = "#FFFFFF" 148 | color.WhiteSmoke = "#F5F5F5" 149 | color.Yellow = "#FFFF00" 150 | color.YellowGreen = "#9ACD32" 151 | 152 | color.Default = color.White 153 | color.Error = color.Red 154 | color.Warning = color.GoldenRod 155 | color.GreenText = "#789922" 156 | 157 | return color 158 | -------------------------------------------------------------------------------- /scripts/contentFixer.lua: -------------------------------------------------------------------------------- 1 | tableHelper = require("tableHelper") 2 | require("utils") 3 | 4 | local contentFixer = {} 5 | 6 | local deadlyItems = { "keening", "sunder" } 7 | local fixesByCell = {} 8 | 9 | -- Delete the chargen boat and associated guards and objects 10 | fixesByCell["-1, -9"] = { disable = { 268178, 297457, 297459, 297460, 299125 }} 11 | fixesByCell["-2, -9"] = { disable = { 172848, 172850, 172852, 289104, 297461, 397559 }} 12 | fixesByCell["-2, -10"] = { disable = { 297463, 297464, 297465, 297466 }} 13 | 14 | -- Delete the census papers and unlock the doors 15 | fixesByCell["Seyda Neen, Census and Excise Office"] = { disable = { 172859 }, unlock = { 119513, 172860 }} 16 | 17 | function contentFixer.FixCell(pid, cellDescription) 18 | 19 | if fixesByCell[cellDescription] ~= nil then 20 | 21 | for action, refNumArray in pairs(fixesByCell[cellDescription]) do 22 | 23 | tes3mp.ClearObjectList() 24 | tes3mp.SetObjectListPid(pid) 25 | tes3mp.SetObjectListCell(cellDescription) 26 | 27 | for arrayIndex, refNum in ipairs(refNumArray) do 28 | tes3mp.SetObjectRefNum(refNum) 29 | tes3mp.SetObjectMpNum(0) 30 | tes3mp.SetObjectRefId("") 31 | if action == "disable" then tes3mp.SetObjectState(false) end 32 | if action == "unlock" then tes3mp.SetObjectLockLevel(0) end 33 | tes3mp.AddObject() 34 | end 35 | 36 | if action == "delete" then 37 | tes3mp.SendObjectDelete() 38 | elseif action == "disable" then 39 | tes3mp.SendObjectState() 40 | elseif action == "unlock" then 41 | tes3mp.SendObjectLock() 42 | end 43 | end 44 | end 45 | end 46 | 47 | -- Unequip items that damage the player when worn 48 | -- 49 | -- Note: Items with constant damage effects like Whitewalker and the Mantle of Woe 50 | -- are already unequipped by default in the TES3MP client, so this only needs 51 | -- to account for scripted items that are missed there 52 | -- 53 | function contentFixer.UnequipDeadlyItems(pid) 54 | 55 | local itemsFound = 0 56 | 57 | for arrayIndex, itemRefId in pairs(deadlyItems) do 58 | if tableHelper.containsKeyValue(Players[pid].data.equipment, "refId", itemRefId, true) then 59 | local itemSlot = tableHelper.getIndexByNestedKeyValue(Players[pid].data.equipment, "refId", itemRefId, true) 60 | Players[pid].data.equipment[itemSlot] = nil 61 | itemsFound = itemsFound + 1 62 | end 63 | end 64 | 65 | if itemsFound > 0 then 66 | Players[pid]:QuicksaveToDrive() 67 | Players[pid]:LoadEquipment() 68 | end 69 | end 70 | 71 | function contentFixer.AdjustSharedCorprusState(pid) 72 | 73 | local corprusId = "corprus" 74 | 75 | if WorldInstance.data.customVariables.corprusCured == true then 76 | if tableHelper.containsValue(Players[pid].data.spellbook, corprusId) == true then 77 | 78 | tableHelper.removeValue(Players[pid].data.spellbook, corprusId) 79 | tableHelper.cleanNils(Players[pid].data.spellbook) 80 | 81 | tes3mp.ClearSpellbookChanges(pid) 82 | tes3mp.SetSpellbookChangesAction(pid, enumerations.spellbook.REMOVE) 83 | tes3mp.AddSpell(pid, corprusId) 84 | tes3mp.SendSpellbookChanges(pid) 85 | 86 | tes3mp.ClearSpellbookChanges(pid) 87 | tes3mp.SetSpellbookChangesAction(pid, enumerations.spellbook.ADD) 88 | for _, spellId in ipairs({"common disease immunity", "blight disease immunity","corprus immunity"}) do 89 | table.insert(Players[pid].data.spellbook, spellId) 90 | tes3mp.AddSpell(pid, spellId) 91 | end 92 | tes3mp.SendSpellbookChanges(pid) 93 | tes3mp.MessageBox(pid, -1, "You have been cured of corprus.") 94 | end 95 | elseif WorldInstance.data.customVariables.corprusGained == true then 96 | if tableHelper.containsValue(Players[pid].data.spellbook, corprusId) == false then 97 | 98 | table.insert(Players[pid].data.spellbook, corprusId) 99 | 100 | tes3mp.ClearSpellbookChanges(pid) 101 | tes3mp.SetSpellbookChangesAction(pid, enumerations.spellbook.ADD) 102 | tes3mp.AddSpell(pid, corprusId) 103 | tes3mp.SendSpellbookChanges(pid) 104 | tes3mp.MessageBox(pid, -1, "You have been afflicted with corprus.") 105 | end 106 | end 107 | end 108 | 109 | function contentFixer.AdjustWorldCorprusVariables(journal) 110 | 111 | local madeAdjustment = false 112 | 113 | for _, journalItem in ipairs(journal) do 114 | 115 | if journalItem.quest == "a2_3_corpruscure" and journalItem.index >= 50 then 116 | WorldInstance.data.customVariables.corprusCured = true 117 | madeAdjustment = true 118 | elseif journalItem.quest == "a2_2_6thhouse" and journalItem.index >= 50 then 119 | WorldInstance.data.customVariables.corprusGained = true 120 | madeAdjustment = true 121 | end 122 | end 123 | 124 | return madeAdjustment 125 | end 126 | 127 | customEventHooks.registerHandler("OnPlayerJournal", function(eventStatus, pid, playerPacket) 128 | if config.shareJournal == true then 129 | local madeAdjustment = contentFixer.AdjustWorldCorprusVariables(playerPacket.journal) 130 | 131 | if madeAdjustment == true then 132 | for otherPid, otherPlayer in pairs(Players) do 133 | if otherPid ~= pid then 134 | contentFixer.AdjustSharedCorprusState(otherPid) 135 | end 136 | end 137 | end 138 | end 139 | end) 140 | 141 | customEventHooks.registerHandler("OnPlayerFinishLogin", function(eventStatus, pid) 142 | if config.shareJournal == true then 143 | contentFixer.AdjustSharedCorprusState(pid) 144 | end 145 | end) 146 | 147 | customEventHooks.registerHandler("OnWorldReload", function(eventStatus) 148 | if config.shareJournal == true then 149 | contentFixer.AdjustWorldCorprusVariables(WorldInstance.data.journal) 150 | end 151 | end) 152 | 153 | return contentFixer 154 | -------------------------------------------------------------------------------- /scripts/custom/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TES3MP/CoreScripts/d7d71d635cd0aa10dfa6b089a999e4113cd516c3/scripts/custom/.gitkeep -------------------------------------------------------------------------------- /scripts/customCommandHooks.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Example usage: 3 | 4 | customCommandHooks.registerCommand("test", function(pid, cmd) 5 | tes3mp.SendMessage(pid, "You can execute a normal command!\n", false) 6 | end) 7 | 8 | 9 | customCommandHooks.registerCommand("ranktest", function(pid, cmd) 10 | tes3mp.SendMessage(pid, "You can execute a rank-checked command!\n", false) 11 | end) 12 | customCommandHooks.setRankRequirement("ranktest", 2) -- must be at least rank 2 13 | 14 | 15 | customCommandHooks.registerCommand("nametest", function(pid, cmd) 16 | tes3mp.SendMessage(pid, "You can execute a name-checked command!\n", false) 17 | end) 18 | customCommandHooks.setNameRequirement("nametest", {"Admin", "Kneg", "Jiub"}) -- must be one of these names 19 | 20 | ]] 21 | 22 | 23 | local customCommandHooks = {} 24 | 25 | local specialCharacter = "/" 26 | 27 | customCommandHooks.commands = {} 28 | customCommandHooks.rankRequirement = {} 29 | customCommandHooks.nameRequirement = {} 30 | 31 | function customCommandHooks.registerCommand(cmd, callback) 32 | customCommandHooks.commands[cmd] = callback 33 | end 34 | 35 | function customCommandHooks.removeCommand(cmd) 36 | customCommandHooks.commands[cmd] = nil 37 | customCommandHooks.rankRequirement[cmd] = nil 38 | customCommandHooks.nameRequirement[cmd] = nil 39 | end 40 | 41 | function customCommandHooks.getCallback(cmd) 42 | return customCommandHooks.commands[cmd] 43 | end 44 | 45 | function customCommandHooks.setRankRequirement(cmd, rank) 46 | if customCommandHooks.commands[cmd] ~= nil then 47 | customCommandHooks.rankRequirement[cmd] = rank 48 | end 49 | end 50 | 51 | function customCommandHooks.removeRankRequirement(cmd) 52 | customCommandHooks.rankRequirement[cmd] = nil 53 | end 54 | 55 | function customCommandHooks.setNameRequirement(cmd, names) 56 | if customCommandHooks.commands[cmd] ~= nil then 57 | customCommandHooks.nameRequirement[cmd] = names 58 | end 59 | end 60 | 61 | function customCommandHooks.addNameRequirement(cmd, name) 62 | if customCommandHooks.commands[cmd] ~= nil then 63 | if customCommandHooks.nameRequirement[cmd] == nil then 64 | customCommandHooks.nameRequirement[cmd] = {} 65 | end 66 | table.insert(customCommandHooks.nameRequirement[cmd], name) 67 | end 68 | end 69 | 70 | function customCommandHooks.removeNameRequirement(cmd) 71 | customCommandHooks.nameRequirement[cmd] = nil 72 | end 73 | 74 | function customCommandHooks.validator(eventStatus, pid, message) 75 | if message:sub(1,1) == specialCharacter then 76 | local cmd = (message:sub(2, #message)):split(" ") 77 | local callback = customCommandHooks.getCallback(cmd[1]) 78 | if callback ~= nil then 79 | if customCommandHooks.nameRequirement[cmd[1]] ~= nil then 80 | if tableHelper.containsValue(customCommandHooks.nameRequirement[cmd[1]], Players[pid].accountName) then 81 | callback(pid, cmd) 82 | return customEventHooks.makeEventStatus(false, nil) 83 | end 84 | elseif customCommandHooks.rankRequirement[cmd[1]] ~= nil then 85 | if Players[pid].data.settings.staffRank >= customCommandHooks.rankRequirement[cmd[1]] then 86 | callback(pid, cmd) 87 | return customEventHooks.makeEventStatus(false, nil) 88 | end 89 | else 90 | callback(pid, cmd) 91 | return customEventHooks.makeEventStatus(false, nil) 92 | end 93 | end 94 | end 95 | end 96 | 97 | customEventHooks.registerValidator("OnPlayerSendMessage", customCommandHooks.validator) 98 | 99 | return customCommandHooks 100 | -------------------------------------------------------------------------------- /scripts/customEventHooks.lua: -------------------------------------------------------------------------------- 1 | local customEventHooks = {} 2 | 3 | customEventHooks.validators = {} 4 | customEventHooks.handlers = {} 5 | 6 | function customEventHooks.makeEventStatus(validDefaultHandler, validCustomHandlers) 7 | return { 8 | validDefaultHandler = validDefaultHandler, 9 | validCustomHandlers = validCustomHandlers 10 | } 11 | end 12 | 13 | function customEventHooks.updateEventStatus(oldStatus, newStatus) 14 | if newStatus == nil then 15 | return oldStatus 16 | end 17 | local result = {} 18 | if newStatus.validDefaultHandler ~= nil then 19 | result.validDefaultHandler = newStatus.validDefaultHandler 20 | else 21 | result.validDefaultHandler = oldStatus.validDefaultHandler 22 | end 23 | 24 | if newStatus.validCustomHandlers ~= nil then 25 | result.validCustomHandlers = newStatus.validCustomHandlers 26 | else 27 | result.validCustomHandlers = oldStatus.validCustomHandlers 28 | end 29 | 30 | return result 31 | end 32 | 33 | function customEventHooks.registerValidator(event, callback) 34 | if customEventHooks.validators[event] == nil then 35 | customEventHooks.validators[event] = {} 36 | end 37 | table.insert(customEventHooks.validators[event], callback) 38 | end 39 | 40 | function customEventHooks.registerHandler(event, callback) 41 | if customEventHooks.handlers[event] == nil then 42 | customEventHooks.handlers[event] = {} 43 | end 44 | table.insert(customEventHooks.handlers[event], callback) 45 | end 46 | 47 | function customEventHooks.triggerValidators(event, args) 48 | local eventStatus = customEventHooks.makeEventStatus(true, true) 49 | if customEventHooks.validators[event] ~= nil then 50 | for _, callback in ipairs(customEventHooks.validators[event]) do 51 | eventStatus = customEventHooks.updateEventStatus(eventStatus, callback(eventStatus, unpack(args))) 52 | end 53 | end 54 | return eventStatus 55 | end 56 | 57 | function customEventHooks.triggerHandlers(event, eventStatus, args) 58 | if customEventHooks.handlers[event] ~= nil then 59 | for _, callback in ipairs(customEventHooks.handlers[event]) do 60 | eventStatus = customEventHooks.updateEventStatus(eventStatus, callback(eventStatus, unpack(args))) 61 | end 62 | end 63 | end 64 | 65 | return customEventHooks 66 | -------------------------------------------------------------------------------- /scripts/customScripts.lua: -------------------------------------------------------------------------------- 1 | -- Load up your custom scripts here! Ideally, your custom scripts will be placed in the scripts/custom folder and then get loaded like this: 2 | -- 3 | -- require("custom/yourScript") 4 | -- 5 | -- Refer to the Tutorial.md file for information on how to use various event and command hooks in your scripts. 6 | 7 | 8 | -------------------------------------------------------------------------------- /scripts/dataTableBuilder.lua: -------------------------------------------------------------------------------- 1 | dataTableBuilder = {} 2 | 3 | dataTableBuilder.BuildAIData = function(targetPid, targetUniqueIndex, action, 4 | posX, posY, posZ, distance, duration, shouldRepeat) 5 | 6 | local ai = {} 7 | ai.action = action 8 | ai.posX, ai.posY, ai.posZ = posX, posY, posZ 9 | ai.distance = distance 10 | ai.duration = duration 11 | ai.shouldRepeat = shouldRepeat 12 | 13 | if targetPid ~= nil then 14 | ai.targetPlayer = Players[targetPid].accountName 15 | else 16 | ai.targetUniqueIndex = targetUniqueIndex 17 | end 18 | 19 | return ai 20 | end 21 | 22 | -- Use with logicHandler.CreateObject() functions 23 | dataTableBuilder.BuildObjectData = function(refId, count, charge, enchantmentCharge, soul) 24 | 25 | local objectData = {} 26 | objectData.refId = refId 27 | objectData.count = count or 1 28 | objectData.charge = charge or -1 29 | objectData.enchantmentCharge = enchantmentCharge or -1 30 | objectData.soul = soul or "" 31 | 32 | return objectData 33 | end 34 | 35 | return dataTableBuilder 36 | -------------------------------------------------------------------------------- /scripts/database.lua: -------------------------------------------------------------------------------- 1 | -- This is a very unfinished and out-of-date example of using a database in TES3MP 2 | 3 | tableHelper = require("tableHelper") 4 | Database = class("Database") 5 | 6 | function Database:LoadDriver(driver) 7 | 8 | self.driver = require("luasql." .. driver) 9 | 10 | -- Create environment object 11 | self.env = nil 12 | 13 | if driver == "sqlite3" then 14 | self.env = self.driver.sqlite3() 15 | end 16 | end 17 | 18 | function Database:Connect(databasePath) 19 | 20 | self.connection = assert(self.env:connect(databasePath)) 21 | end 22 | 23 | function Database:Execute(query) 24 | 25 | local response = self.connection:execute(query) 26 | 27 | if response == nil then 28 | tes3mp.LogMessage(enumerations.log.ERROR, "Could not execute query: " .. query) 29 | end 30 | 31 | return response 32 | end 33 | 34 | function Database:Escape(string) 35 | 36 | string = self.connection:escape(string) 37 | return string 38 | end 39 | 40 | --- Create a table if it does not already exist 41 | --@param tableName The name of the table. [string] 42 | --@param columnArray An array (to keep column ordering) of key/value pairs. [table] 43 | function Database:CreateTable(tableName, columnArray) 44 | 45 | local query = string.format("CREATE TABLE IF NOT EXISTS %s(", tableName) 46 | 47 | for index, column in pairs(columnArray) do 48 | for name, definition in pairs(column) do 49 | if index > 1 then 50 | query = query .. ", " 51 | end 52 | 53 | -- If this is a constraint, only add its definition to the query 54 | if name == "constraint" then 55 | query = query .. definition 56 | else 57 | query = query .. string.format("%s %s", name, definition) 58 | end 59 | end 60 | end 61 | 62 | query = query .. ")" 63 | self:Execute(query) 64 | end 65 | 66 | function Database:DeleteRows(tableName, condition) 67 | 68 | local query = string.format("DELETE FROM %s %s", tableName, condition) 69 | self:Execute(query) 70 | end 71 | 72 | --- Insert a row into a table 73 | --@param tableName The name of the table. [string] 74 | --@param valueTable A key/value table where the keys are the names of columns. [table] 75 | function Database:InsertRow(tableName, valueTable) 76 | 77 | local query = string.format("INSERT OR REPLACE INTO %s", tableName) 78 | local queryColumns = "" 79 | local queryValues = "" 80 | local count = 0 81 | 82 | for column, value in pairs(valueTable) do 83 | 84 | count = count + 1 85 | 86 | if count > 1 then 87 | queryColumns = queryColumns .. ", " 88 | queryValues = queryValues .. ", " 89 | end 90 | 91 | queryColumns = queryColumns .. tostring(column) 92 | queryValues = queryValues .. '\'' .. self:Escape(tostring(value)) .. '\'' 93 | end 94 | 95 | if count > 0 then 96 | 97 | query = query .. string.format("(%s) VALUES(%s)", queryColumns, queryValues) 98 | self:Execute(query) 99 | end 100 | end 101 | 102 | function Database:SelectRow(tableName, condition) 103 | 104 | local rows = self:SelectRows(tableName, condition) 105 | return rows[1] 106 | end 107 | 108 | function Database:SelectRows(tableName, condition) 109 | 110 | local query = string.format("SELECT * FROM %s %s", tableName, condition) 111 | local cursor = self:Execute(query) 112 | local rows = {} 113 | local currentRow = cursor:fetch({}, "a") 114 | 115 | while currentRow do 116 | table.insert(rows, currentRow) 117 | currentRow = cursor:fetch({}, "a") 118 | end 119 | 120 | return rows 121 | end 122 | 123 | function Database:GetSingleValue(tableName, column, condition) 124 | 125 | local query = string.format("SELECT %s FROM %s %s", column, tableName, condition) 126 | local cursor = self:Execute(query) 127 | local row = cursor:fetch({}, "a") 128 | 129 | if row == nil or row[column] == nil then 130 | return nil 131 | else 132 | return row[column] 133 | end 134 | end 135 | 136 | function Database:SavePlayer(dbPid, data) 137 | 138 | -- Put all of these INSERT statements into a single transaction to avoid freezes 139 | self:Execute("BEGIN TRANSACTION;") 140 | 141 | for category, categoryTable in pairs(data) do 142 | if tableHelper.usesNumericalKeys(categoryTable) then 143 | 144 | local tableName = "player_slots_" .. category 145 | 146 | -- Delete the current slots before repopulating them 147 | self:DeleteRows(tableName, string.format("WHERE dbPid = '%s'", dbPid)) 148 | 149 | for slot, slotObject in pairs(categoryTable) do 150 | local tempTable = tableHelper.deepCopy(slotObject) 151 | tempTable.dbPid = dbPid 152 | tempTable.slot = slot 153 | self:InsertRow(tableName, tempTable) 154 | end 155 | elseif category ~= "login" then 156 | 157 | local tableName = "player_" .. category 158 | tes3mp.LogMessage(enumerations.log.INFO, "Saving category " .. category) 159 | local tempTable = tableHelper.deepCopy(categoryTable) 160 | tempTable.dbPid = dbPid 161 | self:InsertRow(tableName, tempTable) 162 | end 163 | end 164 | 165 | self:Execute("END TRANSACTION;") 166 | end 167 | 168 | function Database:LoadPlayer(dbPid, data) 169 | 170 | local slotTables = { "equipment", "inventory", "spellbook" } 171 | 172 | for category, categoryTable in pairs(data) do 173 | 174 | if tableHelper.containsValue(slotTables, category) then 175 | local tableName = "player_slots_" .. category 176 | 177 | local rows = self:SelectRows(tableName, string.format("WHERE dbPid = '%s'", dbPid)) 178 | 179 | for index, row in pairs(rows) do 180 | local slot = row.slot 181 | 182 | -- Remove database-only columns in case we want to save the data again to a different format 183 | row.dbPid = nil 184 | row.slot = nil 185 | 186 | data[category][slot] = row 187 | end 188 | else 189 | local tableName = "player_" .. category 190 | local row = self:SelectRow(tableName, string.format("WHERE dbPid = '%s'", dbPid)) 191 | 192 | if row ~= nil then 193 | 194 | -- Remove database-only indexes in case we want to save the data again to a different format 195 | row.dbPid = nil 196 | 197 | data[category] = row 198 | end 199 | end 200 | end 201 | 202 | return data 203 | end 204 | 205 | function Database:SaveWorld(data) 206 | 207 | for category, categoryTable in pairs(data) do 208 | 209 | local tableName = "world_" .. category 210 | local tempTable = tableHelper.deepCopy(categoryTable) 211 | tempTable.rowid = 0 212 | self:InsertRow(tableName, tempTable) 213 | end 214 | end 215 | 216 | function Database:LoadWorld(data) 217 | 218 | for category, categoryTable in pairs(data) do 219 | 220 | local tableName = "world_" .. category 221 | local row = self:SelectRow(tableName, "WHERE rowid = 0") 222 | 223 | if row ~= nil then 224 | data[category] = row 225 | end 226 | end 227 | 228 | return data 229 | end 230 | 231 | function Database:CreateCellTables() 232 | 233 | local columnList, valueTable 234 | 235 | -- Frequently reused rows related to database cell IDs 236 | local dbCidRow = {dbCid = "INTEGER PRIMARY KEY ASC"} 237 | local constraintRow = {constraint = "FOREIGN KEY(dbCid) REFERENCES cell_entry(dbCid)" } 238 | 239 | columnList = { 240 | dbCidRow, 241 | {description = "TEXT UNIQUE"} 242 | } 243 | 244 | self:CreateTable("cell_entry", columnList) 245 | end 246 | 247 | function Database:CreateWorldTables() 248 | 249 | local columnList 250 | 251 | columnList = { 252 | {currentMpNum = "INTEGER"} 253 | } 254 | 255 | self:CreateTable("world_general", columnList) 256 | end 257 | 258 | function Database:CreatePlayerTables() 259 | 260 | local columnList, valueTable 261 | 262 | -- Frequently reused rows related to database player IDs 263 | local dbPidRow = {dbPid = "INTEGER PRIMARY KEY ASC"} 264 | local constraintRow = {constraint = "FOREIGN KEY(dbPid) REFERENCES player_login(dbPid)" } 265 | 266 | columnList = { 267 | dbPidRow, 268 | {name = "TEXT UNIQUE"}, 269 | {password = "TEXT"} 270 | } 271 | 272 | self:CreateTable("player_login", columnList) 273 | 274 | columnList = { 275 | dbPidRow, 276 | {admin = "INTEGER"}, 277 | {consoleAllowed = "TEXT NOT NULL CHECK (consoleAllowed IN ('true', 'false', 'default'))"}, 278 | constraintRow 279 | } 280 | 281 | self:CreateTable("player_settings", columnList) 282 | 283 | columnList = { 284 | dbPidRow, 285 | {race = "TEXT"}, 286 | {head = "TEXT"}, 287 | {hair = "TEXT"}, 288 | {gender = "BOOLEAN NOT NULL CHECK (gender IN (0, 1))"}, 289 | {class = "TEXT"}, 290 | {birthsign = "TEXT"}, 291 | constraintRow 292 | } 293 | 294 | self:CreateTable("player_character", columnList) 295 | 296 | columnList = { 297 | dbPidRow, 298 | {name = "TEXT"}, 299 | {description = "TEXT"}, 300 | {specialization = "INTEGER"}, 301 | {majorAttributes = "TEXT"}, 302 | {majorSkills = "TEXT"}, 303 | {minorSkills = "TEXT"}, 304 | constraintRow 305 | } 306 | 307 | self:CreateTable("player_customClass", columnList) 308 | 309 | columnList = { 310 | dbPidRow, 311 | {cell = "TEXT"}, 312 | {posX = "NUMERIC"}, 313 | {posY = "NUMERIC"}, 314 | {posZ = "NUMERIC"}, 315 | {rotX = "NUMERIC"}, 316 | {rotY = "NUMERIC"}, 317 | {rotZ = "NUMERIC"}, 318 | constraintRow 319 | } 320 | 321 | self:CreateTable("player_location", columnList) 322 | 323 | columnList = { 324 | dbPidRow, 325 | {level = "INTEGER"}, 326 | {levelProgress = "INTEGER"}, 327 | {healthBase = "NUMERIC"}, 328 | {healthCurrent = "NUMERIC"}, 329 | {magickaBase = "NUMERIC"}, 330 | {magickaCurrent = "NUMERIC"}, 331 | {fatigueBase = "NUMERIC"}, 332 | {fatigueCurrent = "NUMERIC"}, 333 | constraintRow 334 | } 335 | 336 | self:CreateTable("player_stats", columnList) 337 | 338 | columnList = { dbPidRow } 339 | 340 | for i = 0, (tes3mp.GetAttributeCount() - 1) do 341 | local attributePair = {} 342 | attributePair[tes3mp.GetAttributeName(i)] = "INTEGER" 343 | table.insert(columnList, attributePair) 344 | end 345 | 346 | table.insert(columnList, constraintRow) 347 | self:CreateTable("player_attributes", columnList) 348 | self:CreateTable("player_attributeSkillIncreases", columnList) 349 | 350 | columnList = { dbPidRow } 351 | 352 | for i = 0, (tes3mp.GetSkillCount() - 1) do 353 | local skillPair = {} 354 | skillPair[tes3mp.GetSkillName(i)] = "INTEGER" 355 | table.insert(columnList, skillPair) 356 | end 357 | 358 | table.insert(columnList, constraintRow) 359 | self:CreateTable("player_skills", columnList) 360 | 361 | -- Turn INTEGER into NUMERIC for skillProgress 362 | tableHelper.replaceValue(columnList, "INTEGER", "NUMERIC") 363 | 364 | self:CreateTable("player_skillProgress", columnList) 365 | 366 | columnList = { 367 | {dbPid = "INTEGER"}, 368 | {slot = "INTEGER"}, 369 | {refId = "TEXT"}, 370 | {count = "INTEGER"}, 371 | {charge = "INTEGER"}, 372 | constraintRow 373 | } 374 | 375 | self:CreateTable("player_slots_equipment", columnList) 376 | self:CreateTable("player_slots_inventory", columnList) 377 | 378 | columnList = { 379 | {dbPid = "INTEGER"}, 380 | {slot = "INTEGER"}, 381 | {spellId = "TEXT"}, 382 | constraintRow 383 | } 384 | 385 | self:CreateTable("player_slots_spellbook", columnList) 386 | end 387 | 388 | return Database 389 | -------------------------------------------------------------------------------- /scripts/enumerations.lua: -------------------------------------------------------------------------------- 1 | -- This file is used to track and simplify dealing with all enumerations 2 | -- currently implemented in packets 3 | 4 | enumerations = {} 5 | enumerations.ai = { CANCEL = 0, ACTIVATE = 1, COMBAT = 2, ESCORT = 3, FOLLOW = 4, TRAVEL = 5, WANDER = 6 } 6 | enumerations.aiPrintableAction = { CANCEL = "cancelling current AI", ACTIVATE = "activating", 7 | COMBAT = "initiating combat with", ESCORT = "escorting", FOLLOW = "following", TRAVEL = "travelling to", 8 | WANDER = "wandering" } 9 | enumerations.container = { SET = 0, ADD = 1, REMOVE = 2, REQUEST = 3 } 10 | enumerations.containerSub = { NONE = 0, DRAG = 1, DROP = 2, TAKE_ALL = 3, REPLY_TO_REQUEST = 4, RESTOCK_RESULT = 5 } 11 | enumerations.dialogueChoice = { TOPIC = 0, PERSUASION = 1, COMPANION_SHARE = 2, BARTER = 3, SPELLS = 4, TRAVEL = 5, 12 | SPELLMAKING = 6, ENCHANTING = 7, TRAINING = 8, REPAIR = 9 } 13 | enumerations.doorstate = { OPEN = 1, CLOSED = 2 } 14 | enumerations.drawstate = { NONE = 0, WEAPON = 1, SPELL = 2 } 15 | enumerations.effects = { WATER_BREATHING = 0, SWIFT_SWIM = 1, WATER_WALKING = 2, SHIELD = 3, FIRE_SHIELD = 4, 16 | LIGHTNING_SHIELD = 5, FROST_SHIELD = 6, BURDEN = 7, FEATHER = 8, JUMP = 9, LEVITATE = 10, SLOW_FALL = 11, LOCK = 12, 17 | OPEN = 13, FIRE_DAMAGE = 14, SHOCK_DAMAGE = 15, FROST_DAMAGE = 16, DRAIN_ATTRIBUTE = 17, DRAIN_HEALTH = 18, 18 | DRAIN_MAGICKA = 19, DRAIN_FATIGUE = 20, DRAIN_SKILL = 21, DAMAGE_ATTRIBUTE = 22, DAMAGE_HEALTH = 23, 19 | DAMAGE_MAGICKA = 24, DAMAGE_FATIGUE = 25, DAMAGE_SKILL = 26, POISON = 27, WEAKNESS_FIRE = 28, WEAKNESS_FROST = 29, 20 | WEAKNESS_SHOCK = 30, WEAKNESS_MAGICKA = 31, WEAKNESS_COMMON_DISEASE = 32, WEAKNESS_BLIGHT_DISEASE = 33, 21 | WEAKNESS_CORPRUS_DISEASE = 34, WEAKNESS_POISON = 35, WEAKNESS_NORMAL_WEAPONS = 36, DISINTEGRATE_WEAPON = 37, 22 | DISINTEGRATE_ARMOR = 38, INVISIBILITY = 39, CHAMELEON = 40, LIGHT = 41, SANCTUARY = 42, NIGHTEYE = 43, CHARM = 44, 23 | PARALYZE = 45, SILENCE = 46, BLIND = 47, SOUND = 48, CALM_HUMANOID = 49, CALM_CREATURE = 50, FRENZY_HUMANOID = 51, 24 | FRENZY_CREATURE = 52, DEMORALIZE_HUMANOID = 53, DEMORALIZE_CREATURE = 54, RALLY_HUMANOID = 55, RALLY_CREATURE = 56, 25 | DISPEL = 57, SOULTRAP = 58, TELEKINESIS = 59, MARK = 60, RECALL = 61, DIVINE_INTERVENTION = 62, 26 | ALMSIVI_INTERVENTION = 63, DETECT_ANIMAL = 64, DETECT_ENCHANTMENT = 65, DETECT_KEY = 66, SPELL_ABSORPTION = 67, 27 | REFLECT = 68, CURE_COMMON_DISEASE = 69, CURE_BLIGHT_DISEASE = 70, CURE_CORPRUS_DISEASE = 71, CURE_POISON = 72, 28 | CURE_PARALYZATION = 73, RESTORE_ATTRIBUTE = 74, RESTORE_HEALTH = 75, RESTORE_MAGICKA = 76, RESTORE_FATIGUE = 77, 29 | RESTORE_SKILL = 78, FORTIFY_ATTRIBUTE = 79, FORTIFY_HEALTH = 80, FORTIFY_MAGICKA = 81, FORTIFY_FATIGUE = 82, 30 | FORTIFY_SKILL = 83, FORTIFY_MAXIMUM_MAGICKA = 84, ABSORB_ATTRIBUTE = 85, ABSORB_HEALTH = 86, ABSORB_MAGICKA = 87, 31 | ABSORB_FATIGUE = 88, ABSORB_SKILL = 89, RESIST_FIRE = 90, RESIST_FROST = 91, RESIST_SHOCK = 92, RESIST_MAGICKA = 93, 32 | RESIST_COMMON_DISEASE = 94, RESIST_BLIGHT_DISEASE = 95, RESIST_CORPRUS_DISEASE = 96, RESIST_POISON = 97, 33 | RESIST_NORMAL_WEAPONS = 98, RESIST_PARALYSIS = 99, REMOVE_CURSE = 100, TURN_UNDEAD = 101, SUMMON_SCAMP = 102, 34 | SUMMON_CLANNFEAR = 103, SUMMON_DAEDROTH = 104, SUMMON_DREMORA = 105, SUMMON_ANCESTRAL_GHOST = 106, 35 | SUMMON_SKELETAL_MINION = 107, SUMMON_BONEWALKER = 108, SUMMON_GREATER_BONEWALKER = 109, SUMMON_BONELORD = 110, 36 | SUMMON_WINGED_TWILIGHT = 111, SUMMON_HUNGER = 112, SUMMON_GOLDEN_SAINT = 113, SUMMON_FLAME_ATRONACH = 114, 37 | SUMMON_FROST_ATRONACH = 115, SUMMON_STORM_ATRONACH = 116, FORTIFY_ATTACK = 117, COMMAND_CREATURE = 118, 38 | COMMAND_HUMANOID = 119, BOUND_DAGGER = 120, BOUND_LONGSWORD = 121, BOUND_MACE = 122, BOUND_BATTLE_AXE = 123, 39 | BOUND_SPEAR = 124, BOUND_LONGBOW = 125, EXTRASPELL = 126, BOUND_CUIRASS = 127, BOUND_HELM = 128, BOUND_BOOTS = 129, 40 | BOUND_SHIELD = 130, BOUND_GLOVES = 131, CORPRUS = 132, VAMPIRISM = 133, SUMMON_CENTURION_SPHERE = 134, SUN_DAMAGE = 135, 41 | STUNTED_MAGICKA = 136, SUMMON_FABRICANT = 137, CALL_WOLF = 138, CALL_BEAR = 139, SUMMON_BONEWOLF = 140, 42 | S_EFFECT_SUMMON_CREATURE04 = 141, S_EFFECT_SUMMON_CREATURE05 = 142 } 43 | enumerations.equipment = { HELMET = 0, CUIRASS = 1, GREAVES = 2, LEFT_PAULDRON = 3, RIGHT_PAULDRON = 4, 44 | LEFT_GAUNTLET = 5, RIGHT_GAUNTLET = 6, BOOTS = 7, SHIRT = 8, PANTS = 9, SKIRT = 10, ROBE = 11, LEFT_RING = 12, 45 | RIGHT_RING = 13, AMULET = 14, BELT = 15, CARRIED_RIGHT = 16, CARRIED_LEFT = 17, AMMUNITION = 18 } 46 | enumerations.faction = { RANK = 0, EXPULSION = 1, REPUTATION = 2 } 47 | enumerations.inventory = { SET = 0, ADD = 1, REMOVE = 2 } 48 | enumerations.journal = { ENTRY = 0, INDEX = 1 } 49 | enumerations.log = { VERBOSE = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4 } 50 | enumerations.miscellaneous = { MARK_LOCATION = 0, SELECTED_SPELL = 1 } 51 | enumerations.objectCategories = { PLAYER = 0, ACTOR = 1, PLACED_OBJECT = 2 } 52 | enumerations.packetOrigin = { CLIENT_GAMEPLAY = 0, CLIENT_CONSOLE = 1, CLIENT_DIALOGUE = 2, 53 | CLIENT_SCRIPT_LOCAL = 3, CLIENT_SCRIPT_GLOBAL = 4, SERVER_SCRIPT = 5 } 54 | enumerations.recordType = { ACTIVATOR = 0, APPARATUS = 1, ARMOR = 2, BODYPART = 3, BOOK = 4, CELL = 5, CLOTHING = 6, 55 | CONTAINER = 7, CREATURE = 8, DOOR = 9, ENCHANTMENT = 10, GAMESETTING = 11, INGREDIENT = 12, LIGHT = 13, 56 | LOCKPICK = 14, MISCELLANEOUS = 15, NPC = 16, POTION = 17, PROBE = 18, REPAIR = 19, SCRIPT = 20, SOUND = 21, 57 | SPELL = 22, STATIC = 23, WEAPON = 24 } 58 | enumerations.resurrect = { REGULAR = 0, IMPERIAL_SHRINE = 1, TRIBUNAL_TEMPLE = 2 } 59 | enumerations.spellbook = { SET = 0, ADD = 1, REMOVE = 2 } 60 | enumerations.variableType = { SHORT = 0, LONG = 1, FLOAT = 2, INT = 3, STRING = 4 } 61 | enumerations.weather = { CLEAR = 0, CLOUDY = 1, FOGGY = 2, OVERCAST = 3, RAIN = 4, THUNDER = 5, ASH = 6, BLIGHT = 7, 62 | SNOW = 8, BLIZZARD = 9 } 63 | 64 | return enumerations 65 | -------------------------------------------------------------------------------- /scripts/guiHelper.lua: -------------------------------------------------------------------------------- 1 | tableHelper = require("tableHelper") 2 | 3 | guiHelper = {} 4 | guiHelper.names = {"LOGIN", "REGISTER", "PLAYERSLIST", "CELLSLIST"} 5 | guiHelper.ID = tableHelper.enum(guiHelper.names) 6 | 7 | guiHelper.ShowLogin = function(pid) 8 | tes3mp.PasswordDialog(pid, guiHelper.ID.LOGIN, "Enter your password:", "") 9 | end 10 | 11 | guiHelper.ShowRegister = function(pid) 12 | tes3mp.PasswordDialog(pid, guiHelper.ID.REGISTER, "Create new password:", 13 | "Warning: there is no guarantee that your password will be stored securely on any game server, so you should use " .. 14 | "a unique one for each server.") 15 | end 16 | 17 | local GetConnectedPlayerList = function() 18 | 19 | local lastPid = tes3mp.GetLastPlayerId() 20 | local list = "" 21 | local divider = "" 22 | 23 | for playerIndex = 0, lastPid do 24 | if playerIndex == lastPid then 25 | divider = "" 26 | else 27 | divider = "\n" 28 | end 29 | if Players[playerIndex] ~= nil and Players[playerIndex]:IsLoggedIn() then 30 | 31 | list = list .. tostring(Players[playerIndex].name) .. " (pid: " .. tostring(Players[playerIndex].pid) .. 32 | ", ping: " .. tostring(tes3mp.GetAvgPing(Players[playerIndex].pid)) .. ")" .. divider 33 | end 34 | end 35 | 36 | return list 37 | end 38 | 39 | local GetLoadedCellList = function() 40 | local list = "" 41 | local divider = "" 42 | 43 | local cellCount = logicHandler.GetLoadedCellCount() 44 | local cellIndex = 0 45 | 46 | for key, value in pairs(LoadedCells) do 47 | cellIndex = cellIndex + 1 48 | 49 | if cellIndex == cellCount then 50 | divider = "" 51 | else 52 | divider = "\n" 53 | end 54 | 55 | list = list .. key .. " (auth: " .. LoadedCells[key]:GetAuthority() .. ", loaded by " .. 56 | LoadedCells[key]:GetVisitorCount() .. ")" .. divider 57 | end 58 | 59 | return list 60 | end 61 | 62 | local GetLoadedRegionList = function() 63 | local list = "" 64 | local divider = "" 65 | 66 | local regionCount = logicHandler.GetLoadedRegionCount() 67 | local regionIndex = 0 68 | 69 | for key, value in pairs(WorldInstance.storedRegions) do 70 | local visitorCount = WorldInstance:GetRegionVisitorCount(key) 71 | 72 | if visitorCount > 0 then 73 | regionIndex = regionIndex + 1 74 | 75 | if regionIndex == regionCount then 76 | divider = "" 77 | else 78 | divider = "\n" 79 | end 80 | 81 | list = list .. key .. " (auth: " .. WorldInstance:GetRegionAuthority(key) .. ", loaded by " .. 82 | visitorCount .. ")" .. divider 83 | end 84 | end 85 | 86 | return list 87 | end 88 | 89 | local GetPlayerInventoryList = function(pid) 90 | 91 | local list = "" 92 | local divider = "" 93 | local lastItemIndex = tableHelper.getCount(Players[pid].data.inventory) 94 | 95 | for index, currentItem in ipairs(Players[pid].data.inventory) do 96 | 97 | if index == lastItemIndex then 98 | divider = "" 99 | else 100 | divider = "\n" 101 | end 102 | 103 | list = list .. index .. ": " .. currentItem.refId .. " (count: " .. currentItem.count .. ")" .. divider 104 | end 105 | 106 | return list 107 | end 108 | 109 | guiHelper.ShowPlayerList = function(pid) 110 | 111 | local playerCount = logicHandler.GetConnectedPlayerCount() 112 | local label = playerCount .. " connected player" 113 | 114 | if playerCount ~= 1 then 115 | label = label .. "s" 116 | end 117 | 118 | tes3mp.ListBox(pid, guiHelper.ID.PLAYERSLIST, label, GetConnectedPlayerList()) 119 | end 120 | 121 | guiHelper.ShowCellList = function(pid) 122 | 123 | local cellCount = logicHandler.GetLoadedCellCount() 124 | local label = cellCount .. " loaded cell" 125 | 126 | if cellCount ~= 1 then 127 | label = label .. "s" 128 | end 129 | 130 | tes3mp.ListBox(pid, guiHelper.ID.CELLSLIST, label, GetLoadedCellList()) 131 | end 132 | 133 | guiHelper.ShowRegionList = function(pid) 134 | 135 | local regionCount = logicHandler.GetLoadedRegionCount() 136 | local label = regionCount .. " loaded region" 137 | 138 | if regionCount ~= 1 then 139 | label = label .. "s" 140 | end 141 | 142 | tes3mp.ListBox(pid, guiHelper.ID.CELLSLIST, label, GetLoadedRegionList()) 143 | end 144 | 145 | guiHelper.ShowInventoryList = function(menuId, pid, inventoryPid) 146 | 147 | local inventoryCount = tableHelper.getCount(Players[pid].data.inventory) 148 | local label = inventoryCount .. " item" 149 | 150 | if inventoryCount ~= 1 then 151 | label = label .. "s" 152 | end 153 | 154 | tes3mp.ListBox(pid, menuId, label, GetPlayerInventoryList(inventoryPid)) 155 | end 156 | 157 | return guiHelper 158 | -------------------------------------------------------------------------------- /scripts/inventoryHelper.lua: -------------------------------------------------------------------------------- 1 | local inventoryHelper = {} 2 | 3 | function inventoryHelper.containsItem(inventory, refId, charge, enchantmentCharge, soul) 4 | 5 | if charge ~= nil then charge = math.floor(charge) end 6 | if enchantmentCharge ~= nil then enchantmentCharge = math.floor(enchantmentCharge) end 7 | 8 | for itemIndex, item in pairs(inventory) do 9 | if item.refId == refId then 10 | 11 | local isValid = true 12 | 13 | if soul ~= nil and item.soul ~= soul then 14 | isValid = false 15 | elseif charge ~= nil and item.charge ~= nil and math.floor(item.charge) ~= charge then 16 | isValid = false 17 | elseif enchantmentCharge ~= nil and item.enchantmentCharge ~= nil and 18 | math.floor(item.enchantmentCharge) ~= enchantmentCharge then 19 | isValid = false 20 | end 21 | 22 | if isValid then 23 | return true 24 | end 25 | end 26 | end 27 | 28 | return false 29 | end 30 | 31 | function inventoryHelper.getItemIndex(inventory, refId, charge, enchantmentCharge, soul) 32 | 33 | if charge ~= nil then charge = math.floor(charge) end 34 | if enchantmentCharge ~= nil then enchantmentCharge = math.floor(enchantmentCharge) end 35 | 36 | for itemIndex, item in pairs(inventory) do 37 | if item.refId == refId then 38 | 39 | local isValid = true 40 | 41 | if soul ~= nil and item.soul ~= soul then 42 | isValid = false 43 | elseif charge ~= nil and item.charge ~= nil and math.floor(item.charge) ~= charge then 44 | isValid = false 45 | elseif enchantmentCharge ~= nil and item.enchantmentCharge ~= nil and 46 | math.floor(item.enchantmentCharge) ~= enchantmentCharge then 47 | isValid = false 48 | end 49 | 50 | if isValid then 51 | return itemIndex 52 | end 53 | end 54 | end 55 | 56 | return nil 57 | end 58 | 59 | function inventoryHelper.getItemIndexes(inventory, refId) 60 | 61 | local indexes = {} 62 | 63 | for itemIndex, item in pairs(inventory) do 64 | if item.refId == refId then 65 | table.insert(indexes, itemIndex) 66 | end 67 | end 68 | 69 | return indexes 70 | end 71 | 72 | function inventoryHelper.addItem(inventory, refId, count, charge, enchantmentCharge, soul) 73 | 74 | if inventoryHelper.containsItem(inventory, refId, charge, enchantmentCharge, soul) then 75 | local index = inventoryHelper.getItemIndex(inventory, refId, charge, enchantmentCharge, soul) 76 | 77 | inventory[index].count = inventory[index].count + count 78 | else 79 | local item = {} 80 | item.refId = refId 81 | item.count = count 82 | item.charge = charge 83 | item.enchantmentCharge = enchantmentCharge 84 | item.soul = soul 85 | 86 | table.insert(inventory, item) 87 | end 88 | end 89 | 90 | -- Return true if an item (comparedItem) is closer to a desired item (idealItem) than 91 | -- another item is (otherItem) 92 | function inventoryHelper.compareClosenessToItem(idealItem, comparedItem, otherItem) 93 | 94 | if comparedItem == otherItem then 95 | return false 96 | end 97 | 98 | -- A difference in refIds instantly resolves the comparison 99 | if idealItem.refId ~= nil and not comparedItem.refId:ciEqual(otherItem.refId) then 100 | if idealItem.refId:ciEqual(comparedItem.refId) then 101 | return true 102 | elseif idealItem.refId:ciEqual(otherItem.refId) then 103 | return false 104 | end 105 | end 106 | 107 | if idealItem.soul ~= nil then 108 | if comparedItem.soul == nil then 109 | comparedItem.soul = "" 110 | end 111 | 112 | if otherItem.soul == nil then 113 | otherItem.soul = "" 114 | end 115 | 116 | -- A difference in souls also instantly resolves the comparison 117 | if not comparedItem.soul:ciEqual(otherItem.soul) then 118 | if idealItem.soul:ciEqual(comparedItem.soul) then 119 | return true 120 | elseif idealItem.soul:ciEqual(otherItem.soul) then 121 | return false 122 | end 123 | end 124 | end 125 | 126 | -- The TES3MP server doesn't yet load up data files, so it doesn't actually know what the 127 | -- maximum charge and enchantmentCharge are supposed to be for a particular refId 128 | -- 129 | -- Use some dirty workarounds here to ignore that fact until the sensible and elegant 130 | -- solution becomes available 131 | 132 | local comparedChargeDiff, otherChargeDiff = 0, 0 133 | local comparedEnchantmentChargeDiff, otherEnchantmentChargeDiff = 0, 0 134 | 135 | if idealItem.charge ~= nil and comparedItem.charge ~= otherItem.charge then 136 | 137 | if comparedItem.charge == nil then 138 | comparedItem.charge = -1 139 | end 140 | 141 | if otherItem.charge == nil then 142 | otherItem.charge = -1 143 | end 144 | 145 | local maxValue = math.max(idealItem.charge, comparedItem.charge, otherItem.charge) 146 | 147 | if maxValue < 400 then maxValue = maxValue + 400 end 148 | 149 | local adjustedIdealCharge = idealItem.charge 150 | local adjustedComparedCharge = comparedItem.charge 151 | local adjustedOtherCharge = otherItem.charge 152 | 153 | if adjustedIdealCharge == -1 then adjustedIdealCharge = maxValue + maxValue / 2 end 154 | if adjustedComparedCharge == -1 then adjustedComparedCharge = maxValue + maxValue / 2 end 155 | if adjustedOtherCharge == -1 then adjustedOtherCharge = maxValue + maxValue / 2 end 156 | 157 | comparedChargeDiff = math.abs(adjustedIdealCharge - adjustedComparedCharge) 158 | otherChargeDiff = math.abs(adjustedIdealCharge - adjustedOtherCharge) 159 | end 160 | 161 | if idealItem.enchantmentCharge ~= nil and comparedItem.enchantmentCharge ~= otherItem.enchantmentCharge then 162 | 163 | if comparedItem.enchantmentCharge == nil then 164 | comparedItem.enchantmentCharge = -1 165 | end 166 | 167 | if otherItem.enchantmentCharge == nil then 168 | otherItem.enchantmentCharge = -1 169 | end 170 | 171 | local maxValue = math.max(idealItem.enchantmentCharge, comparedItem.enchantmentCharge, otherItem.enchantmentCharge) 172 | 173 | if maxValue < 200 then maxValue = maxValue + 200 end 174 | 175 | local adjustedIdealEnchantmentCharge = idealItem.enchantmentCharge 176 | local adjustedComparedEnchantmentCharge = comparedItem.enchantmentCharge 177 | local adjustedOtherEnchantmentCharge = otherItem.enchantmentCharge 178 | 179 | if adjustedIdealEnchantmentCharge == -1 then adjustedIdealEnchantmentCharge = maxValue + maxValue / 2 end 180 | if adjustedComparedEnchantmentCharge == -1 then adjustedComparedEnchantmentCharge = maxValue + maxValue / 2 end 181 | if adjustedOtherEnchantmentCharge == -1 then adjustedOtherEnchantmentCharge = maxValue + maxValue / 2 end 182 | 183 | comparedEnchantmentChargeDiff = math.abs(adjustedIdealEnchantmentCharge - adjustedComparedEnchantmentCharge) 184 | otherEnchantmentChargeDiff = math.abs(adjustedIdealEnchantmentCharge - adjustedOtherEnchantmentCharge) 185 | end 186 | 187 | if comparedChargeDiff + comparedEnchantmentChargeDiff < otherChargeDiff + otherEnchantmentChargeDiff then 188 | return true 189 | end 190 | 191 | return false 192 | end 193 | 194 | function inventoryHelper.removeClosestItem(inventory, refId, count, charge, enchantmentCharge, soul) 195 | 196 | if inventoryHelper.containsItem(inventory, refId) then 197 | local itemIndexesToCompare = inventoryHelper.getItemIndexes(inventory, refId) 198 | local itemIndexesByCloseness = {} 199 | local idealItem = { refId = refId, charge = charge, enchantmentCharge = enchantmentCharge, 200 | soul = soul } 201 | 202 | for _, comparedItemIndex in ipairs(itemIndexesToCompare) do 203 | 204 | local comparedItem = inventory[comparedItemIndex] 205 | local isLeastClose = true 206 | 207 | for closenessRanking, otherItemIndex in ipairs(itemIndexesByCloseness) do 208 | local otherItem = inventory[otherItemIndex] 209 | 210 | if inventoryHelper.compareClosenessToItem(idealItem, comparedItem, otherItem) then 211 | table.insert(itemIndexesByCloseness, closenessRanking, comparedItemIndex) 212 | isLeastClose = false 213 | break 214 | end 215 | end 216 | 217 | if isLeastClose then 218 | table.insert(itemIndexesByCloseness, comparedItemIndex) 219 | end 220 | end 221 | 222 | local remainingCount = count 223 | 224 | for closenessRanking, currentItemIndex in ipairs(itemIndexesByCloseness) do 225 | 226 | if remainingCount > 0 then 227 | local currentItem = inventory[currentItemIndex] 228 | 229 | currentItem.count = currentItem.count - remainingCount 230 | 231 | if currentItem.count < 1 then 232 | remainingCount = 0 - currentItem.count 233 | currentItem = nil 234 | else 235 | remainingCount = 0 236 | end 237 | 238 | inventory[currentItemIndex] = currentItem 239 | else 240 | break 241 | end 242 | end 243 | end 244 | end 245 | 246 | function inventoryHelper.removeExactItem(inventory, refId, count, charge, enchantmentCharge, soul) 247 | 248 | if inventoryHelper.containsItem(inventory, refId, charge, enchantmentCharge, soul) then 249 | local index = inventoryHelper.getItemIndex(inventory, refId, charge, enchantmentCharge, soul) 250 | 251 | inventory[index].count = inventory[index].count - count 252 | 253 | if inventory[index].count < 1 then 254 | inventory[index] = nil 255 | end 256 | end 257 | end 258 | 259 | -- Deprecated 260 | function inventoryHelper.removeItem(inventory, refId, count, charge, enchantmentCharge, soul) 261 | return inventoryHelper.removeClosestItem(inventory, refId, count, charge, enchantmentCharge, soul) 262 | end 263 | 264 | return inventoryHelper 265 | -------------------------------------------------------------------------------- /scripts/menu/advancedExample.lua: -------------------------------------------------------------------------------- 1 | Menus["advanced example origin"] = { 2 | text = { 3 | color.Orange .. "Welcome, " .. color.Yellow, 4 | menuHelper.variables.currentPlayerVariable("data.login.name"), 5 | color.Orange .. "! This is an example of an advanced menu. Use it as a starting point for your own.\n\n" .. 6 | color.White .. "Select what kind of functions you want to run." 7 | }, 8 | buttons = { 9 | { caption = "Player functions", 10 | destinations = { menuHelper.destinations.setDefault("advanced example player") } 11 | }, 12 | { caption = "World instance functions", 13 | destinations = { menuHelper.destinations.setDefault("advanced example world") } 14 | }, 15 | { caption = "logicHandler functions", 16 | destinations = { menuHelper.destinations.setDefault("advanced example logichandler") } 17 | }, 18 | { caption = "Global functions", 19 | destinations = { menuHelper.destinations.setDefault("advanced example global") } 20 | }, 21 | { caption = "Exit", destinations = nil } 22 | } 23 | } 24 | 25 | Menus["advanced example player"] = { 26 | text = color.Orange .. "Select a function to run on this player.", 27 | buttons = { 28 | { caption = "Save()", 29 | destinations = { 30 | menuHelper.destinations.setDefault(nil, 31 | { 32 | menuHelper.effects.runPlayerFunction("Save") 33 | }) 34 | } 35 | }, 36 | { caption = "Message(\"This is a test\n\")", 37 | destinations = { 38 | menuHelper.destinations.setDefault(nil, 39 | { 40 | menuHelper.effects.runPlayerFunction("Message", {"This is a test\n"}) 41 | }) 42 | } 43 | }, 44 | { caption = "Back", destinations = { menuHelper.destinations.setFromCustomVariable("previousCustomMenu") } }, 45 | { caption = "Exit", destinations = nil } 46 | } 47 | } 48 | 49 | Menus["advanced example world"] = { 50 | text = { 51 | color.Orange .. "The world time is set to year " .. color.Yellow, 52 | menuHelper.variables.globalVariable("WorldInstance", "data.time.year"), 53 | color.Orange .. ", month " .. color.Yellow, 54 | menuHelper.variables.globalVariable("WorldInstance", "data.time.month"), 55 | color.Orange .. " and day " .. color.Yellow, 56 | menuHelper.variables.globalVariable("WorldInstance", "data.time.day"), 57 | color.Orange .. ". Select a function to run on this world." 58 | }, 59 | buttons = { 60 | { caption = "IncrementDay() and LoadTime(nil, true)", 61 | destinations = { 62 | menuHelper.destinations.setDefault(nil, 63 | { 64 | menuHelper.effects.runGlobalFunction("WorldInstance", "IncrementDay"), 65 | menuHelper.effects.runGlobalFunction("WorldInstance", "LoadTime", 66 | {menuHelper.variables.currentPid(), true}) 67 | }) 68 | } 69 | }, 70 | { caption = "Back", destinations = { menuHelper.destinations.setFromCustomVariable("previousCustomMenu") } }, 71 | { caption = "Exit", destinations = nil } 72 | } 73 | } 74 | 75 | Menus["advanced example logichandler"] = { 76 | text = color.Orange .. "Select a function to run on the logicHandler.", 77 | buttons = { 78 | { caption = "CreateObjectAtPlayer(menuHelper.variables.currentPid(), \"rat\", \"spawn\")", 79 | destinations = { 80 | menuHelper.destinations.setDefault(nil, 81 | { 82 | menuHelper.effects.runGlobalFunction("logicHandler", "CreateObjectAtPlayer", 83 | {menuHelper.variables.currentPid(), "rat", "spawn"}) 84 | }) 85 | } 86 | }, 87 | { caption = "Back", destinations = { menuHelper.destinations.setFromCustomVariable("previousCustomMenu") } }, 88 | { caption = "Exit", destinations = nil } 89 | } 90 | } 91 | 92 | Menus["advanced example global"] = { 93 | text = color.Orange .. "Select a global function to run.", 94 | buttons = { 95 | { caption = "OnPlayerSendMessage(menuHelper.variables.currentPid(), \"This is a test chat message\")", 96 | destinations = { 97 | menuHelper.destinations.setDefault(nil, 98 | { 99 | menuHelper.effects.runGlobalFunction(nil, "OnPlayerSendMessage", 100 | {menuHelper.variables.currentPid(), "This is a test chat message"}) 101 | }) 102 | } 103 | }, 104 | { caption = "Back", destinations = { menuHelper.destinations.setFromCustomVariable("previousCustomMenu") } }, 105 | { caption = "Exit", destinations = nil } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /scripts/menu/defaultCrafting.lua: -------------------------------------------------------------------------------- 1 | Menus["default crafting origin"] = { 2 | text = color.Orange .. "What would you like to craft?\n" .. 3 | color.Yellow .. "White pillow" .. color.White .. " - 1 per 2 folded cloth\n" .. 4 | color.Yellow .. "Hammock pillow" .. color.White .. " - 15 per 1 bolt of cloth\n" .. 5 | color.Yellow .. "Guarskin drum" .. color.White .. " - 1 per 3 guar hides", 6 | buttons = { 7 | { caption = "White pillow", 8 | destinations = { 9 | menuHelper.destinations.setDefault("lack of materials"), 10 | menuHelper.destinations.setConditional("default crafting pillow white", 11 | { 12 | menuHelper.conditions.requireItem("misc_de_foldedcloth00", 2) 13 | }) 14 | } 15 | }, 16 | { caption = "Hammock pillow", 17 | destinations = { 18 | menuHelper.destinations.setDefault("lack of materials"), 19 | menuHelper.destinations.setConditional("default crafting pillow hammock", 20 | { 21 | menuHelper.conditions.requireItem({"misc_clothbolt_01", "misc_clothbolt_02", "misc_clothbolt_03"}, 1) 22 | }) 23 | } 24 | }, 25 | { caption = "Guarskin drum", 26 | destinations = { 27 | menuHelper.destinations.setDefault("lack of materials"), 28 | menuHelper.destinations.setConditional("default crafting drum guarskin", 29 | { 30 | menuHelper.conditions.requireItem("ingred_guar_hide_01", 3) 31 | }) 32 | } 33 | }, 34 | { caption = "Exit", destinations = nil } 35 | } 36 | } 37 | 38 | Menus["default crafting pillow white"] = { 39 | text = "How many would you like to craft?", 40 | buttons = { 41 | { caption = "1", 42 | destinations = { 43 | menuHelper.destinations.setDefault("lack of materials"), 44 | menuHelper.destinations.setConditional("reward generic singular", 45 | { 46 | menuHelper.conditions.requireItem("misc_de_foldedcloth00", 2) 47 | }, 48 | { 49 | menuHelper.effects.removeItem("misc_de_foldedcloth00", 2), 50 | menuHelper.effects.giveItem("misc_uni_pillow_01", 1) 51 | }) 52 | } 53 | }, 54 | { caption = "5", 55 | destinations = { 56 | menuHelper.destinations.setDefault("lack of materials"), 57 | menuHelper.destinations.setConditional("reward generic plural", 58 | { 59 | menuHelper.conditions.requireItem("misc_de_foldedcloth00", 10) 60 | }, 61 | { 62 | menuHelper.effects.removeItem("misc_de_foldedcloth00", 10), 63 | menuHelper.effects.giveItem("misc_uni_pillow_01", 5) 64 | }) 65 | } 66 | }, 67 | { caption = "Back", destinations = { menuHelper.destinations.setDefault("default crafting origin") } }, 68 | { caption = "Exit", destinations = nil } 69 | } 70 | } 71 | 72 | Menus["default crafting pillow hammock"] = { 73 | text = "How many would you like to craft?", 74 | buttons = { 75 | { caption = "15", 76 | destinations = { 77 | menuHelper.destinations.setDefault("lack of materials"), 78 | menuHelper.destinations.setConditional("reward generic plural", 79 | { 80 | menuHelper.conditions.requireItem({"misc_clothbolt_01", "misc_clothbolt_02", "misc_clothbolt_03"}, 1) 81 | 82 | }, 83 | { 84 | menuHelper.effects.removeItem({"misc_clothbolt_01", "misc_clothbolt_02", "misc_clothbolt_03"}, 1), 85 | menuHelper.effects.giveItem("Misc_Uni_Pillow_02", 15) 86 | }) 87 | } 88 | }, 89 | { caption = "60", 90 | destinations = { 91 | menuHelper.destinations.setDefault("lack of materials"), 92 | menuHelper.destinations.setConditional("reward generic plural", 93 | { 94 | menuHelper.conditions.requireItem({"misc_clothbolt_01", "misc_clothbolt_02", "misc_clothbolt_03"}, 4) 95 | }, 96 | { 97 | menuHelper.effects.removeItem({"misc_clothbolt_01", "misc_clothbolt_02", "misc_clothbolt_03"}, 4), 98 | menuHelper.effects.giveItem("Misc_Uni_Pillow_02", 60) 99 | }) 100 | } 101 | }, 102 | { caption = "Back", destinations = { menuHelper.destinations.setDefault("default crafting origin") } }, 103 | { caption = "Exit", destinations = nil } 104 | } 105 | } 106 | 107 | Menus["default crafting drum guarskin"] = { 108 | text = "How many would you like to craft?", 109 | buttons = { 110 | { caption = "1", 111 | destinations = { 112 | menuHelper.destinations.setDefault("lack of materials"), 113 | menuHelper.destinations.setConditional("reward generic singular", 114 | { 115 | menuHelper.conditions.requireItem("ingred_guar_hide_01", 3) 116 | }, 117 | { 118 | menuHelper.effects.removeItem("ingred_guar_hide_01", 3), 119 | menuHelper.effects.giveItem("misc_de_drum_02", 1) 120 | }) 121 | } 122 | }, 123 | { caption = "5", 124 | destinations = { 125 | menuHelper.destinations.setDefault("lack of materials"), 126 | menuHelper.destinations.setConditional("reward generic plural", 127 | { 128 | menuHelper.conditions.requireItem("ingred_guar_hide_01", 15) 129 | }, 130 | { 131 | menuHelper.effects.removeItem("ingred_guar_hide_01", 15), 132 | menuHelper.effects.giveItem("misc_de_drum_02", 5) 133 | }) 134 | } 135 | }, 136 | { caption = "Back", destinations = { menuHelper.destinations.setDefault("default crafting origin") } }, 137 | { caption = "Exit", destinations = nil } 138 | } 139 | } 140 | 141 | Menus["lack of materials"] = { 142 | text = "You lack the materials required.", 143 | buttons = { 144 | { caption = "Back", destinations = { menuHelper.destinations.setFromCustomVariable("previousCustomMenu") } }, 145 | { caption = "Ok", destinations = nil } 146 | } 147 | } 148 | 149 | Menus["reward generic singular"] = { 150 | text = "Congratulations! The item is now yours", 151 | buttons = { 152 | { caption = "Craft more", destinations = { menuHelper.destinations.setFromCustomVariable("previousCustomMenu") } }, 153 | { caption = "Exit", destinations = nil } 154 | } 155 | } 156 | 157 | Menus["reward generic plural"] = { 158 | text = "Congratulations! The items are now yours", 159 | buttons = { 160 | { caption = "Craft more", destinations = { menuHelper.destinations.setFromCustomVariable("previousCustomMenu") } }, 161 | { caption = "Exit", destinations = nil } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /scripts/menuHelper.lua: -------------------------------------------------------------------------------- 1 | require("config") 2 | inventoryHelper = require("inventoryHelper") 3 | 4 | local menuHelper = {} 5 | menuHelper.conditions = {} 6 | menuHelper.effects = {} 7 | menuHelper.destinations = {} 8 | menuHelper.variables = {} 9 | 10 | function menuHelper.conditions.requireItem(inputRefIds, inputCount) 11 | 12 | if type(inputRefIds) ~= "table" then 13 | inputRefIds = { inputRefIds } 14 | end 15 | 16 | local condition = { 17 | conditionType = "item", 18 | refIds = inputRefIds, 19 | count = inputCount 20 | } 21 | 22 | return condition 23 | end 24 | 25 | function menuHelper.conditions.requireAttribute(inputName, inputValue) 26 | local condition = { 27 | conditionType = "attribute", 28 | attributeName = inputName, 29 | attributeValue = inputValue 30 | } 31 | 32 | return condition 33 | end 34 | 35 | function menuHelper.conditions.requireSkill(inputName, inputValue) 36 | local condition = { 37 | conditionType = "skill", 38 | skillName = inputName, 39 | skillValue = inputValue 40 | } 41 | 42 | return condition 43 | end 44 | 45 | function menuHelper.conditions.requireStaffRank(inputValue) 46 | local condition = { 47 | conditionType = "staffRank", 48 | rankValue = inputValue 49 | } 50 | 51 | return condition 52 | end 53 | 54 | function menuHelper.conditions.requirePlayerFunction(inputFunctionName, inputArguments) 55 | local condition = { 56 | conditionType = "playerFunction", 57 | functionName = inputFunctionName, 58 | arguments = inputArguments 59 | } 60 | 61 | return condition 62 | end 63 | 64 | -- Deprecated 65 | function menuHelper.conditions.requireAdminRank(inputValue) 66 | return menuHelper.conditions.requireStaffRank(inputValue) 67 | end 68 | 69 | function menuHelper.effects.giveItem(inputRefId, inputCount) 70 | local effect = { 71 | effectType = "item", 72 | action = "give", 73 | refId = inputRefId, 74 | count = inputCount 75 | } 76 | 77 | return effect 78 | end 79 | 80 | function menuHelper.effects.removeItem(inputRefIds, inputCount) 81 | 82 | if type(inputRefIds) ~= "table" then 83 | inputRefIds = { inputRefIds } 84 | end 85 | 86 | local effect = { 87 | effectType = "item", 88 | action = "remove", 89 | refIds = inputRefIds, 90 | count = inputCount 91 | } 92 | 93 | return effect 94 | end 95 | 96 | function menuHelper.effects.setPlayerDataVariable(inputVariable, inputValue) 97 | local effect = { 98 | effectType = "playerVariable", 99 | action = "data", 100 | variable = inputVariable, 101 | value = inputValue 102 | } 103 | 104 | return effect 105 | end 106 | 107 | function menuHelper.effects.runPlayerFunction(inputFunctionName, inputArguments) 108 | local effect = { 109 | effectType = "playerFunction", 110 | functionName = inputFunctionName, 111 | arguments = inputArguments 112 | } 113 | 114 | return effect 115 | end 116 | 117 | function menuHelper.effects.runGlobalFunction(inputObjectName, inputFunctionName, inputArguments) 118 | local effect = { 119 | effectType = "globalFunction", 120 | objectName = inputObjectName, 121 | functionName = inputFunctionName, 122 | arguments = inputArguments 123 | } 124 | 125 | return effect 126 | end 127 | 128 | -- Deprecated 129 | function menuHelper.effects.setDataVariable(inputVariable, inputValue) 130 | return menuHelper.effects.setPlayerDataVariable(inputVariable, inputValue) 131 | end 132 | 133 | -- Deprecated 134 | function menuHelper.effects.runFunction(inputFunctionName, inputArguments) 135 | return menuHelper.effects.runPlayerFunction(inputFunctionName, inputArguments) 136 | end 137 | 138 | function menuHelper.destinations.setDefault(inputMenu, inputEffects) 139 | local destination = { 140 | targetMenu = inputMenu, 141 | effects = inputEffects 142 | } 143 | 144 | return destination 145 | end 146 | 147 | function menuHelper.destinations.setFromCustomVariable(inputVariable) 148 | local destination = { 149 | customVariable = inputVariable 150 | } 151 | 152 | return destination 153 | end 154 | 155 | function menuHelper.destinations.setConditional(inputMenu, inputConditions, inputEffects) 156 | local destination = { 157 | targetMenu = inputMenu, 158 | conditions = inputConditions, 159 | effects = inputEffects 160 | } 161 | 162 | return destination 163 | end 164 | 165 | function menuHelper.variables.currentPid() 166 | local variable = { 167 | variableType = "pid", 168 | source = "current" 169 | } 170 | 171 | return variable 172 | end 173 | 174 | function menuHelper.variables.currentChatName() 175 | local variable = { 176 | variableType = "chatName", 177 | source = "current" 178 | } 179 | 180 | return variable 181 | end 182 | 183 | function menuHelper.variables.currentPlayerVariable(inputVariableName) 184 | local variable = { 185 | variableType = "playerVariable", 186 | source = "current", 187 | variableName = inputVariableName 188 | } 189 | 190 | return variable 191 | end 192 | 193 | -- Deprecated 194 | function menuHelper.variables.currentPlayerDataVariable(inputVariableName) 195 | return menuHelper.variables.currentPlayerVariable("data." .. inputVariableName) 196 | end 197 | 198 | function menuHelper.variables.globalVariable(inputObjectName, inputVariableName) 199 | local variable = { 200 | variableType = "globalVariable", 201 | objectName = inputObjectName, 202 | variableName = inputVariableName 203 | } 204 | 205 | return variable 206 | end 207 | 208 | function menuHelper.variables.concatenation(inputDelimiter, ...) 209 | local variable = { 210 | variableType = "argumentArray", 211 | operation = "concatenation", 212 | delimiter = inputDelimiter, 213 | containedVariables = {...} 214 | } 215 | 216 | return variable 217 | end 218 | 219 | function menuHelper.CheckCondition(pid, condition) 220 | 221 | local targetPlayer = Players[pid] 222 | 223 | if condition.conditionType == "item" then 224 | 225 | local remainingCount = condition.count 226 | 227 | for _, currentRefId in ipairs(condition.refIds) do 228 | 229 | if inventoryHelper.containsItem(targetPlayer.data.inventory, currentRefId) then 230 | local itemIndex = inventoryHelper.getItemIndex(targetPlayer.data.inventory, currentRefId) 231 | local item = targetPlayer.data.inventory[itemIndex] 232 | 233 | remainingCount = remainingCount - item.count 234 | 235 | if remainingCount < 1 then 236 | return true 237 | end 238 | end 239 | end 240 | elseif condition.conditionType == "attribute" then 241 | 242 | if targetPlayer.data.attributes[condition.attributeName].base >= condition.attributeValue then 243 | return true 244 | end 245 | elseif condition.conditionType == "skill" then 246 | 247 | if targetPlayer.data.skills[condition.skillName].base >= condition.skillValue then 248 | return true 249 | end 250 | elseif condition.conditionType == "staffRank" then 251 | 252 | if targetPlayer.data.settings.staffRank >= condition.rankValue then 253 | return true 254 | end 255 | elseif condition.conditionType == "playerFunction" then 256 | 257 | local functionName = condition.functionName 258 | local arguments = condition.arguments 259 | 260 | if arguments == nil then 261 | arguments = {} 262 | -- Fill in any variables placed inside the arguments 263 | else 264 | arguments = menuHelper.ProcessVariables(pid, arguments) 265 | end 266 | 267 | if targetPlayer[functionName](targetPlayer, unpack(arguments)) then 268 | return true 269 | end 270 | end 271 | 272 | return false 273 | end 274 | 275 | function menuHelper.CheckConditionTable(pid, conditions) 276 | 277 | local conditionCount = table.maxn(conditions) 278 | local conditionsMet = 0 279 | 280 | for _, condition in ipairs(conditions) do 281 | 282 | if menuHelper.CheckCondition(pid, condition) then 283 | conditionsMet = conditionsMet + 1 284 | end 285 | end 286 | 287 | if conditionsMet == conditionCount then 288 | return true 289 | end 290 | 291 | return false 292 | end 293 | 294 | function menuHelper.ProcessVariables(pid, inputTable) 295 | 296 | local resultTable = {} 297 | 298 | for tableIndex, tableElement in ipairs(inputTable) do 299 | 300 | local resultValue = "nil" 301 | 302 | if type(tableElement) == "table" and tableElement.variableType ~= nil then 303 | 304 | local variableType = tableElement.variableType 305 | local subType = tableElement.subType 306 | local source = tableElement.source 307 | 308 | if variableType == "pid" then 309 | if source == "current" then 310 | resultValue = pid 311 | end 312 | elseif variableType == "chatName" then 313 | if source == "current" then 314 | resultValue = logicHandler.GetChatName(pid) 315 | end 316 | elseif variableType == "playerVariable" or variableType == "globalVariable" then 317 | 318 | local variableName = tableElement.variableName 319 | 320 | if variableType == "playerVariable" and source == "current" then 321 | resultValue = Players[pid] 322 | elseif variableType == "globalVariable" then 323 | local objectName = tableElement.objectName 324 | 325 | if objectName ~= nil then 326 | resultValue = _G[objectName] 327 | else 328 | resultValue = _G 329 | end 330 | end 331 | 332 | if type(resultValue) == "table" then 333 | -- Allow for nested variables (such as character.race or location.cell) 334 | -- by iterating through every value separated by a period 335 | for nestedName in string.gmatch(variableName, patterns.periodSplit) do 336 | if type(resultValue[nestedName]) ~= "nil" then 337 | resultValue = resultValue[nestedName] 338 | else 339 | resultValue = "nil" 340 | break 341 | end 342 | end 343 | end 344 | elseif variableType == "argumentArray" then 345 | local operation = tableElement.operation 346 | local delimiter = tableElement.delimiter 347 | 348 | local processedVariables = menuHelper.ProcessVariables(pid, tableElement.containedVariables) 349 | 350 | if operation == "concatenation" then 351 | resultValue = tableHelper.concatenateArrayValues(processedVariables, 1, delimiter) 352 | end 353 | end 354 | else 355 | resultValue = tostring(tableElement) 356 | end 357 | 358 | table.insert(resultTable, resultValue) 359 | end 360 | 361 | return resultTable 362 | end 363 | 364 | function menuHelper.ProcessEffects(pid, effects) 365 | 366 | if effects == nil then return end 367 | 368 | local targetPlayer = Players[pid] 369 | local shouldReloadInventory = false 370 | 371 | for _, effect in ipairs(effects) do 372 | 373 | local effectType = effect.effectType 374 | 375 | if effectType == "item" then 376 | 377 | shouldReloadInventory = true 378 | 379 | if effect.action == "give" then 380 | 381 | inventoryHelper.addItem(targetPlayer.data.inventory, effect.refId, effect.count, -1, -1) 382 | 383 | elseif effect.action == "remove" then 384 | 385 | local remainingCount = effect.count 386 | 387 | for _, currentRefId in ipairs(effect.refIds) do 388 | 389 | if remainingCount > 0 and inventoryHelper.containsItem(targetPlayer.data.inventory, 390 | currentRefId) then 391 | 392 | -- If the item is equipped by the target, unequip it first 393 | if inventoryHelper.containsItem(targetPlayer.data.equipment, currentRefId) then 394 | local equipmentItemIndex = inventoryHelper.getItemIndex(targetPlayer.data.equipment, 395 | currentRefId) 396 | targetPlayer.data.equipment[equipmentItemIndex] = nil 397 | end 398 | 399 | local inventoryItemIndex = inventoryHelper.getItemIndex(targetPlayer.data.inventory, 400 | currentRefId) 401 | local item = targetPlayer.data.inventory[inventoryItemIndex] 402 | item.count = item.count - remainingCount 403 | 404 | if item.count < 1 then 405 | remainingCount = 0 - item.count 406 | item = nil 407 | else 408 | remainingCount = 0 409 | end 410 | 411 | targetPlayer.data.inventory[inventoryItemIndex] = item 412 | end 413 | end 414 | end 415 | elseif effectType == "playerVariable" then 416 | 417 | if effect.action == "data" then 418 | targetPlayer.data[effect.variable] = effect.value 419 | end 420 | elseif effectType == "playerFunction" or effectType == "globalFunction" then 421 | 422 | local functionName = effect.functionName 423 | local arguments = effect.arguments 424 | 425 | if arguments == nil then 426 | arguments = {} 427 | -- Fill in any variables placed inside the arguments 428 | else 429 | arguments = menuHelper.ProcessVariables(pid, arguments) 430 | end 431 | 432 | if effectType == "playerFunction" then 433 | targetPlayer[functionName](targetPlayer, unpack(arguments)) 434 | elseif effectType == "globalFunction" then 435 | 436 | local objectName = effect.objectName 437 | 438 | if objectName ~= nil then 439 | local targetObject = _G[objectName] 440 | 441 | -- If this object doesn't have a metatable, don't pass it to itself 442 | -- as an argument 443 | if getmetatable(targetObject) == nil then 444 | targetObject[functionName](unpack(arguments)) 445 | else 446 | targetObject[functionName](targetObject, unpack(arguments)) 447 | end 448 | else 449 | _G[functionName](unpack(arguments)) 450 | end 451 | end 452 | end 453 | end 454 | 455 | targetPlayer:QuicksaveToDrive() 456 | 457 | if shouldReloadInventory then 458 | targetPlayer:LoadInventory() 459 | targetPlayer:LoadEquipment() 460 | end 461 | end 462 | 463 | function menuHelper.GetButtonDestination(pid, buttonPressed) 464 | 465 | if buttonPressed ~= nil then 466 | 467 | local defaultDestination = {} 468 | 469 | if buttonPressed.destinations ~= nil then 470 | 471 | for _, destination in ipairs(buttonPressed.destinations) do 472 | 473 | if destination.customVariable ~= nil then 474 | local customVariable = destination.customVariable 475 | destination.targetMenu = Players[pid][customVariable] 476 | end 477 | 478 | if destination.conditions == nil then 479 | defaultDestination = destination 480 | else 481 | local conditionsMet = menuHelper.CheckConditionTable(pid, destination.conditions) 482 | 483 | if conditionsMet then 484 | return destination 485 | end 486 | end 487 | end 488 | end 489 | 490 | return defaultDestination 491 | end 492 | 493 | return {} 494 | end 495 | 496 | function menuHelper.GetDisplayedButtons(pid, menuIndex) 497 | 498 | if menuIndex == nil or Menus[menuIndex] == nil then return end 499 | local displayedButtons = {} 500 | 501 | for buttonIndex, button in ipairs(Menus[menuIndex].buttons) do 502 | 503 | -- Only display this button if there are no conditions for displaying it, or if 504 | -- the conditions for displaying it are met 505 | local conditionsMet = true 506 | 507 | if button.displayConditions ~= nil then 508 | conditionsMet = menuHelper.CheckConditionTable(pid, button.displayConditions) 509 | end 510 | 511 | if conditionsMet then 512 | table.insert(displayedButtons, button) 513 | end 514 | end 515 | 516 | return displayedButtons 517 | end 518 | 519 | function menuHelper.DisplayMenu(pid, menuIndex) 520 | 521 | if menuIndex == nil or Menus[menuIndex] == nil then return end 522 | 523 | local text = Menus[menuIndex].text 524 | 525 | -- Is this a table? If so, process the variables in it and then concatenate them 526 | if text == nil then 527 | text = "" 528 | elseif type(text) == "table" then 529 | local processedTextVariables = menuHelper.ProcessVariables(pid, text) 530 | text = tableHelper.concatenateArrayValues(processedTextVariables, 1, "") 531 | end 532 | 533 | local displayedButtons = menuHelper.GetDisplayedButtons(pid, menuIndex) 534 | local buttonCount = tableHelper.getCount(displayedButtons) 535 | local buttonList = "" 536 | 537 | for buttonIndex, button in ipairs(displayedButtons) do 538 | 539 | local caption = button.caption 540 | 541 | -- Handle button captions the same way as menu text 542 | if type(caption) == "table" then 543 | local processedTextVariables = menuHelper.ProcessVariables(pid, caption) 544 | caption = tableHelper.concatenateArrayValues(processedTextVariables, 1, "") 545 | end 546 | 547 | buttonList = buttonList .. caption 548 | 549 | if buttonIndex < buttonCount then 550 | buttonList = buttonList .. ";" 551 | end 552 | end 553 | 554 | Players[pid].displayedMenuButtons = displayedButtons 555 | 556 | tes3mp.CustomMessageBox(pid, config.customMenuIds.menuHelper, text, buttonList) 557 | end 558 | 559 | return menuHelper 560 | -------------------------------------------------------------------------------- /scripts/player/json.lua: -------------------------------------------------------------------------------- 1 | require("config") 2 | fileHelper = require("fileHelper") 3 | tableHelper = require("tableHelper") 4 | local BasePlayer = require("player.base") 5 | 6 | local Player = class("Player", BasePlayer) 7 | 8 | function Player:__init(pid, playerName) 9 | BasePlayer.__init(self, pid, playerName) 10 | 11 | -- Ensure filename is valid 12 | self.accountName = fileHelper.fixFilename(playerName) 13 | 14 | self.accountFile = tes3mp.GetCaseInsensitiveFilename(config.dataPath .. "/player/", self.accountName .. ".json") 15 | 16 | if self.accountFile == "invalid" then 17 | self.hasAccount = false 18 | self.accountFile = self.accountName .. ".json" 19 | else 20 | self.hasAccount = true 21 | end 22 | end 23 | 24 | function Player:CreateAccount() 25 | self.hasAccount = jsonInterface.save("player/" .. self.accountFile, self.data) 26 | 27 | if self.hasAccount then 28 | tes3mp.LogMessage(enumerations.log.INFO, "Successfully created JSON file for player " .. self.accountName) 29 | else 30 | local message = "Failed to create JSON file for " .. self.accountName 31 | tes3mp.SendMessage(self.pid, message, true) 32 | tes3mp.Kick(self.pid) 33 | end 34 | end 35 | 36 | function Player:SaveToDrive() 37 | if self.hasAccount then 38 | tes3mp.LogMessage(enumerations.log.INFO, "Saving player " .. logicHandler.GetChatName(self.pid)) 39 | jsonInterface.save("player/" .. self.accountFile, self.data, config.playerKeyOrder) 40 | end 41 | end 42 | 43 | function Player:QuicksaveToDrive() 44 | if self.hasAccount then 45 | jsonInterface.quicksave("player/" .. self.accountFile, self.data) 46 | end 47 | end 48 | 49 | function Player:LoadFromDrive() 50 | self.data = jsonInterface.load("player/" .. self.accountFile) 51 | 52 | if self.data == nil then 53 | tes3mp.LogMessage(enumerations.log.ERROR, "player/" .. self.accountFile .. " cannot be read!") 54 | tes3mp.StopServer(2) 55 | else 56 | -- JSON doesn't allow numerical keys, but we use them, so convert 57 | -- all string number keys into numerical keys 58 | tableHelper.fixNumericalKeys(self.data) 59 | 60 | if self.data.login.password ~= nil and self.data.login.passwordHash == nil then 61 | self:ConvertPlaintextPassword() 62 | end 63 | end 64 | end 65 | 66 | -- Deprecated functions with confusing names, kept around for backwards compatibility 67 | function Player:Save() 68 | self:SaveToDrive() 69 | end 70 | 71 | function Player:Load() 72 | self:LoadFromDrive() 73 | end 74 | 75 | return Player 76 | -------------------------------------------------------------------------------- /scripts/player/sql.lua: -------------------------------------------------------------------------------- 1 | Database = require("database") 2 | local BasePlayer = require("player.base") 3 | 4 | local Player = class("Player", BasePlayer) 5 | 6 | function Player:__init(pid, playerName) 7 | BasePlayer.__init(self, pid, playerName) 8 | 9 | if self.hasAccount == nil then 10 | 11 | self.dbPid = self:GetDatabaseId() 12 | 13 | if self.dbPid ~= nil then 14 | self.hasAccount = true 15 | else 16 | self.hasAccount = false 17 | end 18 | end 19 | end 20 | 21 | function Player:CreateAccount() 22 | Database:InsertRow("player_login", self.data.login) 23 | self.dbPid = self:GetDatabaseId() 24 | Database:SavePlayer(self.dbPid, self.data) 25 | self.hasAccount = true 26 | end 27 | 28 | function Player:SaveToDrive() 29 | if self.hasAccount and self.loggedIn then 30 | Database:SavePlayer(self.dbPid, self.data) 31 | end 32 | end 33 | 34 | function Player:LoadFromDrive() 35 | self.data = Database:LoadPlayer(self.dbPid, self.data) 36 | end 37 | 38 | function Player:GetDatabaseId() 39 | local escapedName = Database:Escape(self.accountName) 40 | return Database:GetSingleValue("player_login", "dbPid", string.format("WHERE name = '%s'", escapedName)) 41 | end 42 | 43 | return Player 44 | -------------------------------------------------------------------------------- /scripts/recordstore/base.lua: -------------------------------------------------------------------------------- 1 | local BaseRecordStore = class("BaseRecordStore") 2 | 3 | BaseRecordStore.defaultData = 4 | { 5 | general = { 6 | currentGeneratedNum = 0 7 | }, 8 | permanentRecords = {}, 9 | generatedRecords = {}, 10 | recordLinks = {}, 11 | unlinkedRecordsToCheck = {} 12 | } 13 | 14 | function BaseRecordStore:__init(storeType) 15 | 16 | self.data = tableHelper.deepCopy(self.defaultData) 17 | self.storeType = storeType 18 | end 19 | 20 | function BaseRecordStore:HasEntry() 21 | return self.hasEntry 22 | end 23 | 24 | function BaseRecordStore:EnsureDataStructure() 25 | 26 | for key, value in pairs(self.defaultData) do 27 | if self.data[key] == nil then 28 | self.data[key] = tableHelper.deepCopy(value) 29 | end 30 | end 31 | end 32 | 33 | function BaseRecordStore:GetCurrentGeneratedNum() 34 | return self.data.general.currentGeneratedNum 35 | end 36 | 37 | function BaseRecordStore:SetCurrentGeneratedNum(currentGeneratedNum) 38 | self.data.general.currentGeneratedNum = currentGeneratedNum 39 | self:QuicksaveToDrive() 40 | end 41 | 42 | function BaseRecordStore:IncrementGeneratedNum() 43 | self:SetCurrentGeneratedNum(self:GetCurrentGeneratedNum() + 1) 44 | return self:GetCurrentGeneratedNum() 45 | end 46 | 47 | function BaseRecordStore:GenerateRecordId() 48 | return config.generatedRecordIdPrefix .. "_" .. self.storeType .. "_" .. self:IncrementGeneratedNum() 49 | end 50 | 51 | -- Go through all the generated records that were at some point tracked as having no links remaining 52 | -- and delete them if they still have no links 53 | function BaseRecordStore:DeleteUnlinkedRecords() 54 | 55 | if type(self.data.unlinkedRecordsToCheck) == "table" then 56 | for arrayIndex, recordId in pairs(self.data.unlinkedRecordsToCheck) do 57 | if not self:HasLinks(recordId) then 58 | self:DeleteGeneratedRecord(recordId) 59 | end 60 | 61 | self.data.unlinkedRecordsToCheck[arrayIndex] = nil 62 | end 63 | end 64 | end 65 | 66 | function BaseRecordStore:DeleteGeneratedRecord(recordId) 67 | 68 | if self.data.generatedRecords[recordId] == nil then 69 | tes3mp.LogMessage(enumerations.log.WARN, "Tried deleting " .. self.storeType .. " record " .. recordId .. 70 | " which doesn't exist!") 71 | return 72 | end 73 | 74 | tes3mp.LogMessage(enumerations.log.WARN, "Deleting generated " .. self.storeType .. " record " .. recordId) 75 | 76 | -- Is this an enchantable record? If so, we should remove any links to it 77 | -- from its associated generated enchantment record if there is one 78 | if tableHelper.containsValue(config.enchantableRecordTypes, self.storeType) then 79 | local enchantmentId = self.data.generatedRecords[recordId].enchantmentId 80 | 81 | if enchantmentId ~= nil and logicHandler.IsGeneratedRecord(enchantmentId) then 82 | local enchantmentStore = RecordStores["enchantment"] 83 | enchantmentStore:RemoveLinkToRecord(enchantmentId, recordId, self.storeType) 84 | enchantmentStore:QuicksaveToDrive() 85 | end 86 | end 87 | 88 | self.data.generatedRecords[recordId] = nil 89 | 90 | if self.data.recordLinks[recordId] ~= nil then 91 | self.data.recordLinks[recordId] = nil 92 | end 93 | 94 | self:QuicksaveToDrive() 95 | end 96 | 97 | -- Check whether there are any links remaining to a certain generated record 98 | function BaseRecordStore:HasLinks(recordId) 99 | 100 | local recordLinks = self.data.recordLinks 101 | 102 | if recordLinks[recordId] == nil then 103 | return false 104 | elseif (recordLinks[recordId].cells ~= nil and not tableHelper.isEmpty(recordLinks[recordId].cells)) or 105 | (recordLinks[recordId].players ~= nil and not tableHelper.isEmpty(recordLinks[recordId].players)) then 106 | return true 107 | -- Is this an enchantment record? If so, check for links to other records for enchantable items 108 | elseif self.storeType == "enchantment" then 109 | for _, enchantableType in pairs(config.enchantableRecordTypes) do 110 | if recordLinks[recordId].records ~= nil and recordLinks[recordId].records[enchantableType] ~= nil and not 111 | tableHelper.isEmpty(recordLinks[recordId].records[enchantableType]) then 112 | return true 113 | end 114 | end 115 | end 116 | 117 | return false 118 | end 119 | 120 | -- Add a link between a record and another record from a different record store, 121 | -- i.e. for enchantments being used by other items 122 | function BaseRecordStore:AddLinkToRecord(recordId, otherRecordId, otherStoreType) 123 | 124 | local recordLinks = self.data.recordLinks 125 | 126 | if recordLinks[recordId] == nil then recordLinks[recordId] = {} end 127 | if recordLinks[recordId].records == nil then recordLinks[recordId].records = {} end 128 | if recordLinks[recordId].records[otherStoreType] == nil then recordLinks[recordId].records[otherStoreType] = {} end 129 | 130 | if not tableHelper.containsValue(recordLinks[recordId].records[otherStoreType], otherRecordId) then 131 | table.insert(recordLinks[recordId].records[otherStoreType], otherRecordId) 132 | end 133 | end 134 | 135 | function BaseRecordStore:RemoveLinkToRecord(recordId, otherRecordId, otherStoreType) 136 | 137 | local recordLinks = self.data.recordLinks 138 | 139 | if recordLinks[recordId] ~= nil and recordLinks[recordId].records ~= nil and 140 | recordLinks[recordId].records[otherStoreType] ~= nil then 141 | 142 | local linkIndex = tableHelper.getIndexByValue(recordLinks[recordId].records[otherStoreType], otherRecordId) 143 | 144 | if linkIndex ~= nil then 145 | recordLinks[recordId].records[otherStoreType][linkIndex] = nil 146 | end 147 | 148 | if not self:HasLinks(recordId) then 149 | table.insert(self.data.unlinkedRecordsToCheck, recordId) 150 | end 151 | end 152 | end 153 | 154 | -- Add a link between a record and a cell it is found in 155 | function BaseRecordStore:AddLinkToCell(recordId, cell) 156 | 157 | local cellDescription = cell.description 158 | local recordLinks = self.data.recordLinks 159 | 160 | if recordLinks[recordId] == nil then recordLinks[recordId] = {} end 161 | if recordLinks[recordId].cells == nil then recordLinks[recordId].cells = {} end 162 | 163 | if not tableHelper.containsValue(recordLinks[recordId].cells, cellDescription) then 164 | table.insert(recordLinks[recordId].cells, cellDescription) 165 | end 166 | end 167 | 168 | function BaseRecordStore:RemoveLinkToCell(recordId, cell) 169 | 170 | local cellDescription = cell.description 171 | local recordLinks = self.data.recordLinks 172 | 173 | if recordLinks[recordId] ~= nil and recordLinks[recordId].cells ~= nil then 174 | 175 | local linkIndex = tableHelper.getIndexByValue(recordLinks[recordId].cells, cellDescription) 176 | 177 | if linkIndex ~= nil then 178 | recordLinks[recordId].cells[linkIndex] = nil 179 | end 180 | 181 | if not self:HasLinks(recordId) then 182 | table.insert(self.data.unlinkedRecordsToCheck, recordId) 183 | end 184 | end 185 | end 186 | 187 | -- Add a link between a record and a player in whose inventory or spellbook it is found 188 | function BaseRecordStore:AddLinkToPlayer(recordId, player) 189 | 190 | local accountName = player.accountName 191 | local recordLinks = self.data.recordLinks 192 | 193 | if recordLinks[recordId] == nil then recordLinks[recordId] = {} end 194 | if recordLinks[recordId].players == nil then recordLinks[recordId].players = {} end 195 | 196 | if not tableHelper.containsValue(recordLinks[recordId].players, accountName) then 197 | table.insert(recordLinks[recordId].players, accountName) 198 | end 199 | end 200 | 201 | function BaseRecordStore:RemoveLinkToPlayer(recordId, player) 202 | 203 | local accountName = player.accountName 204 | local recordLinks = self.data.recordLinks 205 | 206 | if recordLinks[recordId] == nil then recordLinks[recordId] = {} end 207 | if recordLinks[recordId].players == nil then recordLinks[recordId].players = {} end 208 | 209 | local linkIndex = tableHelper.getIndexByValue(recordLinks[recordId].players, accountName) 210 | 211 | if linkIndex ~= nil then 212 | recordLinks[recordId].players[linkIndex] = nil 213 | end 214 | 215 | if not self:HasLinks(recordId) then 216 | table.insert(self.data.unlinkedRecordsToCheck, recordId) 217 | end 218 | end 219 | 220 | function BaseRecordStore:LoadGeneratedRecords(pid, recordList, idArray, forEveryone) 221 | 222 | if type(recordList) ~= "table" then return end 223 | if type(idArray) ~= "table" then return end 224 | 225 | local validIdArray = {} 226 | 227 | -- If these are enchantable records, track generated enchantment records used by them 228 | -- and send them beforehand 229 | local isEnchantable = false 230 | local enchantmentIdArray 231 | 232 | if tableHelper.containsValue(config.enchantableRecordTypes, self.storeType) then 233 | isEnchantable = true 234 | enchantmentIdArray = {} 235 | end 236 | 237 | for _, recordId in pairs(idArray) do 238 | if recordList[recordId] ~= nil and not tableHelper.containsValue(Players[pid].generatedRecordsReceived, recordId) then 239 | 240 | table.insert(Players[pid].generatedRecordsReceived, recordId) 241 | table.insert(validIdArray, recordId) 242 | 243 | if isEnchantable then 244 | local record = recordList[recordId] 245 | local shouldLoadEnchantment = record ~= nil and record.enchantmentId ~= nil and 246 | logicHandler.IsGeneratedRecord(record.enchantmentId) and not 247 | tableHelper.containsValue(Players[pid].generatedRecordsReceived, record.enchantmentId) 248 | 249 | if shouldLoadEnchantment then 250 | table.insert(enchantmentIdArray, record.enchantmentId) 251 | end 252 | end 253 | end 254 | end 255 | 256 | -- Load the associated generated enchantment records first 257 | if isEnchantable and not tableHelper.isEmpty(enchantmentIdArray) then 258 | local enchantmentStore = RecordStores["enchantment"] 259 | enchantmentStore:LoadRecords(pid, enchantmentStore.data.generatedRecords, enchantmentIdArray, forEveryone) 260 | end 261 | 262 | -- Load our own valid generated records 263 | self:LoadRecords(pid, recordList, validIdArray, forEveryone) 264 | end 265 | 266 | function BaseRecordStore:LoadRecords(pid, recordList, idArray, forEveryone) 267 | 268 | if type(recordList) ~= "table" then return end 269 | if type(idArray) ~= "table" then return end 270 | 271 | tes3mp.ClearRecords() 272 | tes3mp.SetRecordType(enumerations.recordType[string.upper(self.storeType)]) 273 | local recordCount = 0 274 | 275 | for _, recordId in pairs(idArray) do 276 | local record = recordList[recordId] 277 | 278 | if record ~= nil then 279 | packetBuilder.AddRecordByType(recordId, record, self.storeType) 280 | recordCount = recordCount + 1 281 | end 282 | end 283 | 284 | if recordCount > 0 then 285 | tes3mp.SendRecordDynamic(pid, forEveryone, false) 286 | end 287 | end 288 | 289 | -- Check if a record is a perfect match for any of the records whose IDs 290 | -- are contained in an ID array, with optional parameters that allow starting 291 | -- from the end of the idArray and performing a limited number of checks 292 | function BaseRecordStore:GetMatchingRecordId(comparedRecord, recordList, idArray, ignoredKeys, useReverseOrder, maximumChecks) 293 | 294 | if idArray == nil then 295 | return nil 296 | end 297 | 298 | local initialValue, finalValue, increment 299 | 300 | if useReverseOrder then 301 | initialValue = #idArray 302 | increment = -1 303 | finalValue = 1 304 | 305 | if maximumChecks ~= nil then 306 | finalValue = math.max(finalValue, initialValue - maximumChecks + 1) 307 | end 308 | else 309 | initialValue = 1 310 | increment = 1 311 | finalValue = #idArray 312 | 313 | if maximumChecks ~= nil then 314 | finalValue = math.min(finalValue, maximumChecks) 315 | end 316 | end 317 | 318 | for arrayIndex = initialValue, finalValue, increment do 319 | 320 | local recordId = idArray[arrayIndex] 321 | local record = recordList[recordId] 322 | 323 | if record ~= nil and tableHelper.isEqualTo(comparedRecord, record, ignoredKeys) then 324 | return recordId 325 | end 326 | end 327 | 328 | return nil 329 | end 330 | 331 | function BaseRecordStore:SaveGeneratedRecords(recordTable) 332 | 333 | for recordId, record in pairs(recordTable) do 334 | self.data.generatedRecords[recordId] = tableHelper.deepCopy(record) 335 | 336 | -- Retain the quantity in the input table (when applicable) so we can check it 337 | -- elsewhere, but remove it from here 338 | self.data.generatedRecords[recordId].quantity = nil 339 | end 340 | end 341 | 342 | return BaseRecordStore 343 | -------------------------------------------------------------------------------- /scripts/recordstore/json.lua: -------------------------------------------------------------------------------- 1 | require("config") 2 | fileHelper = require("fileHelper") 3 | tableHelper = require("tableHelper") 4 | local BaseRecordStore = require("recordstore.base") 5 | 6 | local RecordStore = class("RecordStore", BaseRecordStore) 7 | 8 | function RecordStore:__init(storeType) 9 | BaseRecordStore.__init(self, storeType) 10 | 11 | -- Ensure filename is valid 12 | self.recordstoreFile = storeType .. ".json" 13 | 14 | if self.hasEntry == nil then 15 | local home = config.dataPath .. "/recordstore/" 16 | local file = io.open(home .. self.recordstoreFile, "r") 17 | if file ~= nil then 18 | io.close() 19 | self.hasEntry = true 20 | else 21 | self.hasEntry = false 22 | end 23 | end 24 | end 25 | 26 | function RecordStore:CreateEntry() 27 | jsonInterface.save("recordstore/" .. self.recordstoreFile, self.data) 28 | self.hasEntry = true 29 | end 30 | 31 | function RecordStore:SaveToDrive() 32 | if self.hasEntry then 33 | jsonInterface.save("recordstore/" .. self.recordstoreFile, self.data, config.recordstoreKeyOrder) 34 | end 35 | end 36 | 37 | function RecordStore:QuicksaveToDrive() 38 | if self.hasEntry then 39 | jsonInterface.quicksave("recordstore/" .. self.recordstoreFile, self.data) 40 | end 41 | end 42 | 43 | function RecordStore:LoadFromDrive() 44 | self.data = jsonInterface.load("recordstore/" .. self.recordstoreFile) 45 | 46 | if self.data == nil then 47 | tes3mp.LogMessage(enumerations.log.ERROR, "recordstore/" .. self.recordstoreFile .. " cannot be read!") 48 | tes3mp.StopServer(2) 49 | else 50 | -- JSON doesn't allow numerical keys, but we use them, so convert 51 | -- all string number keys into numerical keys 52 | tableHelper.fixNumericalKeys(self.data) 53 | end 54 | end 55 | 56 | -- Deprecated functions with confusing names, kept around for backwards compatibility 57 | function RecordStore:Save() 58 | self:SaveToDrive() 59 | end 60 | 61 | function RecordStore:Load() 62 | self:LoadFromDrive() 63 | end 64 | 65 | return RecordStore 66 | -------------------------------------------------------------------------------- /scripts/recordstore/sql.lua: -------------------------------------------------------------------------------- 1 | Database = require("database") 2 | local BaseRecordStore = require("recordstore.base") 3 | 4 | local RecordStore = class("RecordStore", BaseRecordStore) 5 | 6 | function RecordStore:__init() 7 | BaseRecordStore.__init(self) 8 | 9 | if self.hasEntry == nil then 10 | 11 | -- Not implemented yet 12 | end 13 | end 14 | 15 | function RecordStore:CreateEntry() 16 | -- Not implemented yet 17 | end 18 | 19 | function RecordStore:SaveToDrive() 20 | -- Not implemented yet 21 | end 22 | 23 | function RecordStore:LoadFromDrive() 24 | -- Not implemented yet 25 | end 26 | 27 | return RecordStore 28 | -------------------------------------------------------------------------------- /scripts/speechCollections.lua: -------------------------------------------------------------------------------- 1 | local speechCollections = {} 2 | 3 | if tableHelper.containsCaseInsensitiveString(clientDataFiles, "Morrowind.esm") then 4 | 5 | speechCollections["argonian"] = { 6 | default = { 7 | folderPath = "a", 8 | malePrefix = "AM", 9 | femalePrefix = "AF", 10 | maleFiles = { 11 | attack = { count = 15 }, 12 | flee = { count = 5 }, 13 | follower = { count = 3 }, 14 | hello = { count = 139 }, 15 | hit = { count = 16, skip = { 11 } }, 16 | idle = { count = 8 }, 17 | intruder = { count = 9, skip = { 7 }, indexPrefixOverride = "OP" }, 18 | service = { count = 12 }, 19 | thief = { count = 5 } 20 | }, 21 | femaleFiles = { 22 | attack = { count = 17, skip = { 11, 15, 16 } }, 23 | flee = { count = 5 }, 24 | follower = { count = 6 }, 25 | hello = { count = 139 }, 26 | hit = { count = 16 }, 27 | idle = { count = 8 }, 28 | oppose = { count = 8 }, 29 | service = { count = 12 }, 30 | thief = { count = 5 } 31 | } 32 | } 33 | } 34 | speechCollections["breton"] = { 35 | default = { 36 | folderPath = "b", 37 | malePrefix = "BM", 38 | femalePrefix = "BF", 39 | maleFiles = { 40 | attack = { count = 15, skip = { 11 } }, 41 | flee = { count = 5 }, 42 | follower = { count = 6 }, 43 | hello = { count = 138, skip = { 126 } }, 44 | hit = { count = 15 }, 45 | idle = { count = 8 }, 46 | intruder = { count = 9, skip = { 7 }, indexPrefixOverride = "OP" }, 47 | service = { count = 12 }, 48 | thief = { count = 5 } 49 | }, 50 | femaleFiles = { 51 | attack = { count = 15, skip = { 11 } }, 52 | flee = { count = 5 }, 53 | follower = { count = 6 }, 54 | hello = { count = 138 }, 55 | hit = { count = 15 }, 56 | idle = { count = 9 }, 57 | oppose = { count = 8 }, 58 | service = { count = 15 }, 59 | thief = { count = 5 } 60 | } 61 | } 62 | } 63 | speechCollections["dark elf"] = { 64 | default = { 65 | folderPath = "d", 66 | malePrefix = "DM", 67 | femalePrefix = "DF", 68 | maleFiles = { 69 | attack = { count = 14 }, 70 | flee = { count = 6 }, 71 | follower = { count = 4 }, 72 | hello = { count = 233 }, 73 | hit = { count = 14 }, 74 | idle = { count = 9 }, 75 | oppose = { count = 8 }, 76 | service = { count = 52 }, 77 | thief = { count = 5 } 78 | }, 79 | femaleFiles = { 80 | attack = { count = 13 }, 81 | flee = { count = 5 }, 82 | follower = { count = 6 }, 83 | hello = { count = 233 }, 84 | hit = { count = 14, skip = { 7 } }, 85 | idle = { count = 9, skip = { 7, 8 } }, 86 | oppose = { count = 8 }, 87 | service = { count = 51 }, 88 | thief = { count = 3, skip = { 1, 2 } } 89 | } 90 | }, 91 | ord = { 92 | folderPath = "ord", 93 | malePrefix = "ORM", 94 | maleFiles = { 95 | attack = { count = 5 }, 96 | hello = { count = 20 }, 97 | idle = { count = 4 }, 98 | intruder = { count = 2 } 99 | } 100 | } 101 | } 102 | speechCollections["high elf"] = { 103 | default = { 104 | folderPath = "h", 105 | malePrefix = "HM", 106 | femalePrefix = "HF", 107 | maleFiles = { 108 | attack = { count = 15 }, 109 | flee = { count = 5 }, 110 | follower = { count = 6 }, 111 | hello = { count = 138 }, 112 | hit = { count = 15, skip = { 14 } }, 113 | idle = { count = 9 }, 114 | oppose = { count = 8 }, 115 | service = { count = 25 }, 116 | thief = { count = 5 } 117 | }, 118 | femaleFiles = { 119 | attack = { count = 15 }, 120 | flee = { count = 5 }, 121 | follower = { count = 6 }, 122 | hello = { count = 138 }, 123 | hit = { count = 15 }, 124 | idle = { count = 8 }, 125 | oppose = { count = 8 }, 126 | service = { count = 18 }, 127 | thief = { count = 5 } 128 | } 129 | } 130 | } 131 | speechCollections["imperial"] = { 132 | default = { 133 | folderPath = "i", 134 | malePrefix = "IM", 135 | femalePrefix = "IF", 136 | maleFiles = { 137 | attack = { count = 14 }, 138 | flee = { count = 4 }, 139 | follower = { count = 3 }, 140 | hello = { count = 179, skip = { 1, 27, 47, 69, 70, 71, 72, 80, 81, 82, 83, 84, 85, 86, 141 | 100, 101, 102, 103, 104, 105, 106, 107, 128, 129, 143, 144, 145, 171, 173, 174, 176 } }, 142 | hit = { count = 10 }, 143 | idle = { count = 9 }, 144 | oppose = { count = 8 }, 145 | service = { count = 34 }, 146 | thief = { count = 5 }, 147 | uniform = { count = 7 } 148 | }, 149 | femaleFiles = { 150 | attack = { count = 15, skip = { 11 } }, 151 | flee = { count = 5 }, 152 | follower = { count = 6 }, 153 | hello = { count = 173, skip = { 159, 163 } }, 154 | hit = { count = 15 }, 155 | idle = { count = 9 }, 156 | oppose = { count = 8 }, 157 | service = { count = 21 }, 158 | thief = { count = 5 } 159 | } 160 | } 161 | } 162 | speechCollections["khajiit"] = { 163 | default = { 164 | folderPath = "k", 165 | malePrefix = "KM", 166 | femalePrefix = "KF", 167 | maleFiles = { 168 | attack = { count = 15, skip = { 11 } }, 169 | flee = { count = 5 }, 170 | follower = { count = 3 }, 171 | hello = { count = 139 }, 172 | hit = { count = 16 }, 173 | idle = { count = 9 }, 174 | intruder = { count = 9, skip = { 7 }, indexPrefixOverride = "OP" }, 175 | service = { count = 9 }, 176 | thief = { count = 5 } 177 | }, 178 | femaleFiles = { 179 | attack = { count = 15, skip = { 11 } }, 180 | flee = { count = 5 }, 181 | follower = { count = 6 }, 182 | hello = { count = 139 }, 183 | hit = { count = 16 }, 184 | idle = { count = 9 }, 185 | oppose = { count = 8 }, 186 | service = { count = 12 }, 187 | thief = { count = 5 } 188 | } 189 | } 190 | } 191 | speechCollections["nord"] = { 192 | default = { 193 | folderPath = "n", 194 | malePrefix = "NM", 195 | femalePrefix = "NF", 196 | maleFiles = { 197 | attack = { count = 20, skip = { 14, 15, 16, 17, 18, 19 } }, 198 | flee = { count = 5 }, 199 | follower = { count = 4 }, 200 | hello = { count = 138 }, 201 | hit = { count = 14 }, 202 | idle = { count = 9 }, 203 | intruder = { count = 9, skip = { 7 }, indexPrefixOverride = "OP" }, 204 | service = { count = 6 }, 205 | thief = { count = 5 } 206 | }, 207 | femaleFiles = { 208 | attack = { count = 15, skip = { 11 } }, 209 | flee = { count = 5 }, 210 | follower = { count = 6 }, 211 | hello = { count = 138 }, 212 | hit = { count = 15 }, 213 | idle = { count = 9 }, 214 | oppose = { count = 8 }, 215 | service = { count = 11 }, 216 | thief = { count = 5 } 217 | } 218 | } 219 | } 220 | speechCollections["orc"] = { 221 | default = { 222 | folderPath = "o", 223 | malePrefix = "OM", 224 | femalePrefix = "OF", 225 | maleFiles = { 226 | attack = { count = 15 }, 227 | flee = { count = 5 }, 228 | follower = { count = 6 }, 229 | hello = { count = 138 }, 230 | hit = { count = 15 }, 231 | idle = { count = 9 }, 232 | oppose = { count = 8 }, 233 | service = { count = 12 }, 234 | thief = { count = 5 } 235 | }, 236 | femaleFiles = { 237 | attack = { count = 15 }, 238 | flee = { count = 5 }, 239 | follower = { count = 6 }, 240 | hello = { count = 138 }, 241 | hit = { count = 21 }, 242 | idle = { count = 9 }, 243 | oppose = { count = 8 }, 244 | service = { count = 3 }, 245 | thief = { count = 5 } 246 | } 247 | } 248 | } 249 | speechCollections["redguard"] = { 250 | default = { 251 | folderPath = "r", 252 | malePrefix = "RM", 253 | femalePrefix = "RF", 254 | maleFiles = { 255 | attack = { count = 18 }, 256 | flee = { count = 5 }, 257 | follower = { count = 3 }, 258 | hello = { count = 138 }, 259 | hit = { count = 15 }, 260 | idle = { count = 9 }, 261 | intruder = { count = 9, skip = { 7 }, indexPrefixOverride = "OP" }, 262 | service = { count = 12 }, 263 | thief = { count = 5 } 264 | }, 265 | femaleFiles = { 266 | attack = { count = 15, skip = { 1, 11 } }, 267 | flee = { count = 5 }, 268 | follower = { count = 6 }, 269 | hello = { count = 138 }, 270 | hit = { count = 14 }, 271 | idle = { count = 9 }, 272 | oppose = { count = 8 }, 273 | service = { count = 6 }, 274 | thief = { count = 5 } 275 | } 276 | } 277 | } 278 | speechCollections["wood elf"] = { 279 | default = { 280 | folderPath = "w", 281 | malePrefix = "WM", 282 | femalePrefix = "WF", 283 | maleFiles = { 284 | attack = { count = 18, skip = { 14, 15, 16, 17 } }, 285 | flee = { count = 5 }, 286 | follower = { count = 3 }, 287 | hello = { count = 138 }, 288 | hit = { count = 15 }, 289 | idle = { count = 9 }, 290 | intruder = { count = 9, skip = { 7 }, indexPrefixOverride = "OP" }, 291 | service = { count = 6 }, 292 | thief = { count = 5 } 293 | }, 294 | femaleFiles = { 295 | attack = { count = 14 }, 296 | flee = { count = 5 }, 297 | follower = { count = 6 }, 298 | hello = { count = 138 }, 299 | hit = { count = 15 }, 300 | idle = { count = 9 }, 301 | oppose = { count = 8 }, 302 | service = { count = 9 }, 303 | thief = { count = 5 } 304 | } 305 | } 306 | } 307 | 308 | if tableHelper.containsCaseInsensitiveString(clientDataFiles, "Tribunal.esm") then 309 | 310 | speechCollections["dark elf"]["tb"] = { 311 | folderPath = "d", 312 | malePrefix = "DM", 313 | femalePrefix = "DF", 314 | maleFiles = { 315 | hello = { count = 200, filePrefixOverride = "tHlo" }, 316 | idle = { count = 24, filePrefixOverride = "tIdl" } 317 | }, 318 | femaleFiles = { 319 | hello = { count = 173, filePrefixOverride = "tHlo" }, 320 | idle = { count = 17, filePrefixOverride = "tIdl" } 321 | } 322 | } 323 | 324 | speechCollections["imperial"]["tb"] = { 325 | folderPath = "i", 326 | malePrefix = "IM", 327 | femalePrefix = "IF", 328 | maleFiles = { 329 | hello = { count = 116, filePrefixOverride = "tHlo" }, 330 | idle = { count = 13, filePrefixOverride = "tIdl" } 331 | }, 332 | femaleFiles = { 333 | hello = { count = 112, filePrefixOverride = "tHlo" }, 334 | idle = { count = 13, filePrefixOverride = "tIdl" } 335 | } 336 | } 337 | end 338 | 339 | if tableHelper.containsCaseInsensitiveString(clientDataFiles, "Bloodmoon.esm") then 340 | 341 | speechCollections["dark elf"]["bm"] = { 342 | folderPath = "d", 343 | malePrefix = "DM", 344 | femalePrefix = "DF", 345 | maleFiles = { 346 | attack = { count = 6, filePrefixOverride = "bAtk" }, 347 | flee = { count = 4, filePrefixOverride = "bFle" }, 348 | hello = { count = 7, filePrefixOverride = "bHlo" }, 349 | idle = { count = 14, skip = { 1 }, filePrefixOverride = "bIdl" } 350 | }, 351 | femaleFiles = { 352 | attack = { count = 6, filePrefixOverride = "bAtk" }, 353 | flee = { count = 4, filePrefixOverride = "bFle" }, 354 | hello = { count = 1, filePrefixOverride = "bHlo" }, 355 | idle = { count = 15, skip = { 7, 8 }, filePrefixOverride = "bIdl" } 356 | } 357 | } 358 | 359 | speechCollections["imperial"]["bm"] = { 360 | folderPath = "i", 361 | malePrefix = "IM", 362 | femalePrefix = "IF", 363 | maleFiles = { 364 | attack = { count = 9, filePrefixOverride = "bAtk" }, 365 | flee = { count = 4, filePrefixOverride = "bFle" }, 366 | hello = { count = 52, filePrefixOverride = "bHlo" }, 367 | idle = { count = 41, skip = { 16 }, filePrefixOverride = "bIdl" } 368 | }, 369 | femaleFiles = { 370 | attack = { count = 8, filePrefixOverride = "bAtk" }, 371 | flee = { count = 4, filePrefixOverride = "bFle" }, 372 | hello = { count = 17, skip = { 8, 9, 10 }, filePrefixOverride = "bHlo" }, 373 | idle = { count = 13, filePrefixOverride = "bIdl" } 374 | } 375 | } 376 | 377 | speechCollections["nord"]["bm"] = { 378 | folderPath = "n", 379 | malePrefix = "NM", 380 | femalePrefix = "NF", 381 | maleFiles = { 382 | attack = { count = 9, filePrefixOverride = "bAtk" }, 383 | flee = { count = 4, filePrefixOverride = "bFle" }, 384 | hello = { count = 75, filePrefixOverride = "bHlo" }, 385 | idle = { count = 37, filePrefixOverride = "bIdl" } 386 | }, 387 | femaleFiles = { 388 | attack = { count = 9, filePrefixOverride = "bAtk" }, 389 | flee = { count = 4, filePrefixOverride = "bFle" }, 390 | hello = { count = 21, skip = { 8, 9 }, filePrefixOverride = "bHlo" }, 391 | idle = { count = 23, skip = { 21 }, filePrefixOverride = "bIdl" } 392 | } 393 | } 394 | end 395 | end 396 | 397 | return speechCollections 398 | -------------------------------------------------------------------------------- /scripts/speechHelper.lua: -------------------------------------------------------------------------------- 1 | tableHelper = require("tableHelper") 2 | require("utils") 3 | 4 | local speechHelper = {} 5 | 6 | local speechTypesToFilePrefixes = { attack = "Atk", flee = "Fle", follower = "Flw", hello = "Hlo", hit = "Hit", 7 | idle = "Idl", intruder = "int", oppose = "OP", service = "Srv", thief = "Thf", uniform = "uni" } 8 | 9 | function speechHelper.GetSpeechPathFromCollection(speechCollectionTable, speechType, speechIndex, gender) 10 | 11 | if speechCollectionTable == nil or speechTypesToFilePrefixes[speechType] == nil then 12 | return nil 13 | end 14 | 15 | local genderTableName 16 | 17 | if gender == 0 then genderTableName = "femaleFiles" 18 | else genderTableName = "maleFiles" end 19 | 20 | local speechTypeTable = speechCollectionTable[genderTableName][speechType] 21 | 22 | if speechTypeTable == nil then 23 | return nil 24 | else 25 | if speechIndex > speechTypeTable.count then 26 | return nil 27 | elseif speechTypeTable.skip ~= nil and tableHelper.containsValue(speechTypeTable.skip, speechIndex) then 28 | return nil 29 | end 30 | end 31 | 32 | local speechPath = "Vo\\" .. speechCollectionTable.folderPath .. "\\" 33 | 34 | -- Assume there are only going to be subfolders for different genders if there are actually 35 | -- speech files for both genders 36 | if speechCollectionTable.maleFiles ~= nil and speechCollectionTable.femaleFiles ~= nil then 37 | if gender == 0 then 38 | speechPath = speechPath .. "f\\" 39 | else 40 | speechPath = speechPath .. "m\\" 41 | end 42 | end 43 | 44 | local filePrefix 45 | 46 | if speechTypeTable.filePrefixOverride ~= nil then 47 | filePrefix = speechTypeTable.filePrefixOverride 48 | else 49 | filePrefix = speechTypesToFilePrefixes[speechType] 50 | end 51 | 52 | local indexPrefix 53 | 54 | if speechTypeTable.indexPrefixOverride ~= nil then 55 | indexPrefix = speechTypeTable.indexPrefixOverride 56 | elseif gender == 0 then 57 | indexPrefix = speechCollectionTable.femalePrefix 58 | else 59 | indexPrefix = speechCollectionTable.malePrefix 60 | end 61 | 62 | speechPath = speechPath .. filePrefix .. "_" .. indexPrefix .. prefixZeroes(speechIndex, 3) .. ".mp3" 63 | 64 | return speechPath 65 | end 66 | 67 | function speechHelper.GetSpeechPath(pid, speechInput, speechIndex) 68 | 69 | local speechCollectionKey 70 | local speechType 71 | 72 | -- Is there a specific folder at the start of the speechInput? If so, 73 | -- get the speechCollectionKey from it 74 | local underscoreIndex = string.find(speechInput, "_") 75 | 76 | if underscoreIndex ~= nil and underscoreIndex > 1 then 77 | speechCollectionKey = string.sub(speechInput, 1, underscoreIndex - 1) 78 | speechType = string.sub(speechInput, underscoreIndex + 1) 79 | else 80 | speechCollectionKey = "default" 81 | speechType = speechInput 82 | end 83 | 84 | local race = string.lower(Players[pid].data.character.race) 85 | local speechCollectionTable = speechCollections[race][speechCollectionKey] 86 | 87 | if speechCollectionTable ~= nil then 88 | 89 | local gender = Players[pid].data.character.gender 90 | 91 | return speechHelper.GetSpeechPathFromCollection(speechCollectionTable, speechType, speechIndex, gender) 92 | else 93 | return nil 94 | end 95 | end 96 | 97 | function speechHelper.GetPrintableValidListForSpeechCollection(speechCollectionTable, gender, collectionPrefix) 98 | 99 | local validList = {} 100 | local genderTableName 101 | 102 | if gender == 0 then 103 | genderTableName = "femaleFiles" 104 | else 105 | genderTableName = "maleFiles" 106 | end 107 | 108 | if speechCollectionTable[genderTableName] ~= nil then 109 | for speechType, typeDetails in pairs(speechCollectionTable[genderTableName]) do 110 | local validInput = "" 111 | 112 | if collectionPrefix then 113 | validInput = collectionPrefix 114 | end 115 | 116 | validInput = validInput .. speechType .. " 1-" .. typeDetails.count 117 | 118 | if typeDetails.skip ~= nil then 119 | validInput = validInput .. " (except " 120 | validInput = validInput .. tableHelper.concatenateFromIndex(typeDetails.skip, 1, ", ") .. ")" 121 | end 122 | 123 | table.insert(validList, validInput) 124 | end 125 | end 126 | 127 | return validList 128 | end 129 | 130 | function speechHelper.GetPrintableValidListForPid(pid) 131 | 132 | local validList = {} 133 | 134 | local race = string.lower(Players[pid].data.character.race) 135 | local gender = Players[pid].data.character.gender 136 | 137 | -- Print the default speech options first 138 | if speechCollections[race].default ~= nil then 139 | validList = speechHelper.GetPrintableValidListForSpeechCollection(speechCollections[race].default, gender) 140 | end 141 | 142 | for speechCollectionKey, speechCollectionTable in pairs(speechCollections[race]) do 143 | if speechCollectionKey ~= "default" then 144 | tableHelper.insertValues(validList, speechHelper.GetPrintableValidListForSpeechCollection(speechCollectionTable, gender, speechCollectionKey .. "_")) 145 | end 146 | end 147 | 148 | return tableHelper.concatenateFromIndex(validList, 1, ", ") 149 | end 150 | 151 | function speechHelper.PlaySpeech(pid, speechInput, speechIndex) 152 | 153 | local speechPath = speechHelper.GetSpeechPath(pid, speechInput, speechIndex) 154 | 155 | if speechPath ~= nil then 156 | tes3mp.PlaySpeech(pid, speechPath) 157 | return true 158 | end 159 | 160 | return false 161 | end 162 | 163 | return speechHelper 164 | -------------------------------------------------------------------------------- /scripts/stateHelper.lua: -------------------------------------------------------------------------------- 1 | StateHelper = class("StateHelper") 2 | 3 | function StateHelper:LoadJournal(pid, stateObject) 4 | 5 | if stateObject.data.journal == nil then 6 | stateObject.data.journal = {} 7 | end 8 | 9 | tes3mp.ClearJournalChanges(pid) 10 | 11 | for index, journalItem in pairs(stateObject.data.journal) do 12 | 13 | if journalItem.type == enumerations.journal.ENTRY then 14 | 15 | if journalItem.actorRefId == nil then 16 | journalItem.actorRefId = "player" 17 | end 18 | 19 | if journalItem.timestamp ~= nil then 20 | tes3mp.AddJournalEntryWithTimestamp(pid, journalItem.quest, journalItem.index, journalItem.actorRefId, 21 | journalItem.timestamp.daysPassed, journalItem.timestamp.month, journalItem.timestamp.day) 22 | else 23 | tes3mp.AddJournalEntry(pid, journalItem.quest, journalItem.index, journalItem.actorRefId) 24 | end 25 | else 26 | tes3mp.AddJournalIndex(pid, journalItem.quest, journalItem.index) 27 | end 28 | end 29 | 30 | tes3mp.SendJournalChanges(pid) 31 | end 32 | 33 | function StateHelper:LoadFactionRanks(pid, stateObject) 34 | 35 | if stateObject.data.factionRanks == nil then 36 | stateObject.data.factionRanks = {} 37 | end 38 | 39 | tes3mp.ClearFactionChanges(pid) 40 | tes3mp.SetFactionChangesAction(pid, enumerations.faction.RANK) 41 | 42 | for factionId, rank in pairs(stateObject.data.factionRanks) do 43 | 44 | tes3mp.SetFactionId(factionId) 45 | tes3mp.SetFactionRank(rank) 46 | tes3mp.AddFaction(pid) 47 | end 48 | 49 | tes3mp.SendFactionChanges(pid) 50 | end 51 | 52 | function StateHelper:LoadFactionExpulsion(pid, stateObject) 53 | 54 | if stateObject.data.factionExpulsion == nil then 55 | stateObject.data.factionExpulsion = {} 56 | end 57 | 58 | tes3mp.ClearFactionChanges(pid) 59 | tes3mp.SetFactionChangesAction(pid, enumerations.faction.EXPULSION) 60 | 61 | for factionId, state in pairs(stateObject.data.factionExpulsion) do 62 | 63 | tes3mp.SetFactionId(factionId) 64 | tes3mp.SetFactionExpulsionState(state) 65 | tes3mp.AddFaction(pid) 66 | end 67 | 68 | tes3mp.SendFactionChanges(pid) 69 | end 70 | 71 | function StateHelper:LoadFactionReputation(pid, stateObject) 72 | 73 | if stateObject.data.factionReputation == nil then 74 | stateObject.data.factionReputation = {} 75 | end 76 | 77 | tes3mp.ClearFactionChanges(pid) 78 | tes3mp.SetFactionChangesAction(pid, enumerations.faction.REPUTATION) 79 | 80 | for factionId, reputation in pairs(stateObject.data.factionReputation) do 81 | 82 | tes3mp.SetFactionId(factionId) 83 | tes3mp.SetFactionReputation(reputation) 84 | tes3mp.AddFaction(pid) 85 | end 86 | 87 | tes3mp.SendFactionChanges(pid) 88 | end 89 | 90 | function StateHelper:LoadTopics(pid, stateObject) 91 | 92 | if stateObject.data.topics == nil then 93 | stateObject.data.topics = {} 94 | end 95 | 96 | tes3mp.ClearTopicChanges(pid) 97 | 98 | for index, topicId in pairs(stateObject.data.topics) do 99 | 100 | tes3mp.AddTopic(pid, topicId) 101 | end 102 | 103 | tes3mp.SendTopicChanges(pid) 104 | end 105 | 106 | function StateHelper:LoadBounty(pid, stateObject) 107 | 108 | if stateObject.data.fame == nil then 109 | stateObject.data.fame = { bounty = 0, reputation = 0 } 110 | elseif stateObject.data.fame.bounty == nil then 111 | stateObject.data.fame.bounty = 0 112 | end 113 | 114 | -- Update old player files to the new format 115 | if stateObject.data.stats ~= nil and stateObject.data.stats.bounty ~= nil then 116 | stateObject.data.fame.bounty = stateObject.data.stats.bounty 117 | stateObject.data.stats.bounty = nil 118 | end 119 | 120 | tes3mp.SetBounty(pid, stateObject.data.fame.bounty) 121 | tes3mp.SendBounty(pid) 122 | end 123 | 124 | function StateHelper:LoadReputation(pid, stateObject) 125 | 126 | if stateObject.data.fame == nil then 127 | stateObject.data.fame = { bounty = 0, reputation = 0 } 128 | elseif stateObject.data.fame.reputation == nil then 129 | stateObject.data.fame.reputation = 0 130 | end 131 | 132 | tes3mp.SetReputation(pid, stateObject.data.fame.reputation) 133 | tes3mp.SendReputation(pid) 134 | end 135 | 136 | function StateHelper:LoadClientScriptVariables(pid, stateObject) 137 | 138 | if stateObject.data.clientVariables == nil then 139 | stateObject.data.clientVariables = {} 140 | end 141 | 142 | if stateObject.data.clientVariables.globals == nil then 143 | stateObject.data.clientVariables.globals = {} 144 | end 145 | 146 | local variableCount = 0 147 | 148 | tes3mp.ClearClientGlobals() 149 | 150 | for variableId, variableTable in pairs(stateObject.data.clientVariables.globals) do 151 | 152 | if type(variableTable) == "table" then 153 | 154 | if variableTable.variableType == enumerations.variableType.SHORT then 155 | tes3mp.AddClientGlobalInteger(variableId, variableTable.intValue, enumerations.variableType.SHORT) 156 | elseif variableTable.variableType == enumerations.variableType.LONG then 157 | tes3mp.AddClientGlobalInteger(variableId, variableTable.intValue, enumerations.variableType.LONG) 158 | elseif variableTable.variableType == enumerations.variableType.FLOAT then 159 | tes3mp.AddClientGlobalFloat(variableId, variableTable.floatValue) 160 | end 161 | 162 | variableCount = variableCount + 1 163 | end 164 | end 165 | 166 | if variableCount > 0 then 167 | tes3mp.SendClientScriptGlobal(pid) 168 | end 169 | end 170 | 171 | function StateHelper:LoadDestinationOverrides(pid, stateObject) 172 | 173 | if stateObject.data.destinationOverrides == nil then 174 | stateObject.data.destinationOverrides = {} 175 | end 176 | 177 | local destinationCount = 0 178 | 179 | tes3mp.ClearDestinationOverrides() 180 | 181 | for oldCellDescription, newCellDescription in pairs(stateObject.data.destinationOverrides) do 182 | 183 | tes3mp.AddDestinationOverride(oldCellDescription, newCellDescription) 184 | destinationCount = destinationCount + 1 185 | end 186 | 187 | if destinationCount > 0 then 188 | tes3mp.SendWorldDestinationOverride(pid) 189 | end 190 | end 191 | 192 | function StateHelper:LoadMap(pid, stateObject) 193 | 194 | if stateObject.data.mapExplored == nil then 195 | stateObject.data.mapExplored = {} 196 | end 197 | 198 | local tileCount = 0 199 | tes3mp.ClearMapChanges() 200 | 201 | for index, cellDescription in pairs(stateObject.data.mapExplored) do 202 | 203 | local filePath = config.dataPath .. "/map/" .. cellDescription .. ".png" 204 | 205 | if tes3mp.DoesFilePathExist(filePath) then 206 | 207 | local cellX, cellY 208 | _, _, cellX, cellY = string.find(cellDescription, patterns.exteriorCell) 209 | cellX = tonumber(cellX) 210 | cellY = tonumber(cellY) 211 | 212 | if type(cellX) == "number" and type(cellY) == "number" then 213 | tes3mp.LoadMapTileImageFile(cellX, cellY, filePath) 214 | tileCount = tileCount + 1 215 | end 216 | end 217 | end 218 | 219 | if tileCount > 0 then 220 | tes3mp.SendWorldMap(pid) 221 | end 222 | end 223 | 224 | function StateHelper:SaveJournal(stateObject, playerPacket) 225 | 226 | if stateObject.data.journal == nil then 227 | stateObject.data.journal = {} 228 | end 229 | 230 | if stateObject.data.customVariables == nil then 231 | stateObject.data.customVariables = {} 232 | end 233 | 234 | for _, journalItem in ipairs(playerPacket.journal) do 235 | 236 | table.insert(stateObject.data.journal, journalItem) 237 | 238 | if journalItem.quest == "a1_1_findspymaster" and journalItem.index >= 14 then 239 | stateObject.data.customVariables.deliveredCaiusPackage = true 240 | end 241 | end 242 | 243 | stateObject:QuicksaveToDrive() 244 | end 245 | 246 | function StateHelper:SaveFactionRanks(pid, stateObject) 247 | 248 | if stateObject.data.factionRanks == nil then 249 | stateObject.data.factionRanks = {} 250 | end 251 | 252 | for i = 0, tes3mp.GetFactionChangesSize(pid) - 1 do 253 | 254 | local factionId = tes3mp.GetFactionId(pid, i) 255 | stateObject.data.factionRanks[factionId] = tes3mp.GetFactionRank(pid, i) 256 | end 257 | 258 | stateObject:QuicksaveToDrive() 259 | end 260 | 261 | function StateHelper:SaveFactionExpulsion(pid, stateObject) 262 | 263 | if stateObject.data.factionExpulsion == nil then 264 | stateObject.data.factionExpulsion = {} 265 | end 266 | 267 | for i = 0, tes3mp.GetFactionChangesSize(pid) - 1 do 268 | 269 | local factionId = tes3mp.GetFactionId(pid, i) 270 | stateObject.data.factionExpulsion[factionId] = tes3mp.GetFactionExpulsionState(pid, i) 271 | end 272 | 273 | stateObject:QuicksaveToDrive() 274 | end 275 | 276 | function StateHelper:SaveFactionReputation(pid, stateObject) 277 | 278 | if stateObject.data.factionReputation == nil then 279 | stateObject.data.factionReputation = {} 280 | end 281 | 282 | for i = 0, tes3mp.GetFactionChangesSize(pid) - 1 do 283 | 284 | local factionId = tes3mp.GetFactionId(pid, i) 285 | stateObject.data.factionReputation[factionId] = tes3mp.GetFactionReputation(pid, i) 286 | end 287 | 288 | stateObject:QuicksaveToDrive() 289 | end 290 | 291 | function StateHelper:SaveTopics(pid, stateObject) 292 | 293 | if stateObject.data.topics == nil then 294 | stateObject.data.topics = {} 295 | end 296 | 297 | for i = 0, tes3mp.GetTopicChangesSize(pid) - 1 do 298 | 299 | local topicId = tes3mp.GetTopicId(pid, i) 300 | 301 | if not tableHelper.containsValue(stateObject.data.topics, topicId) then 302 | table.insert(stateObject.data.topics, topicId) 303 | end 304 | end 305 | 306 | stateObject:QuicksaveToDrive() 307 | end 308 | 309 | function StateHelper:SaveBounty(pid, stateObject) 310 | 311 | if stateObject.data.fame == nil then 312 | stateObject.data.fame = {} 313 | end 314 | 315 | stateObject.data.fame.bounty = tes3mp.GetBounty(pid) 316 | 317 | stateObject:QuicksaveToDrive() 318 | end 319 | 320 | function StateHelper:SaveReputation(pid, stateObject) 321 | 322 | if stateObject.data.fame == nil then 323 | stateObject.data.fame = {} 324 | end 325 | 326 | stateObject.data.fame.reputation = tes3mp.GetReputation(pid) 327 | 328 | stateObject:QuicksaveToDrive() 329 | end 330 | 331 | function StateHelper:SaveClientScriptGlobal(stateObject, variables) 332 | 333 | if stateObject.data.clientVariables == nil then 334 | stateObject.data.clientVariables = {} 335 | end 336 | 337 | if stateObject.data.clientVariables.globals == nil then 338 | stateObject.data.clientVariables.globals = {} 339 | end 340 | 341 | for id, variable in pairs (variables) do 342 | stateObject.data.clientVariables.globals[id] = variable 343 | end 344 | 345 | stateObject:QuicksaveToDrive() 346 | end 347 | 348 | function StateHelper:SaveMapExploration(pid, stateObject) 349 | 350 | local cell = tes3mp.GetCell(pid) 351 | 352 | if tes3mp.IsInExterior(pid) == true then 353 | if not tableHelper.containsValue(stateObject.data.mapExplored, cell) then 354 | table.insert(stateObject.data.mapExplored, cell) 355 | end 356 | end 357 | end 358 | 359 | return StateHelper 360 | -------------------------------------------------------------------------------- /scripts/world/base.lua: -------------------------------------------------------------------------------- 1 | stateHelper = require("stateHelper") 2 | local BaseWorld = class("BaseWorld") 3 | 4 | -- Keep this here because it's required in mathematical operations 5 | BaseWorld.defaultTimeScale = 30 6 | 7 | BaseWorld.monthLengths = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } 8 | 9 | BaseWorld.storedRegions = {} 10 | 11 | function BaseWorld:__init() 12 | 13 | self.coreVariables = 14 | { 15 | currentMpNum = 0, 16 | hasRunStartupScripts = false 17 | } 18 | 19 | self.data = 20 | { 21 | fame = { 22 | bounty = 0, 23 | reputation = 0 24 | }, 25 | journal = {}, 26 | factionRanks = {}, 27 | factionExpulsion = {}, 28 | factionReputation = {}, 29 | topics = {}, 30 | kills = {}, 31 | time = config.defaultTimeTable, 32 | mapExplored = {}, 33 | destinationOverrides = {}, 34 | customVariables = {} 35 | } 36 | end 37 | 38 | function BaseWorld:HasEntry() 39 | return self.hasEntry 40 | end 41 | 42 | function BaseWorld:EnsureCoreVariablesExist() 43 | 44 | if self.coreVariables == nil then 45 | self.coreVariables = {} 46 | end 47 | 48 | if self.coreVariables.currentMpNum == nil then 49 | if self.data.general.currentMpNum ~= nil then 50 | self.coreVariables.currentMpNum = self.data.general.currentMpNum 51 | self.data.general.currentMpNum = nil 52 | else 53 | self.coreVariables.currentMpNum = 0 54 | end 55 | end 56 | 57 | if self.coreVariables.hasRunStartupScripts == nil then 58 | self.coreVariables.hasRunStartupScripts = false 59 | end 60 | end 61 | 62 | function BaseWorld:EnsureTimeDataExists() 63 | 64 | if self.data.time == nil then 65 | self.data.time = config.defaultTimeTable 66 | end 67 | end 68 | 69 | function BaseWorld:HasRunStartupScripts() 70 | return self.coreVariables.hasRunStartupScripts 71 | end 72 | 73 | function BaseWorld:GetRegionVisitorCount(regionName) 74 | 75 | if self.storedRegions[regionName] == nil then return 0 end 76 | 77 | return tableHelper.getCount(self.storedRegions[regionName].visitors) 78 | end 79 | 80 | function BaseWorld:AddRegionVisitor(pid, regionName) 81 | 82 | if self.storedRegions[regionName] == nil then 83 | self.storedRegions[regionName] = { visitors = {}, forcedWeatherUpdatePids = {} } 84 | end 85 | 86 | -- Only add new visitor if we don't already have them 87 | if not tableHelper.containsValue(self.storedRegions[regionName].visitors, pid) then 88 | table.insert(self.storedRegions[regionName].visitors, pid) 89 | end 90 | end 91 | 92 | function BaseWorld:RemoveRegionVisitor(pid, regionName) 93 | 94 | local loadedRegion = self.storedRegions[regionName] 95 | 96 | -- Only remove visitor if they are actually recorded as one 97 | if tableHelper.containsValue(loadedRegion.visitors, pid) then 98 | tableHelper.removeValue(loadedRegion.visitors, pid) 99 | end 100 | 101 | -- Additionally, remove the visitor from the forcedWeatherUpdatePids if they 102 | -- are still in there 103 | self:RemoveForcedWeatherUpdatePid(pid, regionName) 104 | end 105 | 106 | function BaseWorld:AddForcedWeatherUpdatePid(pid, regionName) 107 | 108 | local loadedRegion = self.storedRegions[regionName] 109 | table.insert(loadedRegion.forcedWeatherUpdatePids, pid) 110 | end 111 | 112 | function BaseWorld:RemoveForcedWeatherUpdatePid(pid, regionName) 113 | 114 | local loadedRegion = self.storedRegions[regionName] 115 | tableHelper.removeValue(loadedRegion.forcedWeatherUpdatePids, pid) 116 | end 117 | 118 | function BaseWorld:IsForcedWeatherUpdatePid(pid, regionName) 119 | 120 | local loadedRegion = self.storedRegions[regionName] 121 | 122 | if tableHelper.containsValue(loadedRegion.forcedWeatherUpdatePids, pid) then 123 | return true 124 | end 125 | 126 | return false 127 | end 128 | 129 | function BaseWorld:GetRegionAuthority(regionName) 130 | 131 | if self.storedRegions[regionName] ~= nil then 132 | return self.storedRegions[regionName].authority 133 | end 134 | 135 | return nil 136 | end 137 | 138 | function BaseWorld:SetRegionAuthority(pid, regionName) 139 | 140 | self.storedRegions[regionName].authority = pid 141 | tes3mp.LogMessage(enumerations.log.INFO, "Authority of region " .. regionName .. " is now " .. 142 | logicHandler.GetChatName(pid)) 143 | 144 | tes3mp.SetAuthorityRegion(regionName) 145 | tes3mp.SendWorldRegionAuthority(pid) 146 | end 147 | 148 | function BaseWorld:IncrementDay() 149 | 150 | self.data.time.daysPassed = self.data.time.daysPassed + 1 151 | 152 | local day = self.data.time.day 153 | local month = self.data.time.month 154 | 155 | -- Is the new day higher than the number of days in the current month? 156 | if day + 1 > (self.monthLengths[month] or 0) then 157 | 158 | -- Is the new month higher than the number of months in a year? 159 | if month + 1 > 12 then 160 | self.data.time.year = self.data.time.year + 1 161 | self.data.time.month = 1 162 | else 163 | self.data.time.month = math.max(1, month + 1) 164 | end 165 | 166 | self.data.time.day = 1 167 | else 168 | 169 | self.data.time.day = day + 1 170 | end 171 | end 172 | 173 | function BaseWorld:GetCurrentTimeScale() 174 | 175 | if self.data.time.dayTimeScale == nil then self.data.time.dayTimeScale = self.defaultTimeScale end 176 | if self.data.time.nightTimeScale == nil then self.data.time.nightTimeScale = self.defaultTimeScale end 177 | 178 | if self.data.time.hour >= config.nightStartHour or self.data.time.hour <= config.nightEndHour then 179 | return self.data.time.nightTimeScale 180 | else 181 | return self.data.time.dayTimeScale 182 | end 183 | end 184 | 185 | function BaseWorld:UpdateFrametimeMultiplier() 186 | self.frametimeMultiplier = WorldInstance:GetCurrentTimeScale() / WorldInstance.defaultTimeScale 187 | end 188 | 189 | function BaseWorld:GetCurrentMpNum() 190 | return self.coreVariables.currentMpNum 191 | end 192 | 193 | function BaseWorld:SetCurrentMpNum(currentMpNum) 194 | self.coreVariables.currentMpNum = currentMpNum 195 | self:QuicksaveCoreVariablesToDrive() 196 | end 197 | 198 | function BaseWorld:LoadJournal(pid) 199 | stateHelper:LoadJournal(pid, self) 200 | end 201 | 202 | function BaseWorld:LoadFactionRanks(pid) 203 | stateHelper:LoadFactionRanks(pid, self) 204 | end 205 | 206 | function BaseWorld:LoadFactionExpulsion(pid) 207 | stateHelper:LoadFactionExpulsion(pid, self) 208 | end 209 | 210 | function BaseWorld:LoadFactionReputation(pid) 211 | stateHelper:LoadFactionReputation(pid, self) 212 | end 213 | 214 | function BaseWorld:LoadTopics(pid) 215 | stateHelper:LoadTopics(pid, self) 216 | end 217 | 218 | function BaseWorld:LoadBounty(pid) 219 | stateHelper:LoadBounty(pid, self) 220 | end 221 | 222 | function BaseWorld:LoadReputation(pid) 223 | stateHelper:LoadReputation(pid, self) 224 | end 225 | 226 | function BaseWorld:LoadClientScriptVariables(pid) 227 | stateHelper:LoadClientScriptVariables(pid, self) 228 | end 229 | 230 | function BaseWorld:LoadDestinationOverrides(pid) 231 | stateHelper:LoadDestinationOverrides(pid, self) 232 | end 233 | 234 | function BaseWorld:LoadMap(pid) 235 | stateHelper:LoadMap(pid, self) 236 | end 237 | 238 | function BaseWorld:LoadKills(pid, forEveryone) 239 | 240 | tes3mp.ClearKillChanges() 241 | 242 | for refId, killCount in pairs(self.data.kills) do 243 | 244 | tes3mp.AddKill(refId, killCount) 245 | end 246 | 247 | tes3mp.SendWorldKillCount(pid, forEveryone) 248 | end 249 | 250 | function BaseWorld:LoadRegionWeather(regionName, pid, forEveryone, forceState) 251 | 252 | local region = self.storedRegions[regionName] 253 | 254 | if region.currentWeather ~= nil then 255 | 256 | tes3mp.SetWeatherRegion(regionName) 257 | tes3mp.SetWeatherCurrent(region.currentWeather) 258 | tes3mp.SetWeatherNext(region.nextWeather) 259 | tes3mp.SetWeatherQueued(region.queuedWeather) 260 | tes3mp.SetWeatherTransitionFactor(region.transitionFactor) 261 | tes3mp.SetWeatherForceState(forceState) 262 | tes3mp.SendWorldWeather(pid, forEveryone) 263 | else 264 | tes3mp.LogMessage(enumerations.log.INFO, "Could not load weather in region " .. regionName .. 265 | " for " .. logicHandler.GetChatName(pid) .. " because we have no weather information for it") 266 | end 267 | end 268 | 269 | function BaseWorld:LoadWeather(pid, forEveryone, forceState) 270 | 271 | for regionName, region in pairs(self.storedRegions) do 272 | 273 | if region.currentWeather ~= nil then 274 | self:LoadRegionWeather(regionName, pid, forEveryone, forceState) 275 | end 276 | end 277 | end 278 | 279 | function BaseWorld:LoadTime(pid, forEveryone) 280 | 281 | tes3mp.SetHour(self.data.time.hour) 282 | tes3mp.SetDay(self.data.time.day) 283 | 284 | if self.data.time.month < 1 then 285 | self.data.time.month = 1 286 | elseif self.data.time.month > 12 then 287 | self.data.time.month = 12 288 | end 289 | 290 | -- The first month has an index of 0 in the C++ code, but 291 | -- table values should be intuitive and range from 1 to 12, 292 | -- so adjust for that by just going down by 1 293 | tes3mp.SetMonth(self.data.time.month - 1) 294 | 295 | tes3mp.SetYear(self.data.time.year) 296 | 297 | tes3mp.SetDaysPassed(self.data.time.daysPassed) 298 | 299 | tes3mp.SetTimeScale(self:GetCurrentTimeScale()) 300 | 301 | tes3mp.SendWorldTime(pid, forEveryone) 302 | end 303 | 304 | function BaseWorld:SaveJournal(playerPacket) 305 | stateHelper:SaveJournal(self, playerPacket) 306 | end 307 | 308 | function BaseWorld:SaveFactionRanks(pid) 309 | stateHelper:SaveFactionRanks(pid, self) 310 | end 311 | 312 | function BaseWorld:SaveFactionExpulsion(pid) 313 | stateHelper:SaveFactionExpulsion(pid, self) 314 | end 315 | 316 | function BaseWorld:SaveFactionReputation(pid) 317 | stateHelper:SaveFactionReputation(pid, self) 318 | end 319 | 320 | function BaseWorld:SaveTopics(pid) 321 | stateHelper:SaveTopics(pid, self) 322 | end 323 | 324 | function BaseWorld:SaveBounty(pid) 325 | stateHelper:SaveBounty(pid, self) 326 | end 327 | 328 | function BaseWorld:SaveReputation(pid) 329 | stateHelper:SaveReputation(pid, self) 330 | end 331 | 332 | function BaseWorld:SaveClientScriptGlobal(variables) 333 | stateHelper:SaveClientScriptGlobal(self, variables) 334 | end 335 | 336 | function BaseWorld:SaveKills(pid) 337 | 338 | tes3mp.ReadReceivedWorldstate() 339 | 340 | for index = 0, tes3mp.GetKillChangesSize() - 1 do 341 | 342 | local refId = tes3mp.GetKillRefId(index) 343 | local number = tes3mp.GetKillNumber(index) 344 | self.data.kills[refId] = number 345 | end 346 | 347 | self:QuicksaveToDrive() 348 | end 349 | 350 | function BaseWorld:SaveRegionWeather(regionName) 351 | 352 | local loadedRegion = self.storedRegions[regionName] 353 | loadedRegion.currentWeather = tes3mp.GetWeatherCurrent() 354 | loadedRegion.nextWeather = tes3mp.GetWeatherNext() 355 | loadedRegion.queuedWeather = tes3mp.GetWeatherQueued() 356 | loadedRegion.transitionFactor = tes3mp.GetWeatherTransitionFactor() 357 | end 358 | 359 | function BaseWorld:SaveMapExploration(pid) 360 | stateHelper:SaveMapExploration(pid, self) 361 | end 362 | 363 | function BaseWorld:SaveMapTiles(mapTiles) 364 | 365 | for index, mapTile in ipairs(mapTiles) do 366 | -- We need to save the image file using the original index in the packet 367 | tes3mp.SaveMapTileImageFile(index - 1, config.dataPath .. "/map/" .. mapTile.filename) 368 | end 369 | end 370 | 371 | return BaseWorld 372 | -------------------------------------------------------------------------------- /scripts/world/json.lua: -------------------------------------------------------------------------------- 1 | require("config") 2 | tableHelper = require("tableHelper") 3 | local BaseWorld = require("world.base") 4 | 5 | local World = class("World", BaseWorld) 6 | 7 | function World:__init() 8 | BaseWorld.__init(self) 9 | 10 | self.coreVariablesFile = "coreVariables.json" 11 | self.worldFile = "world.json" 12 | 13 | if self.hasEntry == nil then 14 | local home = config.dataPath .. "/world/" 15 | local file = io.open(home .. self.worldFile, "r") 16 | if file ~= nil then 17 | io.close() 18 | self.hasEntry = true 19 | else 20 | self.hasEntry = false 21 | end 22 | end 23 | end 24 | 25 | function World:CreateEntry() 26 | jsonInterface.save("world/" .. self.coreVariablesFile, self.coreVariables) 27 | jsonInterface.save("world/" .. self.worldFile, self.data) 28 | self.hasEntry = true 29 | end 30 | 31 | function World:SaveToDrive() 32 | if self.hasEntry then 33 | jsonInterface.save("world/" .. self.coreVariablesFile, self.coreVariables) 34 | jsonInterface.save("world/" .. self.worldFile, self.data, config.worldKeyOrder) 35 | end 36 | end 37 | 38 | function World:QuicksaveToDrive() 39 | if self.hasEntry then 40 | jsonInterface.quicksave("world/" .. self.coreVariablesFile, self.coreVariables) 41 | jsonInterface.quicksave("world/" .. self.worldFile, self.data) 42 | end 43 | end 44 | 45 | function World:QuicksaveCoreVariablesToDrive() 46 | if self.hasEntry then 47 | jsonInterface.quicksave("world/" .. self.coreVariablesFile, self.coreVariables) 48 | end 49 | end 50 | 51 | function World:LoadFromDrive() 52 | self.coreVariables = jsonInterface.load("world/" .. self.coreVariablesFile) 53 | self.data = jsonInterface.load("world/" .. self.worldFile) 54 | 55 | if self.data == nil then 56 | tes3mp.LogMessage(enumerations.log.ERROR, "world/" .. self.worldFile .. " cannot be read!") 57 | tes3mp.StopServer(2) 58 | else 59 | -- JSON doesn't allow numerical keys, but we use them, so convert 60 | -- all string number keys into numerical keys 61 | tableHelper.fixNumericalKeys(self.data) 62 | end 63 | end 64 | 65 | 66 | -- Deprecated functions with confusing names, kept around for backwards compatibility 67 | function World:Save() 68 | self:SaveToDrive() 69 | end 70 | 71 | function World:Load() 72 | self:LoadFromDrive() 73 | end 74 | 75 | return World 76 | -------------------------------------------------------------------------------- /scripts/world/sql.lua: -------------------------------------------------------------------------------- 1 | Database = require("database") 2 | local BaseWorld = require("world.base") 3 | 4 | local World = class("World", BaseWorld) 5 | 6 | function World:__init() 7 | BaseWorld.__init(self) 8 | 9 | if self.hasEntry == nil then 10 | 11 | local test = Database:GetSingleValue("world_general", "currentMpNum", "") 12 | 13 | if test ~= nil then 14 | self.hasEntry = true 15 | else 16 | self.hasEntry = false 17 | end 18 | end 19 | end 20 | 21 | function World:CreateEntry() 22 | self:SaveToDrive() 23 | self.hasEntry = true 24 | end 25 | 26 | function World:SaveToDrive() 27 | Database:SaveWorld(self.data) 28 | end 29 | 30 | function World:LoadFromDrive() 31 | self.data = Database:LoadWorld(self.data) 32 | end 33 | 34 | return World 35 | --------------------------------------------------------------------------------