Storing data across saved games from LUA

And how you want to resolve it?
I dont think that appending such data to save file is good idea.

You can save additional data to same dir as civ5 saves with same name and different extension. Some games stored their saves in that way.

You can't append data to saves anyway, however, Civilization 5 has some ways of allowing modders to save data inside of saves like normal data. If you don't know what I'm talking about, I recommend you read a few of the threads here about saving data in the Lua forum. ;)
 
hello. have just discovered this thread :)
i made a simple table serialization script few days ago (no spaces in string keys and values). so it could be handy i think or provide some foundation to build a data saving system upon. it also can be improved if it will be needed, in terms of performance or universality.
Spoiler :
Code:
-- Serialization
-- Author: killmeplese
-- DateCreated: 10/17/2010 2:40:28 PM
--------------------------------------------------------------
-- With these functions you can save and load tables to/from string variable.
-- Tables might contain numbers, boolean values, string values WITH NO SPACES,
-- and nested tables. The code was tested in WinQLua program.
--
-- Test:
--	tbl = { 1, 2, 3, t={6,7,8}, a=55, b=56, zz={"alpha", "beta", false} };
--	str = SerializeTable(tbl);
--	print(str);
--	tbl = DeserializeTable(str);
--	str = SerializeTable(tbl);
--	print(str);
--
-- Output:
--	{ 1=1 2=2 3=3 a=55 t={ 1=6 2=7 3=8 } zz={ 1=alpha 2=beta 3=false } b=56 }
--	{ 1=1 2=2 3=3 a=55 t={ 1=6 2=7 3=8 } zz={ 1=alpha 2=beta 3=false } b=56 }
--------------------------------------------------------------

function SerializeTable(tbl)
	local res = "{ ";
	local num = 0;
	for k, v in pairs(tbl) do
		if num > 0 then
			res = res.." ";
		end
		res = res..k.."=";
		if type(v) == "table" then
			res = res..SerializeTable(v);
		else
			res = res..tostring(v);	-- tostring is necessary to serialize boolean values
		end
		num = num + 1;
	end
	return res.." }";
end

--------------------------------------------------------------
-- turned out to be much harder than to serialize, lol
function DeserializeTable(str)  -- no spaces in keys and values allowed
	local res = {};  -- result table

	local _,_,str = string.find(str,"{%s(.*)%s}");  -- trim { } brackets
	-- save nested table values to a temp table
	local nested = {};
	local tableNum = 1;
	for t in string.gmatch(str,"{[^{}]*}") do	-- match nested tables
		table.insert(nested, t);
	end
	-- cut nested tables from string and replace them with "{}"
	local simple = string.gsub(str, "{([^{}]*)}", "{}"); 
	-- parse key=value pairs
	for k, v in string.gmatch(simple,"(%w+)=(%S+)") do -- no spaces in keys or values !
		-- parse value
		if v == "{}" then
			v = DeserializeTable(nested[tableNum]);  -- recursively call deserialization
			tableNum = tableNum + 1;
		else
			-- parse simple value, it can be a number, a boolean or a word (string with no spaces)
			local n = tonumber(v);
			if n ~= nil then
				v = n;
			elseif v == "true" then
				v = true;
			elseif v == "false" then
				v = false;
			end
		end
		-- parse key and add data to table:
		local kn = tonumber(k);
		if kn ~= nil then
			table.insert(res, kn, v);   
		else
			res[k] = v;
		end
	end
	return res;  -- return result table
end
-------------------------------------------------------------
it was reported in this thread that loadstring can be used for low-level deserialization so we can deserialize tables of any complexity with 1 line of code and with greater-than-parsing performance, but i experienced problems with loadstring and havent get it working yet.
Here is a loadstring-variant (no restrictions on keys and values):
Spoiler :
Code:
function SerializeTable(tbl)
	local res = "{ ";
	local num = 0;
	for k, v in pairs(tbl) do
		if num > 0 then
			res = res..", ";
		end
		if type(k) == "number" then
			res = res..k.."=";
		else
			res = res.."["..k.."]".."=";
		end
		if type(v) == "table" then
			res = res..SerializeTable(v);
		else
			if v == false or v == true or type(v) == "number" then
				v = tostring(v);
			else
				v = "\""..v.."\"";	-- this is a string
			end
			res = res..v;
		end
		num = num + 1;
	end
	return res.." }";
end

function DeserializeTable(str)
	return assert(loadstring(""..str))();
end
data for different mods can be saved into one table:
data["mod1"] = {...}
data["mod2"] = {...}
etc
so you just deserialize saved script data, put your values in your section and save it back. the downside is poor performance i think as each mod have to serialize/deserialize each time it writes something in this common table, and there might be many of such operations during one turn conducted by several mods. but i cant think of anything better.

it would be great if we had a BeforeGameSaved event so we could store common table in a memory and write it to the script data only on demand. but we have not.

once i browsed civ5luaApi i spotted SaveGame and confused it with an event (it turned out to be a method of Game object). in result i have this non-functioning code:
Spoiler :
Code:
-- ScriptDataManager
-- Author: killmeplease
-- DateCreated: 10/16/2010 8:37:12 AM
--------------------------------------------------------------

-- its like a 3-dimensional array [category, id, mod] with each element containing mod data (which is a table)

local scriptData = { Players = {}, Plots = {}, Units = {} };

function ScriptData(category, pItem, mod)
	if scriptData[category] == nil then
		-- unknown category
		return nil;
	end

	local catData = scriptData[category];
	local itemID = GetID(category, pItem);
	
	print("Accessing "..category.." id: "..itemID);

	local itemData = catData[itemID];
	if itemData == nil then	-- data for this ID was not loaded from ScriptData
		local serializedData = pItem:GetScriptData();	-- load data
		if serializedData == "" or serializedData == nil then	-- is no data?
			-- no data for this ID
			catData[itemID] = { [mod] = {} };	-- create empty data for ID and for mod
			print("Category entry and mod data were created");
		else
			-- there is data, deserialize it:
			catData[itemID] = assert(loadstring(serializedData))();
			if catData[itemID][mod] == nil then	-- no data for given mod though
				catData[itemID][mod] = {};	-- create empty mod data
				print("Mod data was created");
			else
				print("Mod data was loaded");
			end
		end
	end
	return catData[itemID][mod];	-- return cat.item.mod data, loaded or newly created (empty)
end

function GetID(category, pItem)
	if category == "Players" then
		return pItem:GetID();
	elseif category == "Plots" then
		return pItem:GetX() * 1000 + pItem:GetY();
	elseif category == "Units" then
		return pItem:GetOwner() * 1000 + pItem:GetID();	-- GetOwner returns player ID.
	end
end

function SaveScriptData()
	print("saving script data...");
	-- here we have to serialize all data for all changed items and call SetScriptData:
	-- set players script data if changed:
	for id, v in pairs(scriptData.Players) do
		if v ~= nil and v.changed then
			Players[id]:SetScriptData(serialize(v));
		end
	end
	-- set plots script data if changed
	for id, v in pairs(scriptData.Plots) do
		if v ~= nil and v.changed then
			local plotX = math.floor(id / 1000);
			local plotY = id % 1000;
			Map.GetPlotXY(plotX, plotY, 0, 0):SetScriptData(serialize(v));
		end
	end
	-- set units script data if changed
	for id, v in pairs(scriptData.Units) do
		if v ~= nil and v.changed then
			local playerID = math.floor(id / 1000);
			local unitID = id % 1000;
			Players[playerID]:GetUnit(unitID):SetScriptData(serialize(v));
		end
	end
	print("script data saved");
end

Events.SaveGame.Add( SaveScriptData );
its also buggy. just want to illustrate my idea of in-memory cache.
 
hello. have just discovered this thread :)
i made a simple table serialization script few days ago (no spaces in string keys and values). so it could be handy i think or provide some foundation to build a data saving system upon. it also can be improved if it will be needed, in terms of performance or universality.

Since spaces are obviously important in your method, couldn't you strip all the spaces out beforehand and replace them with something like "[SPACE]", so that it doesn't break when a modder uses a space?

I also noticed the lack of a save game event, which would be handy. Without it, we need to save on the beginning of the players turn.

One thing, to be clear, if I wanted to retrieve my mod's data, I would use ScriptData like this?

ScriptData("Players", pPlayer, 42); With 42 being an arbitrary number?

Two changes I would make:

1.) Map.GetPlotXY(plotX, plotY, 0, 0) can be shortened to Map.GetPlot(plotX, plotY);

2.) check that Players[playerID]:GetUnit(unitID) isn't nil. Units can die.
 
Killtech Loadstring does not work. Firaxis must have disabled it.

Further analysis also revealed you forgot a way to set the data so it could be saved. What's the point of saving data if you can never alter it. ;)

I think I almost have it working. It saves the data correctly, and sets it correctly with a new function I added. But it fails to restore the saved data, and gives back garbage. Hopefully you can see what I'm missing. I've uploaded the changes the SVN, please take a look at them.
 
now i will look to svn. have not installed tortoise yet, have to do it

spaces can be introduced, i had not bothered with them because i had no need to save strings with spaces. the most easy way is to replace them with "_".

Saving on begin of turn is a weak solution (i thought of it too) because if user will save in the middle all the changes to script data since turn start will be lost.

One thing, to be clear, if I wanted to retrieve my mod's data, I would use ScriptData like this?

ScriptData("Players", pPlayer, 42); With 42 being an arbitrary number?
Code:
local playerData = ScriptData("Players", pPlayer, "Revolutions");
playerData.Cities["Berlin"].Loyality = x;  -- write
local empireStability = playerData.EmpireStability;  -- read
etc
ScriptData returns a table of your data that can be modified as you like. You can use 42 OFC but its intended to be a mod name :)

Data is saved when SaveScriptData is called. It is saved for all mods, players, plots and units (it is being pushed from global scriptData{} cache to ScriptData fields of corresponding objects) and the best place to call it is SaveGame event (if it was present). I dropped this mod as there is no SaveGame event (as it turned out).
 
The next version for serialization:

PHP:
function Serialize(tbl)
   local res = "{ ";
   local num = 0;
   for k, v in pairs(tbl) do
      if num > 0 then
         res = res.." ";
      end
      res = res..k.."=";
      if type(v) == "table" then
         res = res..SerializeTable(v);
      else
         if v == false or v == true or type(v) == "number" then
            v = tostring(v);
         else
            v = v:gsub('"', "\[QUOTE\]");
            v = v:gsub('{', "\[LCB\]");
            v = v:gsub('}', "\[RCB\]");
            v = "\""..v.."\"";	-- string
         end
         res = res..v;
      end
      num = num + 1;
   end
   return res.." }";
end

function Deserialize(str)
   local tbls = {};  -- nested tables
   local topTbl = {};
   repeat
      topTbl = {};  -- clear variable for top tbl
      -- find first top-level table position in str, its values list and key
      local strStart, strEnd, topTblKey, topTblVals = str:find("([%w]*)={%s([^{}]*)%s}");
      if topTblKey == nil then
         strStart, strEnd, topTblVals = str:find("{%s([^{}]*)%s}");
      end
      --print(topTblVals);

      -- parse values:
      -- save string values of top table to a temp table
      local strings = {};
      for vstr in topTblVals:gmatch('"([^"]*)"') do  -- match "..." string values
         vstr = vstr:gsub("%[QUOTE%]", '"');
         vstr = vstr:gsub("%[LCB%]", '{');
         vstr = vstr:gsub("%[RCB%]", '}');
         table.insert(strings, vstr);
      end
      -- cut string values from top table string and replace them with "@" symbol
      topTblVals = topTblVals:gsub('"[^"]*"', "@");

      -- parse key=value pairs
      stringNum = 1;
      for k, v in topTblVals:gmatch("(%S+)=(%S+)") do
         -- parse value
         if v == "#" then
            v = tbls[k];
            -- tbls[k] = nil;
         elseif v == "@" then
            v = strings[stringNum];
            stringNum = stringNum + 1;
         else
            -- parse simple value, a number or a boolean
            local n = tonumber(v);
            if n ~= nil then
               v = n;
            elseif v == "true" then
               v = true;
            elseif v == "false" then
               v = false;
            end
         end
         -- parse key:
         local n = tonumber(k);
         if n ~= nil then
            k = n;  
         end
         -- add value to the table:
         topTbl[k] = v;
      end
      if topTblKey ~= nil then  -- key is nil when it is a base table
         tbls[topTblKey] = topTbl;
         str = str:sub(1, strStart - 1)..topTblKey.."=#"..str:sub(strEnd + 1);
      else
         str = "#";
      end
      --print(str);
   until str == "#";
   return topTbl;
end

test:
Code:
[COLOR="SeaGreen"]>> tbl = { 1, 2, 3, t={6, {7, 7.5}, 8}, a=55, b=56, zz={"alpha", "beta \"or\" {gamma}", false} };
>> str = SerializeTable(tbl);
>> print(str);
>> tbl = Deserialize(str);
>> str = SerializeTable(tbl);
>> print(str);[/COLOR]
{ 1=1 2=2 3=3 a=55 t={ 1=6 2={ 1=7 2=7.5 } 3=8 } zz={ 1="alpha" 2="beta [QUOTE]or[QUOTE] [LCB]gamma[RCB]" 3=false } b=56 }
{ 1=1 2=2 3=3 a=55 t={ 1=6 2={ 1=7 2=7.5 } 3=8 } zz={ 1="alpha" 2="beta [QUOTE]or[QUOTE] [LCB]gamma[RCB]" 3=false } b=56 }

restrictions:
do not use string keys comprised of numbers only (e.g. "234")
do not use spaces in string keys

examples:
Code:
tbl = {
  [1] = 123, -- [COLOR="SeaGreen"]ok[/COLOR]
  ["1"] = 453, -- [COLOR="Red"]wrong[/COLOR]!
  ["123xz"], -- [COLOR="SeaGreen"]ok[/COLOR]
  ["abc"] = 64, -- [COLOR="SeaGreen"]ok[/COLOR]
  abc2 = 23, -- [COLOR="SeaGreen"]ok[/COLOR]
  ["a b c"] = 231 -- [COLOR="Red"]wrong[/COLOR]!
}
local str = Serialize(tbl);

technique that allows multiple mods to access script data: use mod identifier after deserializing script data to access your data.
read:
Code:
local str = pPlayer:GetScriptData();
data = Deserialize(str);
revdata = data["Revolutions"];
write:
Code:
revdata.somefield = somevalue;
local str = pPlayer:GetScriptData();
data = Deserialize(str);
data["Revolutions"] = revdata;
local str = serialize(data);
pPlayer:SetScriptData(str);
all modders have to use this approach for thier mods be compatible with each other.
otherwise one change for a some item by some mod will erase all other mod's data for this item.
 
so we can write a wrapper for these routines.
and it will look like

local playerData = GetPlayerData(pPlayer, "Revolutions"); -- read
SetPlayerData(pPlayer, playerData, "Revolutions"); -- write

where playerData is some table with mod's values

internal realization:

Code:
function GetPlayerData(pPlayer, modName)
  local sdata = pPlayer:GetScriptData();
  if sdata ~= nil and sdata ~= "" then
    return Deserialize(sdata)[modName];
  end
  return nil;
end

function SetPlayerData(pPlayer, playerData, modName)
  local sdata = pPlayer:GetScriptData();
  local data = nil;
  if sdata ~= nil and sdata ~= "" then
    data = Deserialize(sdata);
    data[modName] = playerData;
  else
    data = { [modName] = playerData };
  end
  sdata = Serialize(data);
  pPlayer:SetScriptData(sdata);
end

function UsePlayerData(pPlayer, modName)  -- this increases performance a bit but to prevent conflicts with other mods it has only be used locally
  local sdata = pPlayer:GetScriptData();
  local data = nil;
  if sdata ~= nil and sdata ~= "" then
    data = Deserialize(sdata);
  else
    data = {};
  end

  local modData = nil;
  if data[modName] == nil then
    data[modName] = {};
  end  
  modData = data[modName];

  local function Save()
    pPlayer:SetScriptData(Serialize(data));
  end

  return modData, Save;
end

-- UsePlayerData example:
local revdata, save = UsePlayerData(pPlayer, "Revolutions");
local somevalue1 = revdata.somefield1;
revdata.somefield2 = somevalue2;
save();
 
ScriptDataManager reincarnation:

Code:
local scriptData = { Players = {}, Plots = {}, Units = {} };

function AccessData(category, pItem, mod)
	if not IsValidCategory(category) then
		-- unknown category
		return nil;
	end
	local catData = scriptData[category];
	local itemData = catData[pItem];
	if itemData == nil then		-- was not initialized yet (accessing for first time)
		local sdata = pItem:GetScriptData();	-- load data
		if sdata == "" or sdata == nil then	-- is no data?
			catData[pItem] = { [mod] = {} };	-- create empty data for item and for mod
		else
			-- there is data, deserialize it:
			catData[pItem] = Deserialize(sdata);
			if catData[pItem][mod] == nil then	-- no data for given mod though
				catData[pItem][mod] = {};		-- create empty
			end
		end
	end
	
	local function Save()
		SaveData(category, pItem);
	end

	return catData[pItem][mod], Save;	-- return cat.item.mod data, loaded or newly created (empty) and function to save changes
end

function SaveData(category, pItem)	-- save function works for all mods data for an item so no need to specify a mod
	if not IsValidCategory(category) or scriptData[category][pItem] == nil then
		return;
	end
	sdata = Serialize(scriptData[category][pItem]);
	pItem:SetScriptData(sdata);
end

function IsValidCategory(category)
	if category == "Players" or category == "Plots" or category == "Units" then
		return true;
	end
	return false;
end
it loads data on demand and saves it in cache so we have not to deserialize it each time we access it. deserialization occures only once after game is loaded, on first demand.
but we have to save state each time we change something, thus launching serialization:
Code:
local revdata, s = AccessData("Players", pPlayer, "Revolutions");
revdata.somefield = somevalue;
s();
 
killmeplease,

As you know, we've been working on some of the same functionality. I'm impressed by what you've created here, but disappointed by the lack of class structure. I think our efforts could nicely compliment one another, but I'm having trouble with your latest iteration of deserialize.

This line of code:
Code:
for vstr in topTblVals:gmatch('"([^"]*)"') do -- match "..." string values

Produces this error:
Code:
...attempt to index local 'topTblVals' (a nil value)

I'd like to suggest the following change so as to handle a nil value:
Code:
		if topTblVals == nil then
			topTblVals = "";
		end

I have a good reason for this, really. :)
 
As you know, we've been working on some of the same functionality. I'm impressed by what you've created here, but disappointed by the lack of class structure.
i do not see a need in making class for 2 global functions.
you can modify this code as you like though, making a class wrapper or such ;)

thanks for bug report. i havent foresaw the situation where a table is empty. so here is the fixed code:

Code:
function Deserialize(str)
   local tbls = {};  -- nested tables
   local topTbl = {};
   repeat
      topTbl = {};  -- clear variable for top tbl
      -- find first top-level table position in str, its values list and key
      local strStart, strEnd, topTblKey, topTblVals = str:find("([%w]*)={%s([^{}]*)%s}");
      if topTblKey == nil then
         strStart, strEnd, topTblVals = str:find("{%s([^{}]*)%s}");
      end
      [COLOR="SeaGreen"]topTblVals = topTblVals or "";[/COLOR]  -- empty table
      ...
 
Please consider the following code. It allows for ITEMIZED data storage across mods. It does not currently load to cache, but I'm working on it. I assume it works across saves, but I haven't tested that.

Currently, the load function will return the entire save table if no mod name is designated, and additional class functions for retrieving different kinds of data groupings can be easily implemented.

SaveUtils.lua
Spoiler :
Code:
-- vymdt.01.2010.10.19.0000
-- Created by: Whys -Open source
--===========================================================================
-- SaveUtils
--===========================================================================
--[[
Credits: killmeplease -- serialize() and deserialize().
Special Thanks: Thalassicus, Afforess.
]]

-----------------------------------------------------------------------------
-- Save Class
-----------------------------------------------------------------------------
--[[
Allows SetScriptData() and GetScriptData() to behave as a hash-table of
tables while maintaining seperation of individual mod data.
ie:
	save( "myMod",   pPlayer, key1, table );
	save( "myMod",   pPlayer, key2, table );
	save( "someMod", pPlayer, key1, table );
	save( "myMod",   pPlot,   ...., ..... ); --same as above.
	save( "myMod",   pUnit,   ...., ..... ); --same as above.

	load( "myMod",   pPlayer, key1 );
	load( "myMod",   pPlayer, key2 );
	load( "someMod", pPlayer, key1 );
	load( "myMod",   pPlot,   .... ); --same as above.
	load( "myMod",   pUnit,   .... ); --same as above.
	load( nil, pPlayer ); --all data for pPlayer.
	load( nil, pPlot );   --all data for pPlot.
	load( nil, pUnit );   --all data for pUnit.
]]
Save = {};

function Save:new( o )
	o = o or {};
	setmetatable( o, self );
	self.__index = self;
	self:initKeys( o );
	return o;
end

-------------------------------------------------
-- Save:initKeys()
-------------------------------------------------
--[[
Deep assignment of keyed values.  Required for
duplication of table data assigned by key.
]]
function Save:initKeys( o )
	for k,v in pairs( o ) do
		self[k] = v;
		if type( v ) == "table" then
			self:initKeys( v );
		end
	end
end
 
-------------------------------------------------
-- Save:set()
-------------------------------------------------
--[[
Sets the given value to the given key for the
given mod.
]]
function Save:set( mod, key, value )
	self[mod] = self[mod] or {};
	self[mod][key] = value;
end
 
-------------------------------------------------
-- Save:get()
-------------------------------------------------
--[[
Returns value for given mod and key.  Returns
self when mod not given.
]]
function Save:get( mod, key )
	local r = self;
	if mod ~= nil and self[mod] ~= nil and key ~= nil then
		r = self[mod][key];
	end
	return r;
end

-----------------------------------------------------------------------------
--END Save Class
-----------------------------------------------------------------------------

-------------------------------------------------
-- save()
-------------------------------------------------
--[[
Saves the given value to the given key for the
given target of the given mod.
]]
function save( mod, target, key, value )
	local pSave = load( mod, target );
	pSave:set( mod, key, value );
	target:SetScriptData(serialize( pSave ));
end

-------------------------------------------------
-- load()
-------------------------------------------------
--[[
Loads the value for the given key for the given
target of the given mod.
]]
function load( mod, target, key )
	local pSave = Save:new(deserialize( target:GetScriptData() ));
	return pSave:get( mod, key );
end

-------------------------------------------------
-- serialize()
-------------------------------------------------
--[[
Created by: killmeplease.
]]
function serialize( tbl )
	local r = "{ ";
	local num = 0;
	for k,v in pairs( tbl ) do
		if num > 0 then
			r = r.." ";
		end
		r = r..k.."=";
		if type( v ) == "table" then
			r = r..serialize( v );
		else
			if v == false or v == true or type( v ) == "number" then
				v = tostring( v );
			else
				v = v:gsub('"', "\[QUOTE\]");
				v = v:gsub('{', "\[LCB\]");
				v = v:gsub('}', "\[RCB\]");
				v = "\""..v.."\""; -- string
			end
			r = r..v;
		end
		num = num +1;
	end
	return r.." }";
end

-------------------------------------------------
-- deserialize()
-------------------------------------------------
--[[
Created by: killmeplease.
]]
function deserialize( str )
	local tbls = {}; -- nested tables
	local topTbl = {};
	repeat
		topTbl = {}; -- clear variable for top tbl
		-- find first top-level table position in str, its values list and key.
		local strStart, strEnd, topTblKey, topTblVals = str:find("([%w]*)={%s([^{}]*)%s}");
		if topTblKey == nil then
			strStart, strEnd, topTblVals = str:find("{%s([^{}]*)%s}");
		end
		if topTblVals == nil then
			topTblVals = "";
		end
		--print(topTblVals);

		-- parse values:
		-- save string values of top table to a temp table
		local strings = {};
		for vstr in topTblVals:gmatch('"([^"]*)"') do -- match "..." string values
			vstr = vstr:gsub("%[QUOTE%]", '"');
			vstr = vstr:gsub("%[LCB%]", '{');
			vstr = vstr:gsub("%[RCB%]", '}');
			table.insert(strings, vstr);
		end
		-- cut string values from top table string and replace them with "@" symbol
		topTblVals = topTblVals:gsub('"[^"]*"', "@");

		-- parse key=value pairs
		stringNum = 1;
		for k,v in topTblVals:gmatch("(%S+)=(%S+)") do
			-- parse value
			if v == "#" then
				v = tbls[k];
				-- tbls[k] = nil;
			elseif v == "@" then
				v = strings[stringNum];
				stringNum = stringNum +1;
			else
				-- parse simple value, a number or a boolean
				local n = tonumber(v);
				if n ~= nil then
					v = n;
				elseif v == "true" then
					v = true;
				elseif v == "false" then
					v = false;
				end
			end
			-- parse key:
			local n = tonumber(k);
			if n ~= nil then
				k = n;  
			end
			-- add value to the table:
			topTbl[k] = v;
		end
		if topTblKey ~= nil then -- key is nil when it is a base table
			tbls[topTblKey] = topTbl;
			str = str:sub(1, strStart -1)..topTblKey.."=#"..str:sub(strEnd +1);
		else
			str = "#";
		end
		--print(str);
	until str == "#";
	return topTbl;
end

--===========================================================================
--END SaveUtils
--===========================================================================
-- Created by: Whys -Open source


Spoiler :
Code:
include( "SaveUtils" );

function testThis()
	
	local pPlayer = GetPlayer();
	local t =  { 1, 2, 3, t={6, {7, 7.5}, 8}, a=55, b=56, zz={"alpha", "beta \"or\" {gamma}", false} };
	
	save( "myMod",   pPlayer, "key1", t );
	save( "myMod",   pPlayer, "key2", {"some value"} );
	save( "someMod", pPlayer, "key1", { 3, 2, 1, true } );
	
	print( "test1: "..serialize(load( "myMod",   pPlayer, "key1" )) );
	print( "test2: "..serialize(load( "myMod",   pPlayer, "key2" )) );
	print( "test3: "..serialize(load( "someMod", pPlayer, "key1" )) );
	print( "test4: "..serialize(load( nil, pPlayer )) );
end
Events.ActivePlayerTurnStart.Add( testThis );

Output:
Spoiler :
Code:
test1: { 1=1 2=2 3=3 a=55 t={ 1=6 2={ 1=7 2=7.5 } 3=8 } zz={ 1="alpha" 2="beta [QUOTE]or[QUOTE] [LCB]gamma[RCB]" 3=false } b=56 }

test2: { 1="some value" }

test3: { 1=3 2=2 3=1 4=true }

test4: { myMod={ key1={ 1=1 2=2 3=3 a=55 t={ 1=6 2={ 1=7 2=7.5 } 3=8 } zz={ 1="alpha" 2="beta [QUOTE]or[QUOTE] [LCB]gamma[RCB]" 3=false } b=56 } key2={ 1="some value" } } someMod={ key1={ 1=3 2=2 3=1 4=true } } }
 
ScriptDataManager, final (i believe) version:
Code:
local scriptData = { };	-- cache table

function AccessData(pItem, modName)
	local itemData = sriptData[pItem];
	if itemData == nil then		
		-- was not initialized yet (being accessing for first time)
		local sdata = pItem:GetScriptData();	-- load data
		if sdata == "" or sdata == nil then		-- is no data?
			scriptData[pItem] = { [modName] = {} };	-- create empty data for item and for mod
		else
			-- there is data for a given item, deserialize it:
			scriptData[pItem] = Deserialize(sdata);
			if scriptData[pItem][modName] == nil then
				scriptData[pItem][modName] = {};	-- if no data for a given mod - create empty
			end
		end
	end

	-- return mod data, loaded or newly created (empty)
	return scriptData[pItem][modName];
end

function SaveData(pItem)	-- save function works for all mods data for an item so no need to specify a mod
	sdata = Serialize(scriptData[pItem]);
	pItem:SetScriptData(sdata);
end

--serialization omitted as there are no changes since post #30

usage:
Code:
local pData = AccessData(pPlayer, "MyMod");
local x2 = pData[2].x;
pData.y = x2 * 42;
SaveData(pPlayer);

i feel its good enough for me and i leave it as that.
 
I'd like to suggest another change, this time to the serialize function. In it's current form, I can only itemize tables, which kind of defeats the whole point of itemization. The following changes to serialize solves the issue and allows me to itemize all data types.

I've created a local sub routine to avoid duplicating logic within the function, without cluttering up the global scope. It all stays nicely packaged. :)

Code:
function serialize( [COLOR="blue"]data[/COLOR] )
	
	[COLOR="Blue"]local r = "";
	local sub = function( v )[/COLOR]
		if v == false or v == true or type( v ) == "number" then
			v = tostring( v );
		else
			v = v:gsub('"', "\[QUOTE\]");
			v = v:gsub('{', "\[LCB\]");
			v = v:gsub('}', "\[RCB\]");
			v = "\""..v.."\""; -- string
		end
		[COLOR="blue"]return v;
	end

	if type(data ) ~= "table" then
		r = sub( data );
	else[/COLOR]
		r = "{ ";
		local num = 0;
		for k,v in pairs( [COLOR="blue"]data[/COLOR] ) do
			if num > 0 then
				r = r.." ";
			end
			r = r..k.."=";
			if type( v ) == "table" then
				r = r..serialize( v );
			else
				[COLOR="blue"]v = sub( v );[/COLOR]
				r = r..v;
			end
			num = num +1;
		end
		[COLOR="blue"]r = [/COLOR]r.." }";
	[COLOR="blue"]end
	return r;
end[/COLOR]

Test:
Code:
include( "SaveUtils" );

function testThis()
	
	local pPlayer = GetPlayer();
	local t =  { 1, 2, 3, t={6, {7, 7.5}, 8}, a=55, b=56, zz={"alpha", "beta \"or\" {gamma}", false} };
	
	save( "myMod",   pPlayer, "key1", "test" );
	save( "myMod",   pPlayer, "key2", 100 );
	save( "someMod", pPlayer, "key1", true );
	save( "myMod",   pPlayer, "key3", t );
	
	print( "test1: "..serialize(load( "myMod",   pPlayer, "key1" )) );
	print( "test2: "..serialize(load( "myMod",   pPlayer, "key2" )) );
	print( "test3: "..serialize(load( "someMod", pPlayer, "key1" )) );
	print( "test4: "..serialize(load( "myMod",   pPlayer, "key3" )) );
	print( "test5: "..serialize(load( nil, pPlayer )) );
end
Events.ActivePlayerTurnStart.Add( testThis );

Output:
Code:
test1: "test"
test2: 100
test3: true
test4: { 1=1 2=2 3=3 a=55 t={ 1=6 2={ 1=7 2=7.5 } 3=8 } zz={ 1="alpha" 2="beta [QUOTE]or[QUOTE] [LCB]gamma[RCB]" 3=false } b=56 }
test5: { myMod={ key1="test" key3={ 1=1 2=2 3=3 a=55 t={ 1=6 2={ 1=7 2=7.5 } 3=8 } zz={ 1="alpha" 2="beta [QUOTE]or[QUOTE] [LCB]gamma[RCB]" 3=false } b=56 } key2=100 } someMod={ key1=true } }
 
Top Bottom