--[[ How to use: Put all lua files into DatabaseRoot then call ExportDatabaseLocalText( tofile = true, newStringBank = false ) at the end of file. ( beware: all your original files will be replaced with optimized files ) If you want to exlucde some input files in DatabaseRoot, just add theirs names into ExcludedFiles --]] local Root = "." if arg and arg[0] then local s, e = string.find( arg[0], "Assets" ) if e then Root = string.sub( arg[0], 1, e ) end end Root = string.gsub( Root, '\\', '/' ) local DatabaseRoot = Root.."/Lua/config" local LuaRoot = Root.."/Lua/config" package.path = package.path..';'..DatabaseRoot..'/?.lua'..';'.. LuaRoot..'/?.lua' local EnableDatasetOptimize = true local EnableDefaultValueOptimize = true local EnableLocalization = true -- set to false to disable localization process local Database = {} local CSV --= require "std.csv" local DefaultNumberSerializedFormat = "%.14g" local NumberSerializedFormat = DefaultNumberSerializedFormat local DatabaseLocaleTextName = "_LocaleText" local StringBankOutput = DatabaseRoot.."/"..DatabaseLocaleTextName..".lua" local StringBankCSVOutput = DatabaseRoot.."/"..DatabaseLocaleTextName..".csv" local MaxStringBankRedundancy = 100 local MaxStringBankBinSize = 524288 local LocaleTextLeadingTag = '@' local MaxLocalVariableNum = 160 -- lparser.c #define MAXVARS 200 local RefTableName = "__rt" local DefaultValueTableName = "__default_values" local PrintTableRefCount = false local UnknownName = "___noname___" local floor = math.floor local fmod = math.fmod local ExcludedFiles = { --Add file name to exclude from build _LocaleText = true, } local UniquifyTables = {} -- hash -> table local UniquifyTablesIds = {} -- id -> hash local UniquifyTablesInvIds = {} -- table -> id local UniquifyTablesRefCounter = {} -- table -> refcount local function HashString( v ) local val = 0 local fmod = fmod local gmatch = string.gmatch local byte = string.byte local MaxStringBankBinSize = MaxStringBankBinSize local c for _c in gmatch( v, "." ) do c = byte( _c ) val = val + c * 193951 val = fmod( val, MaxStringBankBinSize ) val = val * 399283 val = fmod( val, MaxStringBankBinSize ) end return val end local function AddStringToBank( stringBank, str ) local meta = getmetatable( stringBank ) local reversed = nil local counter = nil if not meta then meta = { __counter = { used = {} }, -- mark used hash value __reversed = {} -- string -> hash reverse lookup } reversed = meta.__reversed counter = meta.__counter setmetatable( stringBank, meta ) local remove = {} -- lazy initialize reverse lut for h, s in pairs( stringBank ) do local _h = reversed[ s ] assert( _h == nil ) reversed[ s ] = h end end reversed = reversed or meta.__reversed counter = counter or meta.__counter local hash = reversed[ str ] if hash then counter.used[ hash ] = true return hash end hash = HashString( str ) local _v = stringBank[ hash ] while _v do hash = hash + 1 hash = fmod( hash, MaxStringBankBinSize ) _v = stringBank[ hash ] end assert( not reversed[ str ] ) stringBank[ hash ] = str reversed[ str ] = hash counter.used[ hash ] = true return hash end local function OrderedForeach( _table, _func ) if type( _table ) == "table" then local kv = {} for k, v in pairs( _table ) do kv[ #kv + 1 ] = { k, v } end table.sort( kv, function( _l, _r ) local l = _l[ 1 ] local r = _r[ 1 ] local lt = type( l ) local rt = type( r ) if lt == rt and lt ~= "table" then return l < r else return tostring( l ) < tostring( r ) end end ) for _, _v in ipairs( kv ) do local k = _v[ 1 ] local v = _v[ 2 ] _func( k, v ) end end end local function OrderedForeachByValue( _table, _func ) if type( _table ) == "table" then local kv = {} for k, v in pairs( _table ) do kv[ #kv + 1 ] = { k, v } end table.sort( kv, function( _l, _r ) local l = _l[ 2 ] local r = _r[ 2 ] local lt = type( l ) local rt = type( r ) if lt == rt and lt ~= "table"then return l < r else return tostring( l ) < tostring( r ) end end ) for _, _v in ipairs( kv ) do local k = _v[ 1 ] local v = _v[ 2 ] if not pcall( _func, k, v ) then return false end end return true end end local function EncodeEscapeString( s ) local buf = {} buf[#buf + 1] = "\"" string.gsub( s, ".", function ( c ) if c == '\n' then buf[#buf + 1] = "\\n" elseif c == '\t' then buf[#buf + 1] = "\\t" elseif c == '\r' then buf[#buf + 1] = "\\r" elseif c == '\a' then buf[#buf + 1] = "\\a" elseif c == '\b' then buf[#buf + 1] = "\\b" elseif c == '\\' then buf[#buf + 1] = "\\\\" elseif c == '\"' then buf[#buf + 1] = "\\\"" elseif c == '\'' then buf[#buf + 1] = "\\\'" elseif c == '\v' then buf[#buf + 1] = "\\\v" elseif c == '\f' then buf[#buf + 1] = "\\\f" else buf[#buf + 1] = c end end ) buf[#buf + 1] = "\"" return table.concat( buf, "" ) end local function StringBuilder() local sb = {} local f = function( str ) if str then sb[ #sb + 1 ] = str end return f, sb end return f end local function CreateFileWriter( fileName, mode ) local file = nil local indent = 0 if mode and fileName then local _file, err = io.open( fileName ) if _file ~= nil then --print( "remove file "..fileName ) os.remove( fileName ) end file = io.open( fileName, mode ) end local ret = nil if file then ret = { write = function( ... ) if indent > 0 then for i = 0, indent - 1 do file:write( "\t" ) end end return file:write( ... ) end, close = function( ... ) return file:close() end } else ret = { write = function( ... ) for i = 0, indent - 1 do io.write( "\t" ) end return io.write( ... ) end, close = function( ... ) end } end ret.indent = function( count ) count = count or 1 indent = indent + count or 1 end ret.outdent = function( count ) count = count or 1 if indent >= count then indent = indent - count end end return ret end local function SetNumberSerializedFormat( f ) NumberSerializedFormat = f or DefaultNumberSerializedFormat if NumberSerializedFormat == "" then NumberSerializedFormat = DefaultNumberSerializedFormat end print( "set NumberSerializedFormat: ".. NumberSerializedFormat ) end local DefaultVisitor = { recursive = true, iVisit = function( i, v, curPath ) print( string.format( "%s[%d] = %s", curPath, i, tostring( v ) ) ) return true end, nVisit = function( n, v, curPath ) print( string.format( "%s[%g] = %s", curPath, n, tostring( v ) ) ) return true end, sVisit = function( s, v, curPath ) local _v = tostring( v ) print( #curPath > 0 and curPath.."."..s.." = ".._v or s.." = ".._v ) return true end, xVisit = function( k, v, curPath ) local sk = tostring( k ) local sv = tostring( v ) print( #curPath > 0 and curPath.."."..sk.." = "..sv or sk.." = "..sv ) return true end } local function WalkDataset( t, visitor, parent ) if not parent then parent = "" end -- all integer key local continue = true if visitor.iVisit then for i, v in ipairs( t ) do local _t = type( v ) if _t == "table" and visitor.recursive then continue = WalkDataset( v, visitor, string.format( "%s[%g]", parent, i ) ) elseif _t == "string" or _t == "number" then continue = visitor.iVisit( i, v, parent ) else -- not support value type if visitor.xVisit then continue = visitor.xVisit( i, v, parent ) end end if not continue then return continue end end end local len = #t local keys = {} local idict = {} for k, v in pairs( t ) do local _t = type( k ) if _t == "number" then local intKey = k == math.floor( k ); if k > len or k <= 0 or not intKey then idict[k] = v end elseif _t == "string" then keys[#keys + 1] = k else --table, function, ... --not support data type for key if visitor.xVisit then continue = visitor.xVisit( k, v, parent ) end end if not continue then return continue end end -- for all number keys those are not in array part -- key must be number for k, v in pairs( idict ) do local intKey = k == math.floor( k ); local _t = type( v ) if _t ~= "table" then if _t == "number" or _t == "string" then if intKey then if visitor.iVisit then continue = visitor.iVisit( k, v, parent ) end else if visitor.nVisit then continue = visitor.nVisit( k, v, parent ) end end else -- not support value data type if visitor.xVisit then continue = visitor.xVisit( k, v, parent ) end end elseif visitor.recursive then if intKey then continue = WalkDataset( v, visitor, string.format( "%s[%d]", parent, k ) ) else continue = WalkDataset( v, visitor, string.format( "%s[%g]", parent, k ) ) end end if not continue then return continue end end -- sort all string keys table.sort( keys ) -- for all none-table value local tableValue for k, v in pairs( keys ) do local value = t[v] local _t = type( value ) if _t == "number" or _t == "string" then -- print all number or string value here if visitor.sVisit then continue = visitor.sVisit( v, value, parent ) end elseif _t == "table" then -- for table value if not tableValue then tableValue = {} end tableValue[ k ] = v else if visitor.xVisit then continue = visitor.xVisit( v, value, parent ) end end if not continue then return continue end end if visitor.recursive then -- for all table value if tableValue then for k, v in pairs( tableValue ) do local value = t[v] continue = WalkDataset( value, visitor, #parent > 0 and parent.."."..v or v ) if not continue then return continue end end end end return continue end local function PrintDataset( t, parent ) if not parent then parent = "" end local string_format = string.format -- all integer key for i, v in ipairs( t ) do local _t = type( v ) if _t == "table" then PrintDataset( v, string_format( "%s[%g]", parent, i ) ) elseif _t == "string" or _t == "number" then print( string.format( "%s[%d] = %s", parent, i, tostring( v ) ) ) else -- not support value type end end local len = #t local keys = {} local idict = {} for k, v in pairs( t ) do local _t = type( k ) if _t == "number" then if k > len or k <= 0 then idict[k] = v end elseif _t == "string" then keys[#keys + 1] = k else --table, function, ... --not support data type for key end end -- for all number keys those are not in array part -- key must be number for k, v in pairs( idict ) do local intKey = k == math.floor( k ) local _t = type( v ) if _t ~= "table" then if _t == "number" or _t == "string" then if intKey then print( string_format( "%s[%d] = %s", parent, k, tostring( v ) ) ) else print( string_format( "%s[%g] = %s", parent, k, tostring( v ) ) ) end else -- not support value data type end else if intKey then PrintDataset( v, string_format( "%s[%d]", parent, k ) ) else PrintDataset( v, string_format( "%s[%g]", parent, k ) ) end end end -- sort all string keys table.sort( keys ) -- for all none-table value local tableValue for k, v in pairs( keys ) do local value = t[v] local _t = type( value ) if _t ~= "table" then -- print all number or string value here local _value = tostring( value ) print( #parent > 0 and parent.."."..v.." = ".._value or v.." = ".._value ) else -- for table value if not tableValue then tableValue = {} end tableValue[ k ] = v end end -- for all table value if tableValue then for k, v in pairs( tableValue ) do local value = t[v] PrintDataset( value, #parent > 0 and parent.."."..v or v ) end end end local function DeserializeTable( val ) local loader = loadstring or load -- lua5.2 compat local chunk = loader( "return " .. val ) local ok, ret = pcall( chunk ) if not ok then ret = nil print( "DeserializeTable failed!"..val ) end return ret end local function _SerializeTable( val, name, skipnewlines, campact, depth, tableRef ) local valt = type( val ) depth = depth or 0 campact = campact or false local append = StringBuilder() local eqSign = " = " local tmp = "" local string_format = string.format if not campact then append( string.rep( "\t", depth ) ) skipnewlines = skipnewlines or false else skipnewlines = true eqSign = "=" end if name then local nt = type( name ) if nt == "string" then if name ~= "" then if string.match( name,'^%d+' ) then append( "[\"" ) append( name ) append( "\"]" ) else append( name ) end else append( "[\"\"]" ) end append( eqSign ) elseif nt == "number" then append( string_format( "[%s]", tostring( name ) ) ) append( eqSign ) else tmp = tmp .. "\"[inserializeable datatype for key:" .. nt .. "]\"" end end local ending = not skipnewlines and "\n" or "" if tableRef then local refName = tableRef[ val ] if refName then valt = "ref" val = refName end end if valt == "table" then append( "{" ) append( ending ) local array_part = {} local count = 0 for k, v in ipairs( val ) do if type( val ) ~= "function" then array_part[k] = true if count > 0 then append( "," ) append( ending ) end append( _SerializeTable( v, nil, skipnewlines, campact, depth + 1, tableRef ) ) count = count + 1 end end local sortedK = {} for k, v in pairs( val ) do if type( v ) ~= "function" then if not array_part[k] then sortedK[#sortedK + 1] = k end end end table.sort( sortedK ) for i, k in ipairs( sortedK ) do local v = val[k] if count > 0 then append( "," ) append( ending ) end append( _SerializeTable( v, k, skipnewlines, campact, depth + 1, tableRef ) ) count = count + 1 end if count >= 1 then append( ending ) end if not campact then append( string.rep( "\t", depth ) ) end append( "}" ) elseif valt == "number" then if DefaultNumberSerializedFormat == NumberSerializedFormat or math.floor( val ) == val then append( tostring( val ) ) else append( string_format( NumberSerializedFormat, val ) ) end elseif valt == "string" then append( EncodeEscapeString( val ) ) elseif valt == "boolean" then append( val and "true" or "false" ) elseif valt == "ref" then append( val or "nil" ) else tmp = tmp .. "\"[inserializeable datatype:" .. valt .. "]\"" end local _, slist = append() return table.concat( slist, "" ) end local function SerializeTable( val, skipnewlines, campact, tableRef, name ) getmetatable( "" ).__lt = function( a, b ) return tostring( a ):lower() < tostring( b ):lower() end local ret = _SerializeTable( val, name, skipnewlines, campact, 0, tableRef ) getmetatable( "" ).__lt = nil return ret end local function DumpStringBank( stringBank ) print( 'dump database local string bank begin...' ) for k, v in pairs( stringBank ) do print( string.format( "\t[%g] = %s", k, v ) ) end print( 'dump database local string bank end.' ) end local function SaveStringBankToLua( stringBank, tofile ) if tofile then local fileName = StringBankOutput local _file, err = io.open( fileName ) if _file ~= nil then _file:close() os.remove( fileName ) end file = io.open( fileName, "w" ) local fmt = string.format file:write( fmt( "local %s = {\n", DatabaseLocaleTextName ) ) for k, v in pairs( stringBank ) do file:write( fmt( "\t[%g] = %s,\n", k, EncodeEscapeString( v ) ) ) end file:write( "}\n" ) file:write( fmt( "return %s\n--EOF", DatabaseLocaleTextName ) ) file:close() else DumpStringBank( stringBank ) end end local function SaveStringBankToCSV( stringBank, tofile ) local _exists = {} for k, v in pairs( stringBank ) do assert( not _exists[v] ) _exists[ v ] = k end local csv = CSV if tofile and csv then local fileName = StringBankCSVOutput local _file, err = io.open( fileName ) if _file ~= nil then _file:close() os.remove( fileName ) end local t = {} local count = 1 for k, v in pairs( stringBank ) do t[count] = { k, v } count = count + 1 end table.sort( t, function( a, b ) return a[1] < b[1] end ) csv.save( fileName, t, true ) else SaveStringBankToLua( stringBank, tofile ) end end local function LoadStringBankFromLua( info ) local stringBank = {} local chunk = loadfile( StringBankOutput ) if chunk then print( 'load string bank: '..StringBankOutput ) local last = chunk() if last and type( last ) == "table" then for k, v in pairs( last ) do stringBank[ k ] = v end end end return stringBank end local function LoadStringBankFromCSV() local stringBank = {} local csv = CSV if csv then local fileName = StringBankCSVOutput local file, err = io.open( fileName ) if file then print( "Load StringBank: " .. fileName ) file:close() local b = csv.load( fileName, true ) local allstr = {} for i = 1, #b do local key = b[ i ][ 1 ] local value = b[ i ][ 2 ] local oldHash = allstr[ value ] if not oldHash then stringBank[ key ] = value allstr[ value ] = key else print( string.format( "\"%s\" already exists in StringBank with hash %d", value, oldHash ) ) end end end else return LoadStringBankFromLua() end return stringBank end local function TrimStringBank( stringBank ) -- remove useless values local meta = getmetatable( stringBank ) if meta then local counter = meta.__counter if counter then local used = counter.used if used then local count = 0 for hash, str in pairs( stringBank ) do count = count + 1 end local unused = {} for hash, str in pairs( stringBank ) do if not used[ hash ] then unused[#unused + 1] = hash end end if #unused > MaxStringBankRedundancy then for _, h in ipairs( unused ) do stringBank[ h ] = nil end end end end end end local function GetAllFileNamesAtPath( path ) path, _ = path:gsub( "/", "\\" ) local ret = {} for dir in io.popen( string.format( "dir \"%s\" /S/b", path ) ):lines() do local s, e, f = dir:find( ".+\\(.+)%.lua$" ) if f then table.insert( ret, f ) end end table.sort( ret ) return ret end local function LoadDataset( name ) if not Database then _G["Database"] = {} Database.loaded = {} end local loader = function( name ) Database.loaded = Database.loaded or {} local r = Database.loaded[name] if r then return r end local pname = string.gsub( name, "%.", "/" ) local split = function( s, p ) local rt= {} string.gsub( s, '[^'..p..']+', function( w ) table.insert( rt, w ) end ) return rt end local curName = pname..".lua" local fileName = DatabaseRoot.."/"..curName local checkFileName = function( path, name ) path, _ = path:gsub( "/", "\\" ) local _name = string.lower( name ) for dir in io.popen( string.format( "dir \"%s\" /s/b", path ) ):lines() do local s, e, f = dir:find( ".+\\(.+%.lua)$" ) if f then local _f = string.lower( f ) if _name == _f then return name == f, f -- not match, real name end end end end local m, real = checkFileName( DatabaseRoot, curName ) if not m and real then local msg = string.format( "filename must be matched by case! realname: \"%s\", you pass: \"%s\"", real, curName ) print( msg ) os.execute( "pause" ) end local chunk = loadfile( fileName ) if not chunk then fileName = LuaRoot.."/"..pname..".lua" chunk, err = loadfile( fileName ) if err then print( "\n\n" ) print( "----------------------------------" ) print( "Load lua failed: "..fileName ) print( "Error:" ) print( "\t"..err ) print( "----------------------------------" ) print( "\n\n" ) end end print( fileName ) assert( chunk ) if not chunk then os.execute( "pause" ) end local rval = chunk() if rval.__name ~= nil then os.execute( "pause table's key must not be '__name' which is the reserved keyword." ) end if rval.__sourcefile ~= nil then os.execute( "pause table's key must not be '__sourcefile' which is the reserved keyword." ) end rval.__name = name rval.__sourcefile = fileName local namespace = Database local ns = split( pname, '/' ) local xname = ns[#ns] -- last one if #ns > 1 then for i = 1, #ns - 1 do local n = ns[i]; if namespace[n] == nil then namespace[n] = {} end namespace = namespace[n] end end namespace[xname] = rval print( "dataset: "..name.." has been loaded" ) Database.loaded[name] = rval return rval end return loader( name ) end local function CheckNotAscii( v ) if v ~= nil and type( v ) == "string" then local byte = string.byte for _c in string.gmatch( v, "." ) do local c = byte( _c ) if c < 0 or c > 127 then return true end end end return false end local function LocalizeRecord( id, record, genCode, StringBank ) local localized_fields = nil local subTable = nil OrderedForeach( record, function( k, v ) local vt = type( v ) if vt == "string" then if CheckNotAscii( v ) then if #v > 0 and string.sub( v, 1, 1 ) == LocaleTextLeadingTag then print( string.format( "invalid leading character for localized text! key, value: %s, %s", k, v ) ) os.execute( "pause" ) end if not localized_fields then localized_fields = {} end -- build localized id string with tag local sid = AddStringToBank( StringBank, v ) localized_fields[ k ] = string.format( "%s%g", LocaleTextLeadingTag, sid ) if genCode then genCode[ #genCode + 1 ] = { id, sid, v } end end elseif vt == "table" then if not subTable then subTable = {} end subTable[ #subTable + 1 ] = v end end ) local localized = false if localized_fields then -- override localized string with tag localized = true for k, v in pairs( localized_fields ) do record[ k ] = localized_fields[ k ] end end if subTable then for _, sub in ipairs( subTable ) do localized = LocalizeRecord( 0, sub, genCode, StringBank ) or localized end end return localized end local function GetValueTypeNameCS( value ) local t = type( value ) if t == "string" then return "string" elseif t == "number" then if value == math.floor( value ) then return "int" else return "float" end elseif t == "boolean" then return "bool" elseif t == "table" then return "table" else return "void" end end local function UniquifyTable( t ) if t == nil or type( t ) ~= "table" then return nil end local hash = SerializeTable( t, true, true ) local ref = UniquifyTables[ hash ] if ref then local refcount = UniquifyTablesRefCounter[ ref ] or 1 UniquifyTablesRefCounter[ ref ] = refcount + 1 return ref end local overwrites = nil for k, v in pairs( t ) do overwrites = overwrites or {} if type( v ) == "table" then overwrites[ k ] = UniquifyTable( v ) end end if overwrites then for k, v in pairs( overwrites ) do t[ k ] = overwrites[ k ] end end local id = #UniquifyTablesIds + 1 UniquifyTablesIds[ id ] = hash UniquifyTables[ hash ] = t UniquifyTablesInvIds[ t ] = id UniquifyTablesRefCounter[ t ] = 1 return t end local function OptimizeDataset( dataset ) local ids = {} local names = {} local idType = nil -- choose cs data type -- for all fields in a record local typeNameTable = {} for k, v in pairs( dataset ) do local _sk = tostring( k ) if _sk ~= "__name" and _sk ~= "__sourcefile" then if not idType then idType = type( k ) end if idType == type( k ) then ids[ #ids + 1 ] = k end end end if EnableDefaultValueOptimize then -- find bigest table to generate all fields local majorItem = 1 local f = 0 for k, v in pairs( dataset ) do if type( v ) == "table" then local num = 0 for _, _ in pairs( v ) do num = num + 1 end if num > f then f = num majorItem = k end end end local v = dataset[ majorItem ] if type( v ) == "table" then for name, value in pairs( v ) do local nt = type( name ) if nt == "string" and name == "id" then print( "this table already has a field named 'id'" ) end if nt == "string" then names[ #names + 1 ] = name end end table.sort( names, function( a, b ) return a:lower() < b:lower() end ) for i, field in ipairs( names ) do -- for all record / row for r, t in ipairs( ids ) do local record = dataset[ t ] if record[ field ] ~= nil then local v = record[ field ] local curType = GetValueTypeNameCS( v ) if not typeNameTable[ field ] then typeNameTable[ field ] = curType elseif typeNameTable[ field ] == "int" and curType == "float" then -- overwrite int to float typeNameTable[ field ] = curType elseif curType == "table" then -- overwrite to table typeNameTable[ field ] = curType end end end -- patching miss fields with default values local curType = typeNameTable[ field ] for r, t in ipairs( ids ) do local record = dataset[ t ] local v = record[ field ] if v == nil then local ft = typeNameTable[ field ] if ft == "string" then v = "" elseif ft == "number" or ft == "int" or ft == "float" then v = 0 elseif ft == "table" then v = {} elseif ft == "bool" then v = false end record[ field ] = v end end end end end ids = {} idType = nil UniquifyTables = {} UniquifyTablesIds = {} UniquifyTablesInvIds = {} UniquifyTablesRefCounter = {} local isIntegerKey = true local overwrites = nil OrderedForeach( dataset, function( k, v ) local _sk = tostring( k ) if _sk ~= "__name" and _sk ~= "__sourcefile" then if not idType then idType = type( k ) end -- check type if idType == type( k ) then ids[ #ids + 1 ] = k if idType == "number" then if isIntegerKey then isIntegerKey = k == floor( k ) end end end if type( v ) == "table" then overwrites = overwrites or {} overwrites[ k ] = UniquifyTable( v ) end end end ) if overwrites then for k, v in pairs( overwrites ) do dataset[ k ] = overwrites[ k ] end end local returnVal = nil if EnableDefaultValueOptimize then local defaultValues = nil for i, field in ipairs( names ) do local curType = typeNameTable[ field ] -- for all record/row local defaultValueStat = { } for r, t in ipairs( ids ) do local record = dataset[ t ] local v = record[ field ] if v ~= nil then local vcount = defaultValueStat[ v ] or 0 defaultValueStat[ v ] = vcount + 1 else assert( "default value missing!" ) end end -- find the mostest used as a default value local max = -1 local defaultValue = nil local _defaultValue = "{}" local result = OrderedForeachByValue( defaultValueStat, function( value, count ) if count >= max then if count > max then max = count defaultValue = value _defaultValue = SerializeTable( defaultValue, true, true ) else if curType == "table" then local _value = SerializeTable( value, true, true ) if #_value > #_defaultValue then defaultValue = value _defaultValue = SerializeTable( defaultValue, true, true ) end else local _value = value local _defaultValue = defaultValue if type( value ) == 'boolean' then _value = value and 1 or 0 _defaultValue = defaultValue and 1 or 0 end if _value < _defaultValue then defaultValue = value _defaultValue = SerializeTable( defaultValue, true, true ) end end end end end ) if not result then error( string.format( "create default value for \"%s\" failed. please make sure all the value's types are the same.", field ) ) end if defaultValue ~= nil then defaultValues = defaultValues or {} defaultValues[ field ] = defaultValue end end returnVal = defaultValues end -- remove tables whose's ref is 1 and re-mapping id local newid = 1 local newIds = {} local newInvIds = {} OrderedForeach( UniquifyTablesIds, function( id, hash ) local table = UniquifyTables[ hash ] local refcount = UniquifyTablesRefCounter[ table ] if refcount == 1 then UniquifyTables[ hash ] = nil else newIds[ newid ] = hash newInvIds[ table ] = newid newid = newid + 1 end end ) UniquifyTablesIds = newIds UniquifyTablesInvIds = newInvIds return returnVal end local function ToUniqueTableRefName( id ) if id <= MaxLocalVariableNum then return string.format( RefTableName.."_%d", id ) else return string.format( RefTableName.."[%d]", id - MaxLocalVariableNum ) end end local function SaveDatasetToFile( dataset, tofile, tableRef, name ) if tofile then outFile = CreateFileWriter( dataset.__sourcefile, "w" ) else outFile = CreateFileWriter() end local ptr2ref = nil if tableRef and tableRef.ptr2ref then ptr2ref = tableRef.ptr2ref end if tableRef and tableRef.name2value then local name2table = tableRef.name2value local tables = tableRef.tables local tableIds = tableRef.tableIds local ptr2ref = tableRef.ptr2ref local refcounter = tableRef.refcounter local maxLocalVariableNum = tableRef.maxLocalVariableNum or MaxLocalVariableNum local refTableName = tableRef.refTableName or "__rt" local tableNum = #tableIds for id, hash in ipairs( tableIds ) do local table = tables[ hash ] if table and id <= maxLocalVariableNum then local refname = ptr2ref[ table ] -- temp comment out top level ref ptr2ref[ table ] = nil local refcount = refcounter[ table ] outFile.write( string.format( "%slocal %s = %s\n", PrintTableRefCount and string.format( "--ref:%d\n", refcount ) or "", refname, SerializeTable( table, false, false, ptr2ref ) ) ) ptr2ref[ table ] = refname else break end end if tableNum > maxLocalVariableNum then local maxCount = tableNum - maxLocalVariableNum outFile.write( string.format( "local %s = createtable and createtable( %d, 0 ) or {}\n", refTableName, maxCount ) ) for id = maxLocalVariableNum + 1, tableNum do local offset = id - maxLocalVariableNum local hash = tableIds[ id ] local table = tables[ hash ] local refname = ptr2ref[ table ] -- temp comment out top level ref ptr2ref[ table ] = nil local refcount = refcounter[ table ] outFile.write( string.format( "%s%s[%d] = %s\n", PrintTableRefCount and string.format( "-- %s, ref:%d\n", refname, refcount ) or "", refTableName, offset, SerializeTable( table, false, false, ptr2ref ) ) ) ptr2ref[ table ] = refname end end end local datasetName = dataset.__name or name if not datasetName then datasetName = UnknownName dataset.__name = datasetName end outFile.write( string.format( "local %s = \n", datasetName ) ) -- remove none table value local removed = nil for k, v in pairs( dataset ) do if type( v ) ~= "table" then removed = removed or {} removed[ #removed + 1 ] = k end end if removed then for _, k in ipairs( removed ) do dataset[ k ] = nil end end outFile.write( SerializeTable( dataset, false, false, ptr2ref ) ) outFile.write( "\n" ) if tableRef and tableRef.postOutput then tableRef.postOutput( outFile ) end outFile.write( string.format( "\nreturn %s\n", datasetName ) ) outFile.close() end local function ExportOptimizedDataset( t, StringBank ) local datasetName = t.__name if not datasetName then datasetName = UnknownName t.__name = datasetName end local localized = false local genCode = false if EnableLocalization then OrderedForeach( t, function( id, _record ) if type( _record ) == "table" then localized = LocalizeRecord( id, _record, genCode, StringBank ) or localized end end ) end local tableRef = nil local defaultValues = nil if EnableDatasetOptimize then defaultValues = OptimizeDataset( t ) if defaultValues then local removeDefaultValues = function( record ) local removes = nil local adds = nil for field, defaultVal in pairs( defaultValues ) do local value = record[ field ] local hasValue = true if value == nil then assert( false, "OptimizeDataset should patch all missing fields!" ) hasValue = false end if value == defaultVal and hasValue then removes = removes or {} removes[ #removes + 1 ] = field else adds = adds or {} adds[ field ] = value end end -- remove fields with default value if removes then for _, f in ipairs( removes ) do record[ f ] = nil end end -- patch fields with none-default value if adds then for f, v in pairs( adds ) do record[ f ] = v end end end local removed = {} for _, record in pairs( t ) do if type( record ) == "table" then if not removed[ record ] then removeDefaultValues( record ) removed[ record ] = true end end end end local reftables = nil local ptr2ref = nil -- create ref table: table -> refname for _, hash in pairs( UniquifyTablesIds ) do local t = UniquifyTables[ hash ] if t then local refName = ToUniqueTableRefName( UniquifyTablesInvIds[ t ] ) reftables = reftables or {} ptr2ref = ptr2ref or {} reftables[ refName ] = t ptr2ref[ t ] = refName end end tableRef = { name2value = reftables, tables = UniquifyTables, -- hash -> table tableIds = UniquifyTablesIds, -- id -> hash ptr2ref = ptr2ref, -- table -> refname refcounter = UniquifyTablesRefCounter, -- table -> refcount maxLocalVariableNum = MaxLocalVariableNum, refTableName = RefTableName, postOutput = function( outFile ) if defaultValues then outFile.write( string.format( "local %s = %s\n", DefaultValueTableName, SerializeTable( defaultValues, false, false, ptr2ref ) ) ) outFile.write( "do\n" ) outFile.write( string.format( "\tlocal base = { __index = %s, __newindex = function() error( \"Attempt to modify read-only table\" ) end }\n", DefaultValueTableName ) ) outFile.write( string.format( "\tfor k, v in pairs( %s ) do\n", datasetName ) ) outFile.write( "\t\tsetmetatable( v, base )\n" ) outFile.write( "\tend\n" ) outFile.write( "\tbase.__metatable = false\n" ) outFile.write( "end\n" ) end end } end return t, tableRef, localized end --tofile: not output to file, just for debug --newStringBank: if false, exporter will use existing string hash for increamental building local function ExportDatabaseLocalText( tofile, newStringBank ) local StringBank = nil if newStringBank then StringBank = {} else StringBank = LoadStringBankFromCSV() end StringBank = StringBank or {} local localized_dirty = false local files = GetAllFileNamesAtPath( DatabaseRoot ) for _, v in ipairs( files ) do if not ExcludedFiles[ v ] then print( "LoadDataset :"..v ) LoadDataset( v ) local t = Database[ v ] local localized = false if t then local _t, tableRef, localized = ExportOptimizedDataset( t, StringBank ) assert( _t == t ) localized_dirty = localized_dirty or localized SaveDatasetToFile( Database[ v ], tofile, tableRef ) end end end TrimStringBank( StringBank ) if localized_dirty then SaveStringBankToCSV( StringBank, tofile ) else print( "\nDatabase LocaleText is up to date.\n" ) end print( "Database Exporting LocaleText done." ) end --[[ local test = { { 1, 2, 3, a = "123", b = "123" }, { 1, 2, 3, a = "123", b = "123" }, { 1, 2, 5, a = "123", b = "123" }, [9] = { 1, 2, 5, a = "123", b = "123" }, [100] = { 1, 2, 3, a = "tttt", b = "123" }, [11] = { 1, 2, 3, a = "123", b = "123", c = { {1}, {1}, {2}, {2} }, d = { { a = 1, 1 }, { a = 2, 2 } }, e = { { a = 1, 1 }, { a = 2, 2 } }, } } EnableDefaultValueOptimize = false local _localizedText = {} local _src = SerializeTable( test ) local _clone = DeserializeTable( _src ) print( _src ) local t, tableRef = ExportOptimizedDataset( test, _localizedText ) assert( t == test ) --print( SerializeTable( t ) ) SaveDatasetToFile( t, false, tableRef, "test" ) local _dst = SerializeTable( t ) print( _dst ) assert( _src == _dst ) EnableDefaultValueOptimize = true _localizedText = {} t, tableRef = ExportOptimizedDataset( _clone, _localizedText ) assert( t == _clone ) --_clone.__name = "__cloned_test" SaveDatasetToFile( _clone, false, tableRef ) local _dst = SerializeTable( _clone ) print( _dst ) print( _src ~= _dst ) --]] ExportDatabaseLocalText( true )