Files
wnsrc/gamemodes/helix/plugins/saveents/libs/sv_saveents.lua
lifestorm 94063e4369 Upload
2024-08-04 22:55:00 +03:00

410 lines
12 KiB
Lua
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
local ix = ix
local ents = ents
local ipairs = ipairs
local IsValid = IsValid
local ErrorNoHalt = ErrorNoHalt
local util = util
ix.saveEnts = ix.saveEnts or {}
ix.saveEnts.storedTypes = ix.saveEnts.storedTypes or {}
ix.saveEnts.cache = ix.saveEnts.cache or {}
function ix.saveEnts:RegisterEntity(class, bDeleteOnRemove, bAutoSave, bAutoRestore, funcs)
if (!class) then return end
ix.saveEnts.storedTypes[class] = {
class = class,
bDeleteOnRemove = bDeleteOnRemove,
bAutoSave = bAutoSave,
bAutoRestore = bAutoRestore,
OnSave = funcs.OnSave,
OnRestorePreSpawn = funcs.OnRestorePreSpawn,
OnRestore = funcs.OnRestore,
ShouldSave = funcs.ShouldSave,
ShouldRestore = funcs.ShouldRestore,
OnDelete = funcs.OnDelete,
}
end
ix.saveEnts.batch = nil
ix.saveEnts.batchNumber = 0
ix.saveEnts.BATCH_SIZE = 500 -- amount of entities in a single 0.5 second batch, change as needed for performance/time it takes to save all entities
-- Helper function to check if an entity should save
local function ShouldSave(entity, bNoAutoSave)
if (IsValid(entity) and ix.saveEnts.storedTypes[entity:GetClass()] and (!bNoAutoSave or ix.saveEnts.storedTypes[entity:GetClass()].bAutoSave)) then
local info = ix.saveEnts.storedTypes[entity:GetClass()]
if (info.ShouldSave and !info.ShouldSave(entity)) then
return false
end
return true
end
return false
end
-- Helper function to save a batch of entities
local function SaveBatch()
local lib = ix.saveEnts
local offset = lib.batchNumber * lib.BATCH_SIZE
-- If we have a batch and at least one more item above our offset exists (as the for loop starts at offset + 1)
if (lib.batch and #lib.batch > offset) then
local bDone = false
for i = 1, lib.BATCH_SIZE do
-- Check if the item still exists
if (lib.batch[offset + i]) then
lib:SaveEntity(lib.batch[offset + i], true)
else
--Batch complete!
bDone = true
break
end
end
-- Batch not complete, and next batch has at least one more run?
if (!bDone and #lib.batch > (offset + lib.BATCH_SIZE)) then
-- Increase batch number and wait for the timer to run the next batch
lib.batchNumber = lib.batchNumber + 1
return
end
end
--Batch complete if we get this far!
lib.batch = nil
end
--- Saves all posible entities into the Database.
-- If the entity does not exist in the DB, a new entry is added.
-- If the entity does exist in the DB, the existing entry is updated.
-- Note that classes which do not have autosave enabled are ignored, they are expected to handle their own saving already (via SaveClass or SaveEntity)
function ix.saveEnts:SaveAll()
if (ix.shuttingDown) then
timer.Remove("ixSaveEnt")
-- immediately save all date if the server is shutting down
local entities = ents.GetAll()
for i = 1, #entities do
self:SaveEntity(entities[i])
end
else
-- We are still running a batch, can't start a new one
if (self.batch) then return end
local entities = ents.GetAll()
self.batch = {}
for k, entity in ipairs(entities) do
if (ShouldSave(entity, true)) then
self.batch[#self.batch + 1] = entity
end
end
self.batchNumber = 0
-- Run batch 0 manually
SaveBatch()
-- If that doesn't cover it, set a timer
if (self.batch and #self.batch > self.BATCH_SIZE) then
-- We already did one run manually, so run timer for the amount of batches minus one
timer.Create("ixSaveEnt", 0.5, math.ceil(#self.batch / self.BATCH_SIZE) - 1, SaveBatch)
end
end
end
--- Saves all posible entities from a specific class into the Database.
-- If the entity does not exist in the DB, a new entry is added.
-- If the entity does exist in the DB, the existing entry is updated.
-- Classes with autosave disabled will still be saved
function ix.saveEnts:SaveClass(class)
if (self.storedTypes[class]) then
local entities = ents.FindByClass(class)
for i = 1, #entities do
self:SaveEntity(entities[i])
end
end
end
-- Helper function to get the to-save data table
local function CreateDataTable(entity)
local info = ix.saveEnts.storedTypes[entity:GetClass()]
if (!info) then return end
local data = {}
data.pos = entity:GetPos()
data.angles = entity:GetAngles()
data.skin = entity:GetSkin()
data.color = entity:GetColor()
data.material = entity:GetMaterial()
data.nocollide = entity:GetCollisionGroup() == COLLISION_GROUP_WORLD
data.motion = false
data.scale = entity.scaledSize
local physicsObject = entity:GetPhysicsObject()
if (IsValid(physicsObject)) then
data.motion = physicsObject:IsMotionEnabled()
end
local materials = entity:GetMaterials()
if (istable(materials)) then
data.submats = {}
for k in pairs(materials) do
if (entity:GetSubMaterial(k - 1) != "") then
data.submats[k] = entity:GetSubMaterial(k - 1)
end
end
end
local bodygroups = entity:GetBodyGroups()
if (istable(bodygroups)) then
data.bodygroups = {}
for _, v in pairs(bodygroups) do
if (entity:GetBodygroup(v.id) > 0) then
data.bodygroups[v.id] = entity:GetBodygroup(v.id)
end
end
end
if (info.OnSave) then
data = info.OnSave(entity, data) or data
end
return data
end
-- Helper function to save an existing entity (was saved previously already)
local function UpdateEntity(entity)
if (entity.ixSaveEntsID) then
local success, data = pcall(CreateDataTable, entity)
if (!success) then
ErrorNoHalt("[SAVEENTS-U-"..entity:GetClass().."] "..data)
return
end
local dataString = util.TableToJSON(data)
local crc = util.CRC(dataString)
if (crc == entity.ixSaveEntsCRC) then
return
end
local query = mysql:Update("ix_saveents")
query:Where("id", entity.ixSaveEntsID)
query:Where("class", entity:GetClass())
query:Where("map", game.GetMap())
query:Update("data", dataString)
query:Execute()
entity.ixSaveEntsCRC = crc
end
end
-- Helper function to save a new entity (not known to save system yet)
local function CreateEntity(entity)
if (!entity.ixSaveEntsID and !entity.ixSaveEntsBeingCreated) then
local class = entity:GetClass()
local success, data = pcall(CreateDataTable, entity)
if (!success) then
ErrorNoHalt("[SAVEENTS-C-"..class.."] "..data)
return
end
entity.ixSaveEntsBeingCreated = true
local dataString = util.TableToJSON(data)
local insertQuery = mysql:Insert("ix_saveents")
insertQuery:Insert("class", class)
insertQuery:Insert("map", game.GetMap())
insertQuery:Insert("data", dataString)
insertQuery:Insert("deleted", 0)
insertQuery:Callback(function(result, status, id)
if (IsValid(entity) and entity.ixSaveEntsBeingCreated) then
entity.ixSaveEntsID = id
entity.ixSaveEntsCRC = util.CRC(dataString)
entity.ixSaveEntsBeingCreated = nil
else
-- Entity got deleted/unsaved before creation finished, so lets nuke it
ix.saveEnts:DeleteEntityByID(id, class)
end
end)
insertQuery:Execute()
end
end
-- Saves a single entity into the Database.
-- If the entity does not exist in the DB, a new entry is added.
-- If the entity does exist in the DB, the existing entry is updated.
-- Setting bNoAutoSave to true causes the entity to not be saved if its class isn't marked for auto-save
function ix.saveEnts:SaveEntity(entity, bNoAutoSave)
if (!self.dbLoaded) then
self.cache[#self.cache + 1] = {entity, bNoAutoSave}
return
end
if (ShouldSave(entity, bNoAutoSave)) then
if (entity.ixSaveEntsID) then
UpdateEntity(entity)
else
CreateEntity(entity)
end
end
end
-- Restore all entities saved in the DB, for as far as they weren't restored already
function ix.saveEnts:RestoreAll(class)
local restoredEnts = {}
for k, v in ipairs(ents.GetAll()) do
if (v.ixSaveEntsID) then
restoredEnts[v.ixSaveEntsID] = v:GetClass()
end
end
local selectQuery = mysql:Select("ix_saveents")
selectQuery:Where("map", game.GetMap())
selectQuery:Where("deleted", 0)
if (class) then
selectQuery:Where("class", class)
end
selectQuery:Callback(function(result)
if (!result) then return end
for k, v in ipairs(result) do
-- entity was already restored
if (restoredEnts[v.id]) then
if (restoredEnts[v.id] != v.class) then
ErrorNoHalt("[SaveEnts] L'entité de restauration existe déjà avec une classe incompatible ! DBID:"..v.id.."; "..v.class.." (DB) vs "..restoredEnts[v.id].." (spawned)")
end
continue
end
-- this class is no longer registered (likely plugin failed to load, or it was removed)
local info = self.storedTypes[v.class]
if (!info) then continue end
-- do not automatically restore unless we are specifically restoring this class
if (!info.bAutoRestore and class != v.class) then continue end
local data = util.JSONToTable(v.data)
if (!data) then continue end
if (info.ShouldRestore) then
local success, bShouldRestore, bDelete = pcall(info.ShouldRestore, data)
if (!success) then
ErrorNoHalt("[SAVEENTS-L1-"..v.class.."] "..bShouldRestore)
continue
end
if (!bShouldRestore) then
if (bDelete) then
self:DeleteEntityByID(v.id, v.class)
end
continue
end
end
local entity = ents.Create(v.class)
entity.ixSaveEntsID = v.id
entity.ixSaveEntsCRC = util.CRC(v.data)
if (data.pos) then entity:SetPos(data.pos) end
if (data.angles) then entity:SetAngles(data.angles) end
if (data.skin) then entity:SetSkin(data.skin) end
if (data.color) then entity:SetColor(data.color) end
if (data.material) then entity:SetMaterial(data.material) end
if (data.scale) then
timer.Simple(3, function()
ix.scalestuff:ScaleEntity(entity, data.scale)
end)
end
if (info.OnRestorePreSpawn) then
local success, err = pcall(info.OnRestorePreSpawn, entity, data)
if (!success) then
ErrorNoHalt("[SAVEENTS-L2-"..v.class.."] "..err)
continue
end
end
entity:Spawn()
if (data.nocollide) then entity:SetCollisionGroup(COLLISION_GROUP_WORLD) end
if (istable(v.submats)) then
for k2, v2 in pairs(v.submats) do
if (!isnumber(k2) or !isstring(v2)) then
continue
end
entity:SetSubMaterial(k2 - 1, v2)
end
end
if (istable(v.bodygroups)) then
for k2, v2 in pairs(v.bodygroups) do
entity:SetBodygroup(k2, v2)
end
end
if (data.motion != nil) then
local physicsObject = entity:GetPhysicsObject()
if (IsValid(physicsObject)) then
physicsObject:EnableMotion(data.motion)
end
end
if (info.OnRestore) then
local success, err = pcall(info.OnRestore, entity, data)
if (!success) then
ErrorNoHalt("[SAVEENTS-L3-"..v.class.."] "..err)
continue
end
end
end
hook.Run("PostSaveEntsRestore", class)
end)
selectQuery:Execute()
end
-- Delete the save of an entity
function ix.saveEnts:DeleteEntity(entity)
if (ix.shuttingDown) then return end
if (!IsValid(entity)) then return end
if (entity.ixSaveEntsBeingCreated) then
-- stop creation, this will cause it to be deleted after creation is finished
entity.ixSaveEntsBeingCreated = nil
return
end
if (!entity.ixSaveEntsID) then
return
end
local class = entity:GetClass()
self:DeleteEntityByID(entity.ixSaveEntsID, class)
entity.ixSaveEntsID = nil
if (self.storedTypes[class] and self.storedTypes[class].OnDelete) then
self.storedTypes[class].OnDelete(entity)
end
end
-- Manually delete the save of an entity - you should use DeleteEntity unless you know what you are doing
function ix.saveEnts:DeleteEntityByID(id, class)
local deleteQuery = mysql:Update("ix_saveents")
deleteQuery:Where("id", id)
deleteQuery:Where("map", game.GetMap())
if (class) then
--Extra safety check so you can't accidentally delete the wrong entity
deleteQuery:Where("class", class)
end
deleteQuery:Update("deleted", os.time())
deleteQuery:Execute()
end