This commit is contained in:
lifestorm
2024-08-04 22:55:00 +03:00
parent 0e770b2b49
commit 94063e4369
7342 changed files with 1718932 additions and 14 deletions

View File

@@ -0,0 +1,410 @@
--[[
| 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

View File

@@ -0,0 +1,68 @@
--[[
| 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/
--]]
PLUGIN.name = "Save Entities"
PLUGIN.description = "Saves entities into the database, creating a record per entity."
PLUGIN.author = "Gr4Ss"
ix.util.Include("sv_plugin.lua")
ix.config.Add("SaveEntsOldLoadingEnabled", false, "If the old (file-based) entity loading should be used, or the new (DB-based) loading.", nil, {
category = "Autres"
})
CAMI.RegisterPrivilege({
Name = "Helix - SaveEnts",
MinAccess = "superadmin"
})
ix.command.Add("SaveEntsSave", {
description = "Sauvegarde toutes les entités d'une classe spécifique (ou lance la sauvegarde automatique si aucune classe n'est fournie).",
arguments = {
bit.bor(ix.type.string, ix.type.optional)
},
privilege = "SaveEnts",
OnRun = function(self, client, class)
if (class) then
if (!ix.saveEnts.storedTypes[class]) then
return class.." n'est pas une classe saveEnts valide !"
end
ix.saveEnts:SaveClass(class)
return "Sauvegarde de toutes les entités de la classe "..class.."!"
else
ix.saveEnts:SaveAll()
return "Sauvegardé toutes les entités !"
end
end,
})
ix.command.Add("SaveEntsLoad", {
description = "Charge toutes les entités d'une classe spécifique (ou exécute le chargement automatique si aucune classe n'est fournie). Les entités déjà chargées sont ignorées.",
arguments = {
bit.bor(ix.type.string, ix.type.optional)
},
privilege = "SaveEnts",
OnRun = function(self, client, class)
if (class) then
if (!ix.saveEnts.storedTypes[class]) then
return class.." n'est pas une classe saveEnts valide !"
end
ix.saveEnts:RestoreAll(class)
return "Chargé toutes les entités de la classe "..class.."!"
else
ix.saveEnts:RestoreAll()
return "Chargé toutes les entités !"
end
end,
})

View File

@@ -0,0 +1,149 @@
--[[
| 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 PLUGIN = PLUGIN
local ix = ix
function PLUGIN:DatabaseConnected()
local query = mysql:Create("ix_saveents")
query:Create("id", "INT UNSIGNED NOT NULL AUTO_INCREMENT")
query:Create("class", "VARCHAR(255) NOT NULL")
query:Create("map", "VARCHAR(255) NOT NULL")
query:Create("data", "TEXT NOT NULL")
query:Create("deleted", "INT(11) UNSIGNED DEFAULT NULL")
query:PrimaryKey("id")
query:Execute()
ix.saveEnts.dbLoaded = true
for k, v in ipairs(ix.saveEnts.cache) do
ix.saveEnts:SaveEntity(v[1], v[2])
end
if (ix.config.Get("SaveEntsOldLoadingEnabled")) then
local updateQuery = mysql:Update("ix_saveents")
updateQuery:Where("deleted", 0)
updateQuery:Where("map", game.GetMap())
updateQuery:Update("deleted", os.time())
updateQuery:Execute()
end
end
function PLUGIN:SaveData()
ix.saveEnts:SaveAll()
end
function PLUGIN:LoadData()
if (!ix.config.Get("SaveEntsOldLoadingEnabled")) then
ix.saveEnts:RestoreAll()
end
end
function PLUGIN:EntityRemoved(entity)
if (ix.shuttingDown) then return end
if (!entity.ixSaveEntsID and !entity.ixSaveEntsBeingCreated) then return end
local class = entity:GetClass()
if (ix.saveEnts.storedTypes[class] and ix.saveEnts.storedTypes[class].bDeleteOnRemove) then
ix.saveEnts:DeleteEntity(entity)
end
end
function PLUGIN:PhysgunDrop(client, entity)
ix.saveEnts:SaveEntity(entity, true)
end
function PLUGIN:InitializedPlugins()
hook.SafeRun("RegisterSaveEnts")
end
local persistWhitelist = {
["prop_physics"] = true,
["prop_effect"] = true,
}
function PLUGIN:CanProperty(client, property, entity)
if (property == "persist" and IsValid(entity) and ix.saveEnts.storedTypes[entity:GetClass()] and !persistWhitelist[entity:GetClass()]) then
return false
end
end
--[[
WHAT THIS DOES
This plugin saves and restores entities registered with it. Registering entities overal is easier than writing your own save/restore code, as a lot of stuff is already handled by the plugin. For most use cases, you only must tell the plugin what custom data you want to store and how it should be restored. Everything else is done by the plugin: grabbing generic data (position, angles, skin, material, frozen state), writing it into the DB, tracking entities, restoring them on load. The plugin will store the data entity per entity in the DB rather than using a big JSON table for all entities of the same class in a file. This is less risky for data to get lost: entities are only removed from the DB if they specifically get deleted - 'bad' saves aren't possible (e.g. in case of a bad load, causing the save file to be overwritten with an empty table) unless an entity is in a bad state.
You can also easily save entities proactively if your data on them changes by calling the SaveEntity function. This immediately updates the save for your entity, and means there is no risk of a desync in case of a crash before the next SaveData run. As the plugin can write into the DB per entity, this is much more efficient than helix's way of finding all entities of a class and writing a giant table into a file every time one little value changes.
The plugin itself is already proactive in saving an entity every time it is released by the physgun (assuming position or angles changed). Deleted entities also get their save automatically removed.
Further more there is an optimization: if no data changed, no write into the DB is done. This avoids hammering the DB updating 1000's of entities one by one when SaveData runs. A simple CRC check is used for this. This also means that proactively saving entities is good: it causes the save to happen before SaveData is run, helping spread out the load on the DB (better a write every second than 300 writes at the same time every 5 minutes). SaveData also checks entities in batches every 0.5 seconds, so you don't get a giant lag spike on the frame where the save happens.
tl;dr use this plugin, it is easier, better and more performant
HOW TO USE THIS
1) Register your entity class by calling: ix.saveEnts:RegisterEntity(class, bDeleteOnRemove, bAutoSave, bAutoRestore, funcs)
Easiest to do this in the RegisterSaveEnts hook to be sure this plugin has loaded, but do it before entities are restored.
MANDATORY
class: the class of your entity as a string
SEMI-OPTIONAL (defaults to false if not provided, you usually want them as true though)
bDeleteOnRemove: automatically deletes saved data if the entity is removed, preventing it from respawning
bAutoSave: include this entity in the periodic auto-save
bAutoRestore: automatically restore this class with all the other entities
OPTIONAL FIELDS, these all are in the 'funcs' argument
OnSave(entity, data): function to set data on save, either change the data table or return a new data table (overrides the passed data table, they do not get merged!). You can also edit some of the default data in here, or remove it if you do not wish for it to be automatically restored. Note that the library already takes care of pos, angles, skin & motion (clear these fields from the data if you don't want them saved)
OnRestore(entity, data): restore data on your entity using the data table
ShouldSave(entity): return nil/false if you do not wish to save your entity
ShouldRestore(data): return nil/false if you do not wish to restore this data
OnDelete(entity): called if an entity's saved data is deleted (this isn't necessarily when the entity is deleted, depending on how you use the save system)
Example for a simple entity:
ix.saveEnts:RegisterEntity("ix_example", true, true, true, {
OnSave = function(entity, data)
data.someField = entity.someField
end,
OnRestore = function(entity, data)
entity:RestoreSomeField(data.someField)
end
})
MAKE SURE THAT YOUR SAVE/RESTORE FUNCTIONS DO NOT ERROR! This prevents other stuff from saving/loading! Always test when you make changes or add stuff!!!
IF YOU MAKE CHANGES TO THESE FUNCTIONS, KEEP OnRestore BACKWARDS COMPATIBLE WITH THE DATA IN THE DB! Otherwise you get errors loading in your items, and this prevents the new OnSave from being run...
The above takes care of the vast majority of the work.
2) You have a more complex use case, you can manually call some functions (from your code, or using lua_run):
ix.saveEnts:SaveAll(): saves all entities as far as their classes are registered. This is done in batches and may need some time to run, calling it while it is still running has no effect
ix.saveEnts:SaveClass(class): saves all entities of the given class (assuming the class is registered with the plugin), if a class is given the bAutoRestore is ignored should it be nil/false
ix.saveEnts:SaveEntity(entity): creates the save for the entity or updates its existing save
ix.saveEnts:RestoreAll(class): restore all entities, shouldn't cause issues with already loaded entities. Class is optional to filter only to a class. Already restored entities that weren't removed won't be recreated.
ix.saveEnts:DeleteEntity(entity): deletes the saved data for the entity (does not remove the entity itself)
ix.saveEnts:DeleteEntityByID(id, class): manually deletes the saved data for an ID - USE WITH CAUTION (e.g. ensure there is no entity with the ixSaveEntsID left), doesn't call the OnDelete callback either, class is optional for extra safety
SOMETHING FUCKED UP AND YOU NEED TO RESTORE
-Entities were deleted:
-Reset the 'deleted' column in the DB back to 0 (e.g. for a given time, interval or class) and do 'lua_run ix.saveEnts:RestoreAll()' or maprestart
-Note that while ix is shutting down, entity deletion shouldn't happen in the DB for any reason
-An error caused entities to not restore:
-Fix the error and maprestart, unless the entities were explicitly deleted, their data is still in the DB
-Saved data got corrupted:
-You cannot recover this data except from a DB backup
-Use ShouldSave hooks to ensure only correct data gets saved (e.g. do not save containers with inventory ID 0 as this is never correct)
-Entities loaded from another source and got duplicated:
-You will manually have to delete the duplicate entities, or shut the server down and delete them from the DB (based on their ID for example)
-If other sources load in entities on top of saveEnts loading them in, the saveEnts plugin considers these 'new entities' and makes a new additional save for them
KEEP IN MIND:
-Not restoring entities means they will stay in the DB! If an item shouldn't be restored, make sure to not save it... if it was saved anyway, delete it upon restoring it
--]]