; ship save/load script by Admiral MS, rewritten by Goober5000
; script uses "," "&" ":" and "§" for data separation. weapon and subsystem data should not contain any of these or the script fails
; see readme for instructions how to use this script

#Conditional Hooks

$Application: FS2_Open
$On Game Init:
[
	ShipSave = {}
	
	-- Function to hash a string using a basic ascii char codes
	function ShipSave:basicStringHash(inputString)
		local mult = 1
		
		--Check if we have SCPUI. If so, we can use the specified hash value
		if ScpuiSystem ~= nil then
			mult = ScpuiSystem.dataSaverMulti
			if mult == nil then
				mult = 1 -- No SCPUI, so just use 1
			end
		end
		
		local hash = 0
		for i = 1, #inputString do
			local charCode = string.byte(inputString, i)
			hash = (hash * mult) + charCode  -- Adjust the multiplier as needed
		end
		return string.sub(hash, 1, 10)
	end

	--definitions for savefile
	ShipSave.path_shipsave = "data/players"
	ShipSave.save_file_name = ShipSave:basicStringHash(ba.getModTitle()) .. "_checkpoint_data.cfg"
	
	if CheckpointsCleanup then
		if cf.fileExists(ShipSave.save_file_name) then
			cf.deleteFile(ShipSave.save_file_name, ShipSave.path_shipsave)
			ba.print("All checkpoint data for file " .. ShipSave.save_file_name .. " has been deleted!\n")
		end
	end
	
	CheckpointsCleanup = false
		

	-- ================================================== --
	-- sexp functions (public API)
	-- ================================================== --
	
	if ShipSave.save_file_name == nil then
		ba.error("Save load needs a save file name! Please modify the script definitions at the top of the file!")
	end

	function ShipSave:save_init(filename)
    ba.print('Beginning save to ' .. filename .. '\n')
		if filename then
			self.active_filename = filename
		else
			self.active_filename = mn.getMissionFilename()
		end
		self:clear()
	end
	mn.LuaSEXPs["lua-save-init"].Action = function(filename)
		ShipSave:save_init(filename)
	end

	function ShipSave:save_ship(shipname)
		if shipname then
			self.shipsaveload_data.ships[shipname] = self:get_ship_array(shipname)
		end
	end
	mn.LuaSEXPs["lua-save-ship"].Action = function(...)
		for i,v in ipairs(arg) do
			ShipSave:save_ship(v[1])
		end
	end

	function ShipSave:save_variable(varname)
		if varname then
			self.shipsaveload_data.variables[varname] = self:collect_variable_data(varname)
		end
	end
	mn.LuaSEXPs["lua-save-variable"].Action = function(...)
		for i,v in ipairs(arg) do
			ShipSave:save_variable(v[1].Name)
		end
	end
	
	function ShipSave:save_stats()
		self.shipsaveload_data.stats = self:collect_mission_stats()
	end
	mn.LuaSEXPs["lua-save-stats"].Action = function()
		ShipSave:save_stats()
	end
	
	function ShipSave:save_time()
		self.shipsaveload_data.time = mn.getMissionTime() + mn.MissionHUDTimerPadding
	end
	mn.LuaSEXPs["lua-save-time"].Action = function()
		ShipSave:save_time()
	end

	function ShipSave:save_done()
		self:saveshipfile()
	end
	mn.LuaSEXPs["lua-save-done"].Action = function()
		ShipSave:save_done()
	end

	function ShipSave:load_init(filename)
		if filename then
			self.active_filename = filename
		else
			self.active_filename = mn.getMissionFilename()
		end
		self:loadshipfile()
	end
	mn.LuaSEXPs["lua-load-init"].Action = function(filename)
		ShipSave:load_init(filename)
	end

	function ShipSave:load_ship(shipname, loadtype)
		if shipname then
			if self.shipsaveload_data.ships[shipname] then
				self:set_ship_from_data(shipname, self.shipsaveload_data.ships[shipname], loadtype)
			else
				ba.print("Save data does not exist for ship " .. shipname .. ".\n")
			end
		end
	end
	mn.LuaSEXPs["lua-load-ship"].Action = function(loadtype, ...)
		for i,v in ipairs(arg) do
			ShipSave:load_ship(v[1], loadtype)
		end
	end

	function ShipSave:make_ship_arrive(shipname)
		if shipname then
			local entry = mn.ShipRegistry[shipname]
			if entry and entry.Status == NOT_YET_PRESENT then
				local parsed = entry:getParsedShip()
				if parsed:isValid() then	-- if it's not valid, it's the support ship
					local wing = parsed:getWing()
					if wing:isValid() then
						if wing.TotalArrived == 0 then
							wing.ArrivalLocation = "Hyperspace"
							wing:setFlag(true, "no-arrival-warp", "no-arrival-music", "no-arrival-message")
							wing:makeWingArrive()
						end
					else
						parsed.ArrivalLocation = "Hyperspace"
						parsed:setFlag(true, "no-arrival-warp", "no-arrival-music")
						parsed:makeShipArrive()
					end
				end
			end
		end
	end
	mn.LuaSEXPs["lua-spawn-and-load-ship"].Action = function(loadtype, ...)
		for i,v in ipairs(arg) do
			ShipSave:make_ship_arrive(v[1])
			ShipSave:load_ship(v[1], loadtype)
		end
	end

	function ShipSave:load_variable(varname)
		if varname then
			if self.shipsaveload_data.variables[varname] then
				self:set_variable_from_data(varname, self.shipsaveload_data.variables[varname])
			else
				ba.print("Save data does not exist for variable " .. varname .. ".\n")
			end
		end
	end
	mn.LuaSEXPs["lua-load-variable"].Action = function(...)
		for i,v in ipairs(arg) do
			ShipSave:load_variable(v[1].Name)
		end
	end
	
	function ShipSave:load_stats()
		if self.shipsaveload_data.stats then
			self:set_mission_stats_from_data(self.shipsaveload_data.stats)
		else
			ba.print("Save data does not exist for player mission stats.\n")
		end
	end
	mn.LuaSEXPs["lua-load-stats"].Action = function()
		ShipSave:load_stats()
	end
	
	function ShipSave:load_time()
		if self.shipsaveload_data.time then
			mn.MissionHUDTimerPadding = self.shipsaveload_data.time
		else
			ba.print("Save data does not exist for player mission time.\n")
		end
	end
	mn.LuaSEXPs["lua-load-time"].Action = function()
		ShipSave:load_time()
	end

	function ShipSave:copy_ship(shipname1, shipname2, loadtype)
		if shipname1 and shipname2 then
			local data = self:get_ship_array(shipname1)
			self:set_ship_from_data(shipname2, data, loadtype)

			local ship = mn.Ships[shipname1]
			if ship:isValid() then
				local target = mn.Ships[shipname2]
				if target:isValid() then
					target.Physics = ship.Physics
				end
			end
		end
	end
	mn.LuaSEXPs["lua-copy-ship"].Action = function(loadtype, shipname1, ...)
		for i,v in ipairs(arg) do
			ShipSave:copy_ship(shipname1, v[1], loadtype)
		end
	end

	--checks if there is a savefile, is called in fred
	function ShipSave:saveexist(filename)
		
		if filename == nil then
			filename = mn.getMissionFilename()
		end
	
		local json = require("dkjson")
		local file = nil
		local config = {}

		if cf.fileExists(ShipSave.save_file_name) then
			file = cf.openFile(ShipSave.save_file_name, "r", ShipSave.path_shipsave)
			config = json.decode(file:read("*a"))
			file:close()
			if not config then
				ba.print("Please ensure that " .. ShipSave.save_file_name .. " exists in " .. ShipSave.path_shipsave .. " and is valid JSON.")
				return false
			end
		else
			ba.print("SaveLoad: file " .. ShipSave.save_file_name .. " does not exist!\n")
			return false
		end
		
		if config[ba.getCurrentPlayer():getName()] == nil then
			ba.print("SaveLoad: could not find save file for player!\n")
			return false
		end
		
		if config[ba.getCurrentPlayer():getName()][filename] == nil then
			ba.print("SaveLoad: could not find the specified save file!\n")
			return false
		end
		
		return true
	end
	mn.LuaSEXPs["lua-save-file-exists"].Action = function(filename)
		return ShipSave:saveexist(filename)
	end

	--deletes savefile, is called in fred
	function ShipSave:deleteshipsavefile(filename)
		if filename == nil then
			filename = mn.getMissionFilename()
		end
		
		local json = require("dkjson")
		local file = nil
		local config = {}

		if cf.fileExists(ShipSave.save_file_name) then
			file = cf.openFile(ShipSave.save_file_name, "r", ShipSave.path_shipsave)
			config = json.decode(file:read("*a"))
			file:close()
			if not config then
				ba.print("Please ensure that " .. ShipSave.save_file_name .. " exists in " .. ShipSave.path_shipsave .. " and is valid JSON.")
				return
			end
		else
			ba.print("SaveLoad: file " .. ShipSave.save_file_name .. " does not exist!\n")
			return
		end
		
		if config[ba.getCurrentPlayer():getName()] == nil then
			return
		end
		
		config[ba.getCurrentPlayer():getName()][filename] = nil
		
		local file = cf.openFile(ShipSave.save_file_name, 'w', ShipSave.path_shipsave)
		file:write(json.encode(config))
		file:close()
	end
	mn.LuaSEXPs["lua-delete-save-file"].Action = function(filename)
		ShipSave:deleteshipsavefile(filename)
	end

	-- ================================================== --
	-- internal functions, not called by sexp
	-- ================================================== --
	
	function ShipSave:clear()
		self.shipsaveload_data = {}
		self.shipsaveload_data.ships = {}
		self.shipsaveload_data.variables = {}
		self.shipsaveload_data.stats = nil
		self.shipsaveload_data.time = nil
	end

	--stores data into the savefile
	function ShipSave:saveshipfile()
    ba.print('Writing save file\n')
	
		local json = require("dkjson")
		
		local config = {}
		if cf.fileExists(ShipSave.save_file_name) then
			file = cf.openFile(ShipSave.save_file_name, "r", ShipSave.path_shipsave)
			config = json.decode(file:read("*a"))
			file:close()
			if not config then
			  ba.error("Please ensure that " .. ShipSave.save_file_name .. " exists in " .. ShipSave.path_shipsave .. " and is valid JSON.")
			end
		else
			ba.print("SaveLoad: could not open file " .. ShipSave.save_file_name .. " for writing! Previous data may be lost!\n")
		end
		
		if config[ba.getCurrentPlayer():getName()] == nil then
			config[ba.getCurrentPlayer():getName()] = {}
		end
		
		config[ba.getCurrentPlayer():getName()][self.active_filename] = self.shipsaveload_data
		local file = cf.openFile(ShipSave.save_file_name, 'w', ShipSave.path_shipsave)
		file:write(json.encode(config))
		file:close()

    ba.print('Wrote save file\n')
	end

	--loads savefile and splits the lists
	function ShipSave:loadshipfile()
	
		local json = require("dkjson")
		local file = nil
		local config = {}

		if cf.fileExists(ShipSave.save_file_name) then
			file = cf.openFile(ShipSave.save_file_name, "r", ShipSave.path_shipsave)
			config = json.decode(file:read("*a"))
			file:close()
			if not config then
				ba.error("Please ensure that " .. ShipSave.save_file_name .. " exists in " .. ShipSave.path_shipsave .. " and is valid JSON.")
			end
		else
			ba.print("SaveLoad: file " .. ShipSave.save_file_name .. " does not exist!\n")
			return
		end
		
		if config[ba.getCurrentPlayer():getName()] == nil then
			self:clear()
			return
		end
		
		if config[ba.getCurrentPlayer():getName()][self.active_filename] then
			self.shipsaveload_data = config[ba.getCurrentPlayer():getName()][self.active_filename]
		else
			self:clear()
		end
	end
	
	--returns the currently loaded save data
	function ShipSave:getData()
		return self.shipsaveload_data
	end
	
	--replaces the currently loaded save data with the supplied table. DANGEROUS!
	function ShipSave:setData(data)
		self.shipsaveload_data = data
	end
	
	function ShipSave:collect_mission_stats()
		local array = {}
		local stats = ba.getCurrentPlayer().Stats
		array[1] = stats.MissionPrimaryShotsFired
		array[2] = stats.MissionPrimaryShotsHit
		array[3] = stats.MissionPrimaryFriendlyHit
		array[4] = stats.MissionSecondaryShotsFired
		array[5] = stats.MissionSecondaryShotsHit
		array[6] = stats.MissionSecondaryFriendlyHit
		array[7] = stats.MissionTotalKills
		array[8] = stats.MissionAssists
		
		local shipKills = {}
		for i = 1, #tb.ShipClasses do
			local kills = stats:getMissionShipclassKills(tb.ShipClasses[i])
			if kills > 0 then
				shipKills[#shipKills + 1] = {i, kills}
			end
		end
		
		array[9] = shipKills
		
		return array
	end
	
	function ShipSave:set_mission_stats_from_data(array)
		local stats = ba.getCurrentPlayer().Stats
		stats.MissionPrimaryShotsFired = array[1]
		stats.MissionPrimaryShotsHit = array[2]
		stats.MissionPrimaryFriendlyHit = array[3]
		stats.MissionSecondaryShotsFired = array[4]
		stats.MissionSecondaryShotsHit = array[5]
		stats.MissionSecondaryFriendlyHit = array[6]
		stats.MissionTotalKills = array[7]
		stats.MissionAssists = array[8]
		
		for i, v in ipairs(array[9]) do
			local ship = tb.ShipClasses[v[1]]
			local kills = v[2]
			
			stats:setMissionShipclassKills(ship, kills)
		end
	end

	function ShipSave:collect_variable_data(varname)
		if not varname then
			return nil
		end
		local var = mn.SEXPVariables[varname]
		if not var or not var:isValid() then
			return nil
		end

		local t = nil
		if var.Type == SEXPVAR_TYPE_NUMBER then
			t = "n"
		else
			t = "s"
		end
		
		local array = {}
		array[1] = t
		array[2] = var.Value
		return array
	end

	-- tries to set a variable
	function ShipSave:set_variable_from_data(varname, data)
		if not varname or not data then
			return
		end
		local var = mn.SEXPVariables[varname]
		if not var then
			ba.print("SaveLoad: target variable '"..varname.."' not found\n")
			return
		end
		if var.Type == SEXPVAR_TYPE_NUMBER then
			if data[1] ~= "n" then
				ba.print("SaveLoad: variable type mismatch; source is string but target is numeric\n")
				return
			end
			var.Value = tonumber(data[2])
		else
			if data[1] ~= "s" then
				ba.print("SaveLoad: variable type mismatch; source is numeric but target is string\n")
				return
			end
			var.Value = data[2]
		end
	end

	function ShipSave:get_ship_array(shipname)
		if not shipname then
			return nil
		end
		return self:shipdatacollect(shipname)
	end

	function ShipSave:set_ship_from_data(shipname, data, loadtype)
		if not shipname or not data or not loadtype then
			return
		end
		self:shipdataset(shipname, data, loadtype)
	end

	function ShipSave:shipdatacollect(shipname)		--shipdata collector function including string creation
		local ship = mn.Ships[shipname]		--ship handle
		local shipdata = {}

		if (ship:isValid() and (ship:hasShipExploded() == 0)) then		--ship existent and not exploding	
			local basicdata = {}
			basicdata[1] = 1
			basicdata[2] = ship.Class.Name		--ship class
			basicdata[3] = ship.Team.Name			--team
			basicdata[4] = ship.HitpointsMax		--user defined max hp
			basicdata[5] = ship.HitpointsLeft		--hp left
			if (ship.CountermeasureClass:isValid()) then
				basicdata[6] = {}
				basicdata[6][1] = ship.CountermeasuresLeft
				basicdata[6][2] = ship.CountermeasureClass.Name
			else
				basicdata[6] = -1
			end
			if (ship.AfterburnerFuelMax > 0) then
				basicdata[7] = ship.AfterburnerFuelLeft
			else
				basicdata[7] = -1
			end
			if (ship.WeaponEnergyMax > 0) then
				basicdata[8] = ship.WeaponEnergyLeft
			else
				basicdata[8] = -1
			end
			local shields = ship.Shields
			if (shields:isValid()) then		--shield data in case there is a shield, otherwise -1
				basicdata[9] = shields.CombinedMax
				basicdata[10] = shields[1]
				basicdata[11] = shields[2]
				basicdata[12] = shields[3]
				basicdata[13] = shields[4]
			else
				basicdata[9] = -1
				basicdata[10] = -1
				basicdata[11] = -1
				basicdata[12] = -1
				basicdata[13] = -1
			end
			shipdata[1] = basicdata

			local ns = #ship		--# subsystems
			local arr_subsys = {}
			if (ns > 0) then
				arr_subsys[1] = ns
				for i=1,ns do
					arr_subsys[2*i] = ship[i].HitpointsLeft				--hp subsystem
					arr_subsys[2*i+1] = self:weaponsdatacollect(ship[i],2)	--for turrets, only primaries und secondaries
				end
			else
				arr_subsys[1] = -1		--no subsystems
			end
			shipdata[2] = arr_subsys

			shipdata[3] = self:weaponsdatacollect(ship,1)		--normal weapons, primaries, secondaries, tertiaries

			local coords = {}
			for i=1,3 do
				coords[i] = ship.Position[i]
			end
			for i=1,9 do
				coords[i+3] = ship.Orientation[i]
			end
			for i=1,3 do
				coords[i+12] = ship.Physics.Velocity[i]
			end
			for i=1,3 do
				coords[i+15] = ship.Physics.RotationalVelocity[i]
			end
			shipdata[4] = coords
		else
			shipdata[1] = {}
			if (mn.evaluateSEXP("(is-destroyed-delay !0! !"..shipname.."!)")) then
				shipdata[1][1] = -2		--ship destroyed
			elseif (mn.evaluateSEXP("(has-departed-delay !0! !"..shipname.."!)")) then
				shipdata[1][1] = -1		--ship departed
			else
				shipdata[1][1] = -3		--ship handle invalid, ship-vanish used or something like it
			end
		end
		
		return shipdata
	end

	function ShipSave:shipdataset(shipname, shipdata, loadtype)
		local ship = mn.Ships[shipname]		--ship handle
		if not ship:isValid() then
			-- nothing to do
			return
		end

		local basicdata = shipdata[1]
		local shipstatus = basicdata[1]

		-- ship was ingame or wasn't arriving while saving
		if shipstatus == 1 then
			-- maybe change the ship class
			local newclassname = basicdata[2]
			if ship.Class.Name ~= newclassname then
				mn.evaluateSEXP("(change-ship-class !"..newclassname.."! !"..shipname.."!)")
			end

			-- maybe change the ship team
			local newteamname = basicdata[3]
			if (ship.Team.Name ~= newteamname) then
				mn.evaluateSEXP("(change-iff !"..newteamname.."! !"..shipname.."!)")
			end

			ship.HitpointsMax = basicdata[4]
			ship.HitpointsLeft = basicdata[5]
			if (basicdata[6] ~= -1) then
				ship.CountermeasureClass = tb.WeaponClasses[basicdata[6][2]]
				ship.CountermeasuresLeft = basicdata[6][1]
			end

			-- type = 2
			-- loads additionally to type = 1: weapon energy, afterburner energy, shield energy
			if loadtype > 1 then
				if (ship.AfterburnerFuelMax > 0) then
					ship.AfterburnerFuelLeft = basicdata[7]
				end
				if (ship.WeaponEnergyMax > 0) then
					ship.WeaponEnergyLeft = basicdata[8]
				end

				local shields = ship.Shields
				if (shields:isValid() and (basicdata[9] > 0)) then		--ship has shields and save contains shields
					shields.CombinedMax = basicdata[9]
					shields[1] = basicdata[10]
					shields[2] = basicdata[11]
					shields[3] = basicdata[12]
					shields[4] = basicdata[13]
				end
			end

			local arr_subsys = shipdata[2]
			local n_subsystems = arr_subsys[1]
			if (n_subsystems > 0) then		--ship has subsystems?
				for i=1,n_subsystems do
					if ship[i].HitpointsMax > 0 then
						local hitpoints = arr_subsys[2*i]
						if hitpoints > 0 then
							ship[i].HitpointsLeft = hitpoints
						else
							mn.evaluateSEXP("(destroy-subsys-instantly !"..shipname.."! !"..ship[i]:getModelName().."!)")
						end
					end
					self:weaponsinsert(arr_subsys[2*i+1], ship[i], 2)	--turrets loader, only primaries and secondaries
				end
			end

			self:weaponsinsert(shipdata[3], ship, 1)		--ships weapons, primaries, secondaries and tertiaries

			-- type = 3
			-- loads additionally to type = 2: position and orientation
			if loadtype > 2 then
				local coords = shipdata[4]
				ship.Position = ba.createVector(coords[1],coords[2],coords[3])
				ship.Orientation = ba.createOrientation(coords[4],coords[5],coords[6],coords[7],coords[8],coords[9],coords[10],coords[11],coords[12])
			end

		-- recreate departed status
		elseif shipstatus == -1 then
			mn.evaluateSEXP("(set-departure-info !"..shipname.."! !Hyperspace! !<no anchor>! !0! !0! (false))")
			ship:warpOut()

		-- recreate destroyed status
		elseif shipstatus == -2 then
			mn.evaluateSEXP("(destroy-instantly !"..shipname.."!)")

		-- recreate vanished status
		else
			mn.evaluateSEXP("(ship-vanish !"..shipname.."!)")

		end
	end

	function ShipSave:weaponsdatacollect(object,which_wep)		--weapondata to array
		local array = {}
		local bank = object.PrimaryBanks		--handle primaries
		array[1] = self:weaponbankdatacollect(bank)
		bank = object.SecondaryBanks		--handle secondaries
		array[2] = self:weaponbankdatacollect(bank)
		if (which_wep == 1) then		-- 1=main weapons, 2=turrets
			bank = object.TertiaryBanks
			array[3] = self:weaponbankdatacollect(bank)
		else
			array[3] ={}
			array[3][1] = -2
		end
		return array
	end

	function ShipSave:weaponbankdatacollect(bank)		--weaponbankdata to array
		local array = {}
		if (bank:isValid()) then
			local n = #bank		--number of weapons in bank
			if (n > 0) then
				array[1] = n
				array[2] = bank.Linked		--linked
				array[3] = bank.DualFire		--dual fire
				for j=1,n do
					local wbank=bank[j] 	--weaponbank handle
					local weapon = {}
					weapon[1] = wbank.WeaponClass.Name		--weapon class name
					weapon[2] = wbank.Armed		--weapon active
					weapon[3] = wbank.AmmoMax
					if (weapon[3] > 0) then
						weapon[4] = wbank.AmmoLeft
					else
						weapon[4] = -1
					end
					array[3+j] = weapon
				end
			else
				array[1]=-1		--no weapons in bank
			end
		else
			array[1]=-2		--invalid bank, should not happen
		end
		return array
	end

	function ShipSave:weaponsinsert(array,object,which_wep)		--transfers weapondata from array to object
		local bank
		if (array[1][1] > 0) then
			bank=object.PrimaryBanks		--handle primaries
			self:weaponbankinsert(bank,array[1],1)
		end
		if (array[2][1] > 0) then
			bank=object.SecondaryBanks		--handle secondaries
			self:weaponbankinsert(bank,array[2],2)
		end
		if ((which_wep == 1) and (array[3][1] > 0)) then		--1=main weapons, 2=turrets
			bank=object.TertiaryBanks
			self:weaponbankinsert(bank,array[3],0)
		end
	end

	function ShipSave:weaponbankinsert(bank,array,which_wep)		--array to weaponbankdata
		if (bank:isValid()) then
			bank.Linked = array[2]		--linked
			bank.DualFire = array[3]		--dual fire
			local n = array[1]
			for j=1,n do
				local wbank=bank[j] 	--weaponbank handle
				local weapon = array[3+j]
				if (wbank.WeaponClass.Name ~= weapon[1]) then		--prevent it from doing unnecessary stuff
					wbank.WeaponClass = tb.WeaponClasses[weapon[1]]		--weapon class
				end
				local b_armed = weapon[2]
				if b_armed then
					wbank.Armed = b_armed						--weapon active
				end
				-- NOTE: see the documentation on the AmmoMax virtvar in the FSO code.  Setting what you get is not idempotent.
				wbank.AmmoMax = weapon[3] * wbank.WeaponClass.CargoSize
				if (weapon[3] > 0) then
					wbank.AmmoLeft = weapon[4]
				end
			end
		end
	end

]

$On Mission Start:
[
	ShipSave:save_init(mn.getMissionFilename())
]

$On Key Pressed: [
	if CheckpointsCleanup == nil and hv.Key == "F11" then
		CheckpointsCleanup = true
		ba.print("Got manual command to clean up all checkpoint data!\n")
	end
]

#End
