make module/package.seeall use async_scope instead of _G

cjson = require"cjson"

ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=[];',./!@#$%^&*()_+{}|:\"<>?\\`~ "
DEFAULT_FONT = "12px sans-serif"

-- disable invalid functions
os.remove = -> error "os.remove is disabled"
os.rename = -> error "os.rename is disabled"
os.execute = -> error "os.execute is disabled"
os.exit = -> error "os.exit is disabled"
io.tmpfile = -> error "io.tmpfile is disabled"

post_message = (o) ->
  o = cjson.encode o if type(o) == "table"
  nacl.post_message o

printer = (channel="std_out") ->
  (...) ->
    args = {...}
    str = table.concat [tostring s for s in *args], "\t"
    post_message { channel, str }

async_print = printer"std_out"
async_err = printer"std_err"

msg_id = 0
msg_responses = {}

request_response = (msg) ->
  id = msg_id
  msg_id += 1
  post_message { "request", id, msg }
  while true
    msg = coroutine.yield!
    if msg == "response" and msg_responses[id]
      return with msg_responses[id]
        msg_responses[id] = nil

safe_resume = (co, ...) ->
  success, error_msg = coroutine.resume co, ...
  if not success
    async_err debug.traceback co, error_msg
    nacl.set_game_thread nil
    false
  else
    true

async_require = (module_name) ->
  mod = package.loaded[module_name]
  return mod if mod

  loader = if package.preload[module_name]
    package.preload[module_name]
  else
    msg = request_response { "require", module_name }
    status, code = unpack msg
    return async_err code if status == "error"
    assert loadstring code, module_name

  setfenv loader, getfenv 2
  mod = loader module_name
  mod = true if mod == nil
  package.loaded[module_name] = mod
  mod

nacl.lib = {
  core: nacl
  prefetch: (files) ->
    tuples = {}

    add_files = (list, t="text") ->
      for k, file_or_list in pairs list
        if type(file_or_list) == "table"
          add_files file_or_list, k
        else
          table.insert tuples, { t, file_or_list }

    add_files files

    return if #tuples == 0
    status, msg = unpack request_response { "prefetch", tuples }
    error "prefetch: " .. msg if status != "success"

  track_event: (category, action, label=nil, value=nil, interactive=true) ->
    post_message { "track_event", { category, action, label, value, interactive } }
}

async_scope = nil
nacl_package_seeall = (mod) ->
  setmetatable mod, __index: async_scope

module = module

-- scope of the game thread
async_scope = setmetatable {
    print: async_print
    require: async_require
    nacl: nacl.lib

    -- make it so module creates tables in async_scope instead of _G
    module: (...) ->
      _G = _G
      setfenv 0, async_scope
      out = { module ... }
      setfenv 0, _G
      unpack out

    package: setmetatable {
      seeall: nacl_package_seeall
    }, __index: package
  }, {
    __index: _G
  }

game_thread = nil -- to prevent it from being garbage collected

nacl.show_error = async_err -- used by C to send errors to client

nacl.handle_message = (msg) ->
  error "unknown msg: " .. tostring(msg) if type(msg) != "string"
  print ">>", msg\sub 1, 120

  msg = cjson.decode msg

  switch msg[1]
    -- create a new execution thread
    -- throws out the old one
    when "execute"
      fn, err = loadstring msg[2], "execute"
      if not fn
        async_err err
      else
        setfenv fn, async_scope
        run = -> aroma.run fn

        game_thread = coroutine.create run
        nacl.set_game_thread game_thread
        safe_resume game_thread

    when "response"
      _, id, data = unpack msg
      msg_responses[id] = data
      safe_resume game_thread, "response"
    else
      error "Don't know how to handle message: " .. (msg[1] or msg)

img_cache = {}

image_data_from_url = (url) ->
  msg = request_response { "image", url }
  status, bytes, width, height = unpack msg
  if status == "success"
    nacl.image_data_from_byte_string bytes, width, height
  else
    error bytes

class AudioSource
  new: (@source_id, @url) =>
  msg: (...) => post_message { "audio", @source_id, ... }

  play: => @msg "play"
  stop: => @msg "stop"
  rewind: => @msg "rewind"
  setLooping: (should_loop) => @msg "set_looping", should_loop

  __tostring: => "HTMLAudioSource<" .. @url .. ">"

nacl.init = {
  graphics: =>
    old_new_image = @newImage

    @newImage = (url, ...) ->
      return old_new_image url, ... if type(url) != "string"
      return img_cache[url] if img_cache[url]

      img_data = image_data_from_url url
      img = old_new_image img_data
      img_cache[url] = img
      img

    @newFont = (font_str, alphabet=ALPHABET) ->
      glyphs = request_response { "font", font_str, alphabet }
      cache = aroma.font.newGlyphCache!
      for g in *glyphs
        letter, data_string, w, h = unpack g
        data = nacl.image_data_from_byte_string data_string, w, h
        cache\add_glyph letter\byte!, data
      cache\to_font!

    aroma.boot = ->
      if not self.getFont!
        self.setFont self.newFont DEFAULT_FONT

  audio: =>
    @newSource = (url, kind="static") ->
      msg = request_response { "audio", url, kind }
      status, source_id, err_msg = unpack msg
      if status == "success"
        AudioSource source_id, url
      else
        error error_msg

  image: =>
    old_new_image_data = @newImageData
    @newImageData = (...) ->
      args = {...}
      old_new_image_data ... if type(args[1]) != "string"
      image_data_from_url args[1]
}

functions_to_reset = {"draw", "update", "keypressed", "keyreleased", "focus", "load"}
default_modules = { name, true for name in pairs package.loaded }

nacl.init_all = (aroma) ->
  aroma.run = (setup_fn) ->
    -- unload any modules
    for key in pairs package.loaded
      package.loaded[key] = nil unless default_modules[name]

    blank = ->
    aroma[key] = blank for key in *functions_to_reset
    aroma.graphics.reset!
    aroma.boot!

    setup_fn!

    while true -- listen for game messages
      msg = {coroutine.yield!}
      if aroma[msg[1]]
        name = table.remove msg, 1
        aroma[name] unpack msg
      -- else
      --   print "unknown message"
      --   print unpack msg

cjson = require"cjson"

ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=[];',./!@#$%^&*()_+{}|:\"<>?\\`~ "
DEFAULT_FONT = "12px sans-serif"

-- disable invalid functions
os.remove = -> error "os.remove is disabled"
os.rename = -> error "os.rename is disabled"
os.execute = -> error "os.execute is disabled"
os.exit = -> error "os.exit is disabled"
io.tmpfile = -> error "io.tmpfile is disabled"

post_message = (o) ->
  o = cjson.encode o if type(o) == "table"
  nacl.post_message o

printer = (channel="std_out") ->
  (...) ->
    args = {...}
    str = table.concat [tostring s for s in *args], "\t"
    post_message { channel, str }

async_print = printer"std_out"
async_err = printer"std_err"

msg_id = 0
msg_responses = {}

request_response = (msg) ->
  id = msg_id
  msg_id += 1
  post_message { "request", id, msg }
  while true
    msg = coroutine.yield!
    if msg == "response" and msg_responses[id]
      return with msg_responses[id]
        msg_responses[id] = nil

safe_resume = (co, ...) ->
  success, error_msg = coroutine.resume co, ...
  if not success
    async_err debug.traceback co, error_msg
    nacl.set_game_thread nil
    false
  else
    true

async_require = (module_name) ->
  mod = package.loaded[module_name]
  return mod if mod

  loader = if package.preload[module_name]
    package.preload[module_name]
  else
    msg = request_response { "require", module_name }
    status, code = unpack msg
    return async_err code if status == "error"
    assert loadstring code, module_name

  setfenv loader, getfenv 2
  mod = loader module_name
  mod = true if mod == nil
  package.loaded[module_name] = mod
  mod

nacl.lib = {
  core: nacl
  prefetch: (files) ->
    tuples = {}

    add_files = (list, t="text") ->
      for k, file_or_list in pairs list
        if type(file_or_list) == "table"
          add_files file_or_list, k
        else
          table.insert tuples, { t, file_or_list }

    add_files files

    return if #tuples == 0
    status, msg = unpack request_response { "prefetch", tuples }
    error "prefetch: " .. msg if status != "success"

  track_event: (category, action, label=nil, value=nil, interactive=true) ->
    post_message { "track_event", { category, action, label, value, interactive } }
}

-- scope of the game thread
async_scope = setmetatable {
    print: async_print
    require: async_require
    nacl: nacl.lib
  }, {
    __index: _G
  }

game_thread = nil -- to prevent it from being garbage collected

nacl.show_error = async_err -- used by C to send errors to client

nacl.handle_message = (msg) ->
  error "unknown msg: " .. tostring(msg) if type(msg) != "string"
  print ">>", msg\sub 1, 120

  msg = cjson.decode msg

  switch msg[1]
    -- create a new execution thread
    -- throws out the old one
    when "execute"
      fn, err = loadstring msg[2], "execute"
      if not fn
        async_err err
      else
        setfenv fn, async_scope
        run = -> aroma.run fn

        game_thread = coroutine.create run
        nacl.set_game_thread game_thread
        safe_resume game_thread

    when "response"
      _, id, data = unpack msg
      msg_responses[id] = data
      safe_resume game_thread, "response"
    else
      error "Don't know how to handle message: " .. (msg[1] or msg)

img_cache = {}

image_data_from_url = (url) ->
  msg = request_response { "image", url }
  status, bytes, width, height = unpack msg
  if status == "success"
    nacl.image_data_from_byte_string bytes, width, height
  else
    error bytes

class AudioSource
  new: (@source_id, @url) =>
  msg: (...) => post_message { "audio", @source_id, ... }

  play: => @msg "play"
  stop: => @msg "stop"
  rewind: => @msg "rewind"
  setLooping: (should_loop) => @msg "set_looping", should_loop

  __tostring: => "HTMLAudioSource<" .. @url .. ">"

nacl.init = {
  graphics: =>
    old_new_image = @newImage

    @newImage = (url, ...) ->
      return old_new_image url, ... if type(url) != "string"
      return img_cache[url] if img_cache[url]

      img_data = image_data_from_url url
      img = old_new_image img_data
      img_cache[url] = img
      img

    @newFont = (font_str, alphabet=ALPHABET) ->
      glyphs = request_response { "font", font_str, alphabet }
      cache = aroma.font.newGlyphCache!
      for g in *glyphs
        letter, data_string, w, h = unpack g
        data = nacl.image_data_from_byte_string data_string, w, h
        cache\add_glyph letter\byte!, data
      cache\to_font!

    aroma.boot = ->
      if not self.getFont!
        self.setFont self.newFont DEFAULT_FONT

  audio: =>
    @newSource = (url, kind="static") ->
      msg = request_response { "audio", url, kind }
      status, source_id, err_msg = unpack msg
      if status == "success"
        AudioSource source_id, url
      else
        error error_msg

  image: =>
    old_new_image_data = @newImageData
    @newImageData = (...) ->
      args = {...}
      old_new_image_data ... if type(args[1]) != "string"
      image_data_from_url args[1]
}

functions_to_reset = {"draw", "update", "keypressed", "keyreleased", "focus", "load"}
default_modules = { name, true for name in pairs package.loaded }

nacl.init_all = (aroma) ->
  aroma.run = (setup_fn) ->
    -- unload any modules
    for key in pairs package.loaded
      package.loaded[key] = nil unless default_modules[name]

    blank = ->
    aroma[key] = blank for key in *functions_to_reset
    aroma.graphics.reset!
    aroma.boot!

    setup_fn!

    while true -- listen for game messages
      msg = {coroutine.yield!}
      if aroma[msg[1]]
        name = table.remove msg, 1
        aroma[name] unpack msg
      -- else
      --   print "unknown message"
      --   print unpack msg


Always make backups in separate folder

Always make backups in separate folder

--
-- Copyright (C) 2017-2018 DBot

-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do so,
-- subject to the following conditions:

-- The above copyright notice and this permission notice shall be included in all copies
-- or substantial portions of the Software.

-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
-- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.

file.CreateDir('ppm2')
file.CreateDir('ppm2/backups')
file.CreateDir('ppm2/thumbnails')

for _, ffind in ipairs file.Find('ppm2/*.txt', 'DATA')
	fTarget = ffind\sub(1, -5)
	-- maybe joined server with old ppm2 and new clear _current was generated
	if not file.Exists('ppm2/' .. fTarget .. '.dat', 'DATA')
		fRead = file.Read('ppm2/' .. ffind, 'DATA')
		json = util.JSONToTable(fRead)
		if json
			TagCompound = DLib.NBT.TagCompound()
			for key, value in pairs json
				switch luatype(value)
					when 'string'
						TagCompound\AddString(key, value)
					when 'number'
						TagCompound\AddFloat(key, value)
					when 'boolean'
						TagCompound\AddByte(key, value and 1 or 0)
					when 'table'
						-- assume color
						TagCompound\AddByteArray(key, {value.r - 128, value.g - 128, value.b - 128, value.a - 128}) if value.r and value.g and value.b and value.a
					else
						error(luatype(value))
			buf = DLib.BytesBuffer()
			TagCompound\WriteFile(buf)
			stream = file.Open('ppm2/' .. fTarget .. '.dat', 'wb', 'DATA')
			buf\ToFileStream(stream)
			stream\Flush()
			stream\Close()
	file.Delete('ppm2/' .. ffind)

class PonyDataInstance
	@DATA_DIR = "ppm2/"
	@DATA_DIR_BACKUP = "ppm2/backups/"

	@FindFiles = =>
		output = [str\sub(1, #str - 4) for _, str in ipairs file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.dat')]
		return output

	@FindInstances = =>
		output = [@(str\sub(1, #str - 4)) for _, str in ipairs file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.dat')]
		return output

	@PONY_DATA = PPM2.PonyDataRegistry

	@PONY_DATA_MAPPING = {getFunc\lower(), key for key, {:getFunc} in pairs @PONY_DATA}
	@PONY_DATA_MAPPING[key] = key for key, value in pairs @PONY_DATA

	for key, data in pairs @PONY_DATA
		continue unless data.enum
		data.enum = [arg\upper() for _, arg in ipairs data.enum]
		data.enumMapping = {}
		data.enumMappingBackward = {}
		i = -1
		for _, enumVal in ipairs data.enum
			i += 1
			data.enumMapping[i] = enumVal
			data.enumMappingBackward[enumVal] = i
	for key, {:getFunc, :fix, :enumMappingBackward, :enumMapping, :enum, :min, :max, :default} in pairs @PONY_DATA
		@__base["Get#{getFunc}"] = => @dataTable[key]
		@__base["GetMin#{getFunc}"] = => min if min
		@__base["GetMax#{getFunc}"] = => max if max
		@__base["Enum#{getFunc}"] = => enum if enum
		@__base["Get#{getFunc}Types"] = => enum if enum

		@["GetMin#{getFunc}"] = => min if min
		@["GetMax#{getFunc}"] = => max if max
		@["GetDefault#{getFunc}"] = default
		@["GetEnum#{getFunc}"] = => enum if enum
		@["Enum#{getFunc}"] = => enum if enum

		if enumMapping
			@__base["Get#{getFunc}Enum"] = => enumMapping[@dataTable[key]] or enumMapping[0] or @dataTable[key]
			@__base["GetEnum#{getFunc}"] = @__base["Get#{getFunc}Enum"]
		@__base["Reset#{getFunc}"] = => @["Set#{getFunc}"](@, default())
		@__base["Set#{getFunc}"] = (val = defValue, ...) =>
			if luatype(val) == 'string' and enumMappingBackward
				newVal = enumMappingBackward[val\upper()]
				val = newVal if newVal
			newVal = fix(val)
			oldVal = @dataTable[key]
			@dataTable[key] = newVal
			@ValueChanges(key, oldVal, newVal, ...)

	WriteNetworkData: =>
		for _, {:strName, :writeFunc, :getName, :defValue} in ipairs PPM2.NetworkedPonyData.NW_Vars
			if @["Get#{getName}"]
				writeFunc(@["Get#{getName}"](@))
			else
				writeFunc(defValue)

	Copy: (fileName = @filename) =>
		copyOfData = {}
		for key, val in pairs @dataTable
			switch luatype(val)
				when 'number'
					copyOfData[key] = val
				when 'string'
					copyOfData[key] = val
				when 'boolean'
					copyOfData[key] = val
				when 'table'
					{:r, :g, :b} = val
					if r and g and b
						copyOfData[key] = Color(r, g, b)
		newData = @@(fileName, copyOfData, false)
		return newData
	CreateCustomNetworkObject: (goingToNetwork = false, ply = LocalPlayer(), ...) =>
		newData = PPM2.NetworkedPonyData(nil, ply)
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData
	CreateNetworkObject: (goingToNetwork = true, ...) =>
		newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData
	ApplyDataToObject: (target, ...) =>
		for key, value in pairs @GetAsNetworked()
			error("Attempt to apply data to object #{target} at unknown index #{key}!") if not target["Set#{key}"]
			target["Set#{key}"](target, value, ...)
	UpdateController: (...) => @ApplyDataToObject(@nwObj, ...)
	CreateController: (...) => @CreateNetworkObject(false, ...)
	CreateCustomController: (...) => @CreateCustomNetworkObject(false, ...)

	new: (filename, data, readIfExists = true, force = false, doBackup = true) =>
		@SetFilename(filename)
		@NBTTagCompound = DLib.NBT.TagCompound()
		@updateNWObject = true
		@networkNWObject = true
		@valid = @isOpen
		@rawData = data
		@dataTable = {k, default() for k, {:default} in pairs @@PONY_DATA}
		@saveOnChange = true
		if data
			@SetupData(data, true)
		elseif @exists and readIfExists
			@ReadFromDisk(force, doBackup)

	Reset: => @['Reset' .. getFunc](@) for k, {:getFunc} in pairs @@PONY_DATA

	@ERR_MISSING_PARAMETER = 4
	@ERR_MISSING_CONTENT = 5

	GetSaveOnChange: => @saveOnChange
	SaveOnChange: => @saveOnChange
	SetSaveOnChange: (val = true) => @saveOnChange = val
	GetValueFromNBT: (mapData, value) =>
		if mapData.enum and luatype(value) == 'string'
			mapData.fix(mapData.enumMappingBackward[value\upper()])
		elseif mapData.type == 'COLOR'
			if value.r and value.g and value.b and value.a
				mapData.fix(Color(value))
			else
				mapData.fix(Color(value[1] + 128, value[2] + 128, value[3] + 128, value[4] + 128))
		elseif mapData.type == 'BOOLEAN'
			if luatype(value) == 'boolean'
				mapData.fix(value)
			else
				mapData.fix(value == 1)
		else
			mapData.fix(value)

	GetExtraBackupPath: => "#{@@DATA_DIR_BACKUP}#{@filename}_bak_#{os.date('%S_%M_%H-%d_%m_%Y', os.time())}.dat"

	SetupData: (data = @NBTTagCompound, force = false, doBackup = false) =>
		if luatype(data) == 'NBTCompound'
			data = data\GetValue()
		if doBackup or not force
			makeBackup = false
			for key, value2 in pairs(data)
				key = key\lower()
				map = @@PONY_DATA_MAPPING[key]
				if map
					mapData = @@PONY_DATA[map]
					value = @GetValueFromNBT(mapData, value2)
					if mapData.enum
						if luatype(value) == 'string' and not mapData.enumMappingBackward[value\upper()] or luatype(value) == 'number' and not mapData.enumMapping[value]
							return @@ERR_MISSING_CONTENT if not force
							makeBackup = true
							break
			if doBackup and makeBackup and @exists
				fRead = file.Read(@fpath, 'DATA')
				file.Write(@GetExtraBackupPath(), fRead)

		for key, value2 in pairs(data)
			key = key\lower()
			map = @@PONY_DATA_MAPPING[key]
			continue unless map
			mapData = @@PONY_DATA[map]
			@dataTable[key] = @GetValueFromNBT(mapData, value2)
	ValueChanges: (key, oldVal, newVal, saveNow = @exists and @saveOnChange) =>
		if @nwObj and @updateNWObject
			{:getFunc} = @@PONY_DATA[key]
			@nwObj["Set#{getFunc}"](@nwObj, newVal, @networkNWObject)
		@Save() if saveNow
	SetFilename: (filename) =>
		@filename = filename
		@filenameFull = "#{filename}.dat"
		@fpath = "#{@@DATA_DIR}#{filename}.dat"
		@preview = "#{@@DATA_DIR}thumbnails/#{filename}.png"
		@fpathFull = "data/#{@@DATA_DIR}#{filename}.dat"
		@isOpen = @filename ~= nil
		@exists = file.Exists(@fpath, 'DATA')
		return @exists
	SetNetworkData: (nwObj) => @nwObj = nwObj
	SetPonyData: (nwObj) => @nwObj = nwObj
	SetPonyDataController: (nwObj) => @nwObj = nwObj
	SetPonyController: (nwObj) => @nwObj = nwObj
	SetController: (nwObj) => @nwObj = nwObj
	SetDataController: (nwObj) => @nwObj = nwObj

	SetNetworkOnChange: (newVal = true) => @networkNWObject = newVal
	SetUpdateOnChange: (newVal = true) => @updateNWObject = newVal

	GetNetworkOnChange: => @networkNWObject
	GetUpdateOnChange: => @updateNWObject

	GetNetworkData: => @nwObj
	GetPonyData: => @nwObj
	GetPonyDataController: => @nwObj
	GetPonyController: => @nwObj
	GetController: => @nwObj
	GetDataController: => @nwObj

	IsValid: => @valid
	Exists: => @exists
	FileExists: => @exists
	IsExists: => @exists
	GetFileName: => @filename
	GetFilename: => @filename
	GetFileNameFull: => @filenameFull
	GetFilenameFull: => @filenameFull
	GetFilePath: => @fpath
	GetFullFilePath: => @fpathFull
	SerealizeValue: (valID = '') =>
		map = @@PONY_DATA[valID]
		return unless map
		val = @dataTable[valID]
		if map.enum
			return DLib.NBT.TagString(map.enumMapping[val] or map.enumMapping[map.default()])
		elseif map.serealize
			return map.serealize(val)
		else
			switch map.type
				when 'INT'
					return DLib.NBT.TagInt(val)
				when 'FLOAT'
					return DLib.NBT.TagFloat(val)
				when 'URL'
					return DLib.NBT.TagString(val)
				when 'BOOLEAN'
					return DLib.NBT.TagByte(val and 1 or 0)
				when 'COLOR'
					return DLib.NBT.TagByteArray({val.r - 128, val.g - 128, val.b - 128, val.a - 128})
	GetAsNetworked: => {getFunc, @dataTable[k] for k, {:getFunc} in pairs @@PONY_DATA}

	@READ_SUCCESS = 0
	@ERR_FILE_NOT_EXISTS = 1
	@ERR_FILE_EMPTY = 2
	@ERR_FILE_CORRUPT = 3
	ReadFromDisk: (force = false, doBackup = true) =>
		return @@ERR_FILE_NOT_EXISTS unless @exists
		fRead = file.Read(@fpath, 'DATA')
		return @@ERR_FILE_EMPTY if not fRead or fRead == ''
		@NBTTagCompound\ReadFile(DLib.BytesBuffer(fRead))
		return @SetupData(@NBTTagCompound, force, doBackup) or @@READ_SUCCESS

	SaveAs: (path = @fpath) =>
		@NBTTagCompound\AddTag(key, @SerealizeValue(key)) for key, val in pairs @dataTable
		buf = DLib.BytesBuffer()
		@NBTTagCompound\WriteFile(buf)
		stream = file.Open(path, 'wb', 'DATA')
		buf\ToFileStream(stream)
		stream\Flush()
		stream\Close()
		return buf

	SavePreview: (path = @preview) =>
		buildingModel = ClientsideModel('models/ppm/ppm2_stage.mdl', RENDERGROUP_OTHER)
		buildingModel\SetNoDraw(true)
		buildingModel\SetModelScale(0.9)
		model = ClientsideModel('models/ppm/player_default_base_new_nj.mdl')

		with model
			\SetNoDraw(true)
			data = @CreateCustomController(model)
			.__PPM2_PonyData = data
			ctrl = data\GetRenderController()

			if bg = data\GetBodygroupController()
				bg\ApplyBodygroups()

			with model\PPMBonesModifier()
				\ResetBones()
				hook.Call('PPM2.SetupBones', nil, model, data)
				\Think(true)

			\SetSequence(22)
			\FrameAdvance(0)

		timer.Simple 0.5, ->
			renderTarget = GetRenderTarget('ppm2_save_preview_generate2', 1024, 1024, false)
			renderTarget\Download()
			render.PushRenderTarget(renderTarget)
			--render.SuppressEngineLighting(true)
			render.Clear(0, 0, 0, 255, true, true)
			cam.Start3D(Vector(49.373046875, -35.021484375, 58.332901000977), Angle(0, 141, 0), 90, 0, 0, 1024, 1024)

			buildingModel\DrawModel()

			with model
				data = .__PPM2_PonyData
				ctrl = data\GetRenderController()

				if bg = data\GetBodygroupController()
					bg\ApplyBodygroups()

				with model\PPMBonesModifier()
					\ResetBones()
					hook.Call('PPM2.SetupBones', nil, model, data)
					\Think(true)

				with ctrl
					\DrawModels()
					\HideModels(true)
					\PreDraw(model, true)

				\DrawModel()
				ctrl\PostDraw(model, true)

			cam.End3D()

			data = render.Capture({
				format: 'png'
				x: 0
				y: 0
				w: 1024
				h: 1024
				alpha: false
			})

			model\Remove()
			buildingModel\Remove()

			file.Write(path, data)

			--render.SuppressEngineLighting(false)
			render.PopRenderTarget()

	Save: (doBackup = true, preview = true) =>
		file.Write(@GetExtraBackupPath(), file.Read(@fpath, 'DATA')) if doBackup and @exists
		buf = @SaveAs(@fpath)
		@SavePreview(@preview) if preview
		@exists = true
		return buf

PPM2.PonyDataInstance = PonyDataInstance

PPM2.MainDataInstance = nil
PPM2.GetMainData = ->
	if not PPM2.MainDataInstance
		PPM2.MainDataInstance = PonyDataInstance('_current', nil, true, true)
		if not PPM2.MainDataInstance\FileExists()
			PPM2.MainDataInstance\Save()
	return PPM2.MainDataInstance

--
-- Copyright (C) 2017-2018 DBot

-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do so,
-- subject to the following conditions:

-- The above copyright notice and this permission notice shall be included in all copies
-- or substantial portions of the Software.

-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
-- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.

file.CreateDir('ppm2')
file.CreateDir('ppm2/backups')
file.CreateDir('ppm2/thumbnails')

for _, ffind in ipairs file.Find('ppm2/*.txt', 'DATA')
	fTarget = ffind\sub(1, -5)
	-- maybe joined server with old ppm2 and new clear _current was generated
	if not file.Exists('ppm2/' .. fTarget .. '.dat', 'DATA')
		fRead = file.Read('ppm2/' .. ffind, 'DATA')
		json = util.JSONToTable(fRead)
		if json
			TagCompound = DLib.NBT.TagCompound()
			for key, value in pairs json
				switch luatype(value)
					when 'string'
						TagCompound\AddString(key, value)
					when 'number'
						TagCompound\AddFloat(key, value)
					when 'boolean'
						TagCompound\AddByte(key, value and 1 or 0)
					when 'table'
						-- assume color
						TagCompound\AddByteArray(key, {value.r - 128, value.g - 128, value.b - 128, value.a - 128}) if value.r and value.g and value.b and value.a
					else
						error(luatype(value))
			buf = DLib.BytesBuffer()
			TagCompound\WriteFile(buf)
			stream = file.Open('ppm2/' .. fTarget .. '.dat', 'wb', 'DATA')
			buf\ToFileStream(stream)
			stream\Flush()
			stream\Close()
	file.Delete('ppm2/' .. ffind)

class PonyDataInstance
	@DATA_DIR = "ppm2/"
	@DATA_DIR_BACKUP = "ppm2/backups/"

	@FindFiles = =>
		output = [str\sub(1, #str - 4) for _, str in ipairs file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.dat')]
		return output

	@FindInstances = =>
		output = [@(str\sub(1, #str - 4)) for _, str in ipairs file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.dat')]
		return output

	@PONY_DATA = PPM2.PonyDataRegistry

	@PONY_DATA_MAPPING = {getFunc\lower(), key for key, {:getFunc} in pairs @PONY_DATA}
	@PONY_DATA_MAPPING[key] = key for key, value in pairs @PONY_DATA

	for key, data in pairs @PONY_DATA
		continue unless data.enum
		data.enum = [arg\upper() for _, arg in ipairs data.enum]
		data.enumMapping = {}
		data.enumMappingBackward = {}
		i = -1
		for _, enumVal in ipairs data.enum
			i += 1
			data.enumMapping[i] = enumVal
			data.enumMappingBackward[enumVal] = i
	for key, {:getFunc, :fix, :enumMappingBackward, :enumMapping, :enum, :min, :max, :default} in pairs @PONY_DATA
		@__base["Get#{getFunc}"] = => @dataTable[key]
		@__base["GetMin#{getFunc}"] = => min if min
		@__base["GetMax#{getFunc}"] = => max if max
		@__base["Enum#{getFunc}"] = => enum if enum
		@__base["Get#{getFunc}Types"] = => enum if enum

		@["GetMin#{getFunc}"] = => min if min
		@["GetMax#{getFunc}"] = => max if max
		@["GetDefault#{getFunc}"] = default
		@["GetEnum#{getFunc}"] = => enum if enum
		@["Enum#{getFunc}"] = => enum if enum

		if enumMapping
			@__base["Get#{getFunc}Enum"] = => enumMapping[@dataTable[key]] or enumMapping[0] or @dataTable[key]
			@__base["GetEnum#{getFunc}"] = @__base["Get#{getFunc}Enum"]
		@__base["Reset#{getFunc}"] = => @["Set#{getFunc}"](@, default())
		@__base["Set#{getFunc}"] = (val = defValue, ...) =>
			if luatype(val) == 'string' and enumMappingBackward
				newVal = enumMappingBackward[val\upper()]
				val = newVal if newVal
			newVal = fix(val)
			oldVal = @dataTable[key]
			@dataTable[key] = newVal
			@ValueChanges(key, oldVal, newVal, ...)

	WriteNetworkData: =>
		for _, {:strName, :writeFunc, :getName, :defValue} in ipairs PPM2.NetworkedPonyData.NW_Vars
			if @["Get#{getName}"]
				writeFunc(@["Get#{getName}"](@))
			else
				writeFunc(defValue)

	Copy: (fileName = @filename) =>
		copyOfData = {}
		for key, val in pairs @dataTable
			switch luatype(val)
				when 'number'
					copyOfData[key] = val
				when 'string'
					copyOfData[key] = val
				when 'boolean'
					copyOfData[key] = val
				when 'table'
					{:r, :g, :b} = val
					if r and g and b
						copyOfData[key] = Color(r, g, b)
		newData = @@(fileName, copyOfData, false)
		return newData
	CreateCustomNetworkObject: (goingToNetwork = false, ply = LocalPlayer(), ...) =>
		newData = PPM2.NetworkedPonyData(nil, ply)
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData
	CreateNetworkObject: (goingToNetwork = true, ...) =>
		newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData
	ApplyDataToObject: (target, ...) =>
		for key, value in pairs @GetAsNetworked()
			error("Attempt to apply data to object #{target} at unknown index #{key}!") if not target["Set#{key}"]
			target["Set#{key}"](target, value, ...)
	UpdateController: (...) => @ApplyDataToObject(@nwObj, ...)
	CreateController: (...) => @CreateNetworkObject(false, ...)
	CreateCustomController: (...) => @CreateCustomNetworkObject(false, ...)

	new: (filename, data, readIfExists = true, force = false, doBackup = true) =>
		@SetFilename(filename)
		@NBTTagCompound = DLib.NBT.TagCompound()
		@updateNWObject = true
		@networkNWObject = true
		@valid = @isOpen
		@rawData = data
		@dataTable = {k, default() for k, {:default} in pairs @@PONY_DATA}
		@saveOnChange = true
		if data
			@SetupData(data, true)
		elseif @exists and readIfExists
			@ReadFromDisk(force, doBackup)

	Reset: => @['Reset' .. getFunc](@) for k, {:getFunc} in pairs @@PONY_DATA

	@ERR_MISSING_PARAMETER = 4
	@ERR_MISSING_CONTENT = 5

	GetSaveOnChange: => @saveOnChange
	SaveOnChange: => @saveOnChange
	SetSaveOnChange: (val = true) => @saveOnChange = val
	GetValueFromNBT: (mapData, value) =>
		if mapData.enum and luatype(value) == 'string'
			mapData.fix(mapData.enumMappingBackward[value\upper()])
		elseif mapData.type == 'COLOR'
			if value.r and value.g and value.b and value.a
				mapData.fix(Color(value))
			else
				mapData.fix(Color(value[1] + 128, value[2] + 128, value[3] + 128, value[4] + 128))
		elseif mapData.type == 'BOOLEAN'
			if luatype(value) == 'boolean'
				mapData.fix(value)
			else
				mapData.fix(value == 1)
		else
			mapData.fix(value)

	SetupData: (data = @NBTTagCompound, force = false, doBackup = false) =>
		if luatype(data) == 'NBTCompound'
			data = data\GetValue()
		if doBackup or not force
			makeBackup = false
			for key, value2 in pairs(data)
				key = key\lower()
				map = @@PONY_DATA_MAPPING[key]
				if map
					mapData = @@PONY_DATA[map]
					value = @GetValueFromNBT(mapData, value2)
					if mapData.enum
						if luatype(value) == 'string' and not mapData.enumMappingBackward[value\upper()] or luatype(value) == 'number' and not mapData.enumMapping[value]
							return @@ERR_MISSING_CONTENT if not force
							makeBackup = true
							break
			if doBackup and makeBackup and @exists
				bkName = "#{@@DATA_DIR_BACKUP}#{@filename}_bak_#{os.date('%S_%M_%H-%d_%m_%Y', os.time())}.dat"
				fRead = file.Read(@fpath, 'DATA')
				file.Write(bkName, fRead)

		for key, value2 in pairs(data)
			key = key\lower()
			map = @@PONY_DATA_MAPPING[key]
			continue unless map
			mapData = @@PONY_DATA[map]
			@dataTable[key] = @GetValueFromNBT(mapData, value2)
	ValueChanges: (key, oldVal, newVal, saveNow = @exists and @saveOnChange) =>
		if @nwObj and @updateNWObject
			{:getFunc} = @@PONY_DATA[key]
			@nwObj["Set#{getFunc}"](@nwObj, newVal, @networkNWObject)
		@Save() if saveNow
	SetFilename: (filename) =>
		@filename = filename
		@filenameFull = "#{filename}.dat"
		@fpath = "#{@@DATA_DIR}#{filename}.dat"
		@preview = "#{@@DATA_DIR}thumbnails/#{filename}.png"
		@fpathBackup = "#{@@DATA_DIR}#{filename}.bak.dat"
		@fpathFull = "data/#{@@DATA_DIR}#{filename}.dat"
		@isOpen = @filename ~= nil
		@exists = file.Exists(@fpath, 'DATA')
		return @exists
	SetNetworkData: (nwObj) => @nwObj = nwObj
	SetPonyData: (nwObj) => @nwObj = nwObj
	SetPonyDataController: (nwObj) => @nwObj = nwObj
	SetPonyController: (nwObj) => @nwObj = nwObj
	SetController: (nwObj) => @nwObj = nwObj
	SetDataController: (nwObj) => @nwObj = nwObj

	SetNetworkOnChange: (newVal = true) => @networkNWObject = newVal
	SetUpdateOnChange: (newVal = true) => @updateNWObject = newVal

	GetNetworkOnChange: => @networkNWObject
	GetUpdateOnChange: => @updateNWObject

	GetNetworkData: => @nwObj
	GetPonyData: => @nwObj
	GetPonyDataController: => @nwObj
	GetPonyController: => @nwObj
	GetController: => @nwObj
	GetDataController: => @nwObj

	IsValid: => @valid
	Exists: => @exists
	FileExists: => @exists
	IsExists: => @exists
	GetFileName: => @filename
	GetFilename: => @filename
	GetFileNameFull: => @filenameFull
	GetFilenameFull: => @filenameFull
	GetFilePath: => @fpath
	GetFullFilePath: => @fpathFull
	SerealizeValue: (valID = '') =>
		map = @@PONY_DATA[valID]
		return unless map
		val = @dataTable[valID]
		if map.enum
			return DLib.NBT.TagString(map.enumMapping[val] or map.enumMapping[map.default()])
		elseif map.serealize
			return map.serealize(val)
		else
			switch map.type
				when 'INT'
					return DLib.NBT.TagInt(val)
				when 'FLOAT'
					return DLib.NBT.TagFloat(val)
				when 'URL'
					return DLib.NBT.TagString(val)
				when 'BOOLEAN'
					return DLib.NBT.TagByte(val and 1 or 0)
				when 'COLOR'
					return DLib.NBT.TagByteArray({val.r - 128, val.g - 128, val.b - 128, val.a - 128})
	GetAsNetworked: => {getFunc, @dataTable[k] for k, {:getFunc} in pairs @@PONY_DATA}

	@READ_SUCCESS = 0
	@ERR_FILE_NOT_EXISTS = 1
	@ERR_FILE_EMPTY = 2
	@ERR_FILE_CORRUPT = 3
	ReadFromDisk: (force = false, doBackup = true) =>
		return @@ERR_FILE_NOT_EXISTS unless @exists
		fRead = file.Read(@fpath, 'DATA')
		return @@ERR_FILE_EMPTY if not fRead or fRead == ''
		@NBTTagCompound\ReadFile(DLib.BytesBuffer(fRead))
		return @SetupData(@NBTTagCompound, force, doBackup) or @@READ_SUCCESS

	SaveAs: (path = @fpath) =>
		@NBTTagCompound\AddTag(key, @SerealizeValue(key)) for key, val in pairs @dataTable
		buf = DLib.BytesBuffer()
		@NBTTagCompound\WriteFile(buf)
		stream = file.Open(path, 'wb', 'DATA')
		buf\ToFileStream(stream)
		stream\Flush()
		stream\Close()
		return buf

	SavePreview: (path = @preview) =>
		buildingModel = ClientsideModel('models/ppm/ppm2_stage.mdl', RENDERGROUP_OTHER)
		buildingModel\SetNoDraw(true)
		buildingModel\SetModelScale(0.9)
		model = ClientsideModel('models/ppm/player_default_base_new_nj.mdl')

		with model
			\SetNoDraw(true)
			data = @CreateCustomController(model)
			.__PPM2_PonyData = data
			ctrl = data\GetRenderController()

			if bg = data\GetBodygroupController()
				bg\ApplyBodygroups()

			with model\PPMBonesModifier()
				\ResetBones()
				hook.Call('PPM2.SetupBones', nil, model, data)
				\Think(true)

			\SetSequence(22)
			\FrameAdvance(0)

		timer.Simple 0.5, ->
			renderTarget = GetRenderTarget('ppm2_save_preview_generate2', 1024, 1024, false)
			renderTarget\Download()
			render.PushRenderTarget(renderTarget)
			--render.SuppressEngineLighting(true)
			render.Clear(0, 0, 0, 255, true, true)
			cam.Start3D(Vector(49.373046875, -35.021484375, 58.332901000977), Angle(0, 141, 0), 90, 0, 0, 1024, 1024)

			buildingModel\DrawModel()

			with model
				data = .__PPM2_PonyData
				ctrl = data\GetRenderController()

				if bg = data\GetBodygroupController()
					bg\ApplyBodygroups()

				with model\PPMBonesModifier()
					\ResetBones()
					hook.Call('PPM2.SetupBones', nil, model, data)
					\Think(true)

				with ctrl
					\DrawModels()
					\HideModels(true)
					\PreDraw(model, true)

				\DrawModel()
				ctrl\PostDraw(model, true)

			cam.End3D()

			data = render.Capture({
				format: 'png'
				x: 0
				y: 0
				w: 1024
				h: 1024
				alpha: false
			})

			model\Remove()
			buildingModel\Remove()

			file.Write(path, data)

			--render.SuppressEngineLighting(false)
			render.PopRenderTarget()

	Save: (doBackup = true, preview = true) =>
		if doBackup and @exists
			fRead = file.Read(@fpath, 'DATA')
			file.Write(@fpathBackup, fRead)
		buf = @SaveAs(@fpath)
		@SavePreview(@preview) if preview
		@exists = true
		return buf

PPM2.PonyDataInstance = PonyDataInstance

PPM2.MainDataInstance = nil
PPM2.GetMainData = ->
	if not PPM2.MainDataInstance
		PPM2.MainDataInstance = PonyDataInstance('_current', nil, true, true)
		if not PPM2.MainDataInstance\FileExists()
			PPM2.MainDataInstance\Save()
	return PPM2.MainDataInstance


moonscript spec cleanup

moonscript spec cleanup

require "moon"

describe "moonscript tests", ->
  it "runs", ->
    assert.are.equal true, true

  it "fails", ->
    assert.error(-> assert.are.equal false, true)