require(game.ReplicatedFirst.ReadyModule)("ServerScript")

local objectModule = require(_G.Modules.ObjectModule)
--local objectInteraction = require(_G.Modules.ObjectInteraction.Main)
local rules = require(_G.Modules.RuleModule)
local commands = require(_G.Modules.CommandsModule)
local partStateModule = require(_G.Modules.PartStateModule)

DEFAULT_MAX_SAVES = 20
SAVE_DATA_LIMIT = 5*10^6
PLAYER_INTERACTION_DISTANCE = 100
PLAYER_MAX_FLOOD = 10
PLAYER_FLOOD_TICK = 4
VEHICLE_SAVE_STORE = "VehicleSaveStore"
VEHICLE_SAVE_INFO_STORE = "VehicleSaveInfo"
USER_MOD_STORE = "ModStore"
MAIN_GAME_PLACEID = 11370438752
PLACE_COOLDOWN = 0.75

--[[if game.PlaceId ~= MAIN_GAME_PLACEID then
	VEHICLE_SAVE_STORE = "VehicleSaveStoreTest"
	VEHICLE_SAVE_INFO_STORE = "VehicleSaveInfoTest"
	USER_MOD_STORE = "ModStoreTest"
end]]

for index,value in pairs(_G.Rules[_G.RuleMode]) do
	rules.setRule(index,value)
end

local function GetStoreData(store,key,ver)
	local success,value = pcall(function() -- Protection call
		local data
		if ver then -- When a version is provided get it based on that
			data = store:GetVersionAsync(key,ver)
		else
			data = store:GetAsync(key)
		end
		return data
	end)
	if ver then print(store,key,ver) end
	if not success then warn(value) end
	return success,value
end

local function SetStoreData(store,key,val)
	local success = pcall(function()
		store:SetAsync(key,val)
	end)
	return success
end

PartExperience = {}
PartCost = {}

local saves = {}

local players = {}

local admins = {
	[558665455] = true
}

local _player = {} -- Metatable for player class
_player.__index = _player

_player.Remove = function(this) -- Clear data
	players[this.userid] = nil
	players[this.instance] = nil
end

_player.IsAdmin = function(this) -- Has commands permissions
	if admins[this.instance.UserId] then return true end
	if this.admin then return true end
end

_player.DrainFloodGate = function(this,amount)
	this.flood = this.flood - amount
	if this.flood < 0 then this.flood = 0 end
end

_player.CheckLastPlaced = function(this)
	local possible = ((tick() - this.last_placed) > PLACE_COOLDOWN)
	if possible then this.last_placed = tick() end
	return possible
end

_player.CheckFloodGate = function(this,amount)
	this.flood = this.flood + amount
	if this.flood > PLAYER_MAX_FLOOD then return false else return true end
end

_player.OwnsPart = function(this,part)
	return _G.Methods.playerOwnsPart(this.instance,part)
end

_player.OwnsSave = function(this,group) -- Owns a save DataStore if...
	if this:IsAdmin() or group:match("public") or group == this.userid then
		return true
	else
		this:Notify("Permission Error","You do not own this save: "..group)
		warn("Permission error: "..group.." "..this.userid)
		return false
	end
end

_player.InRange = function(this,cframe) -- Is the player close enough to interact
	this = this.instance
	if 
		not this or
		not cframe or 
		not this.Character or
		not this.Character.PrimaryPart
	then 
		print("Bad parameters",this,cframe,this.Character)
	end
	return (cframe-this.Character.PrimaryPart.Position).Position.Magnitude < PLAYER_INTERACTION_DISTANCE
end

_player.CanAfford = function(this,part)
end

_player.CanUsePart = function(this,part)
	local experience = PartExperience[part]
	if experience and experience > this.stats.experience then return false end
	local cost = PartCost[part]
	if cost and cost > this.stats.money then return false end
	return not not part:IsDescendantOf(_G.Storage.Parts)
end

_player.Notify = function(this,title,message)
	if this.dummy then return end
	_G.Remotes.Notification:FireClient(this.instance,title,message)
end

_player.GetParts = function(this)
	return _G.Tags:GetTagged("P_"..this.userid)
end

_player.WaitForStatistics = function(this)
	repeat
		wait()
	until this.stats
end

_player.EvaluateSaves = function(this)
	this:WaitForStatistics()
	if this.evaluate_save_lock then repeat wait() until not this.evaluate_save_lock end
	this.evaluate_save_lock = true
	this.max_saves = DEFAULT_MAX_SAVES + (this.stats.additional_saves or 0)
	if game:GetService("MarketplaceService"):UserOwnsGamePassAsync(tonumber(this.userid),1014956678) then 
		this.max_saves = this.max_saves + 20
	end
	this.evaluate_save_lock = false
end

_player.LoadSave = function(this,group,key)
	if not this:CheckFloodGate(1) then return end
	
	--print(string.format("pg:%s,pk:%s",group,key))
	
	local ver
	local _group,_key = group:match("(.-):(.+)")
	if _group and _key then ver = key key = _key group = _group end

	if not players[group] then return end
	if not players[group].saves then error(group,key) end
	
	if not this:OwnsSave(group) then return end
	
	--print(string.format("k:%s,g:%s,v:%s",key,group,ver or "nil"))
	if players[group].saves[key..(ver or "")] then
		return players[group].saves[key]
	else
		local success,data = GetStoreData(players[group].stores.saves,key,ver)
		if success then
			if not ver then players[group].saves[key] = data end
			return data
		end
	end
end

players = {
	public = setmetatable({
		parttag = "P_ublic",
		userid = "public",
		flood = -math.huge,
		saves = {},
		stores = {
			saves = _G.Data:GetDataStore(VEHICLE_SAVE_STORE,"public"),
			save_info = _G.Data:GetDataStore(VEHICLE_SAVE_INFO_STORE,"public")
		}
	},_player)
}

local function GetPlayer(id) -- Get a player by their id AS A STRING
	local player = players[id] or players[tostring(id)]
	if not player then error("No player data identified by "..tostring(id),2) end
	return player
end

local function AddPlayer(instance)
	local data = { -- The player data...
		instance = instance, -- Player instance in game.Players
		userid = tostring(instance.UserId),
		dummy = not instance.Parent,
		name = instance.DisplayName,
		saves = {}, -- Vehicle{} []
		flood = 0, -- If this gets too high, block remotes
		stats = nil,
		join = tick(),
		last_placed = tick(),
		max_saves = DEFAULT_MAX_SAVES,
		parttag = "P_"..tostring(instance.UserId),
		stores = {} -- DataStore
	}
	setmetatable(data,_player) -- ...made into a class
	players[instance] = data -- Register them by id and instance
	players[data.userid] = data
	data.stores.saves = _G.Data:GetDataStore(VEHICLE_SAVE_STORE,data.userid) -- Vehicles
	data.stores.mods = _G.Data:GetDataStore(USER_MOD_STORE,data.userid)	-- Properties
	data.stores.save_info = _G.Data:GetDataStore(VEHICLE_SAVE_INFO_STORE,data.userid) -- Vehicle Save Info
	_,data.stats = GetStoreData(data.stores.mods,"stats")
	if data.stats then
		data.stats = _G.Http:JSONDecode(data.stats)
	else
		data.stats = {}
	end
	_G.Default(data.stats,{
		experience = 0,
		money = 100,
		extra_saves = 0,
		playtime = 0,
		color = require(_G.Modules.NameColorModule).ComputeNameColor(instance.Name)
	})
	wait(2)
	local playtime = data.stats.playtime
	if playtime ~= 0 then
		local ending = "seconds"
		if playtime > 60 then ending = "minutes" playtime = playtime / 60
			if playtime > 60 then ending = "hours" playtime = playtime / 60 
				if playtime > 24 then ending = "days" playtime = playtime / 24 
				end
			end
		end
		data:Notify("Welcome",string.format("Welcome back to Plane Building! You've been playing for %d %s!",math.floor(playtime),ending))
		if game.PlaceId ~= MAIN_GAME_PLACEID then 
			data:Notify("Warning",string.format("You are not in the main game place! Irreversible save corruption and gameplay bugs are likely!"))
		end
	else
		data:Notify("Welcome","Hey newbie, have a warm welcome to Plane Building! Thanks so much for playing!")
	end
end
game.Players.PlayerAdded:Connect(AddPlayer)

local function RemovePlayer(instance) -- For the event
	local player = GetPlayer(instance)
	player.stats.playtime = player.stats.playtime + (tick() - player.join)
	SetStoreData(player.stores.mods,"stats",_G.Http:JSONEncode(player.stats))
	player:Remove()
end
game.Players.PlayerRemoving:Connect(RemovePlayer)

local function Weld(part1,part2,temporary) -- Make a BUILDING weld 
	local weld = Instance.new("WeldConstraint")
	weld.Name = "BuiltWeldConstraint"
	weld.Parent = part1
	weld.Part0 = part1
	weld.Part1 = part2
	if temporary then weld.Name = "WeldConstraint" end -- Or not.
	return weld
end

local function SetupPart(player,part)
	part = part:Clone()
	part.Parent = game.Workspace.Objects
	_G.Tags:AddTag(part,"P_"..player.userid)
	part:SetAttribute("NetworkOwner",player.userid)
	return part
end

local function CopyTags(part1,part2)
	for _,tag in pairs(_G.Tags:GetTags(part2)) do
		if not part1:HasTag(tag) and string.sub(tag,1,2) == "P_" then
			part1:AddTag(tag)
		end
	end 
end

local function AttemptWeld(player,part,connection)
	local new_weld
	if connection and 
		(player:OwnsPart(connection) or player:OwnsPart(connection.Parent))
	then
		local attach = part
		if part:IsA("Model") then attach = attach.PrimaryPart end
		new_weld = Weld(attach,connection)
		new_weld.Parent = part
	end
	CopyTags(part,connection)
	return new_weld
end

local function ResizePart(part,size)
	if part:IsA("Model") then
		part:ScaleTo(size.Magnitude)
	else
		part.Size = size
	end
	part:SetAttribute("Size",size)
end

local function _CreatePart(player,part,cframe,connection,scale)
	local player = GetPlayer(player)
	player:CheckFloodGate(4)
	if 
		not player:CheckLastPlaced() or 
		not player:CanUsePart(part)
	then
		return
	end
	if connection then cframe = connection.CFrame:ToWorldSpace(cframe) end
	if not player:InRange(cframe) then return end
	part = part:Clone()
	part.Parent = game.Workspace.Objects
	if scale then ResizePart(part,scale) end
	_G.Methods.setItemCFrame(part,cframe)
	AttemptWeld(player,part,connection)
	--[[if part:IsA("Model") then
		_G.Methods.setNetworkOwner(part.PrimaryPart,player.instance)
	else
		_G.Methods.setNetworkOwner(part,player.instance)
	end]]
	part:AddTag(player.parttag)
	wait()
	return part
end

local function ConfigurePart(player,part,own)
	part:AddTag(player.parttag)
	part.Parent = game.Workspace.Objects
	local owner
	if own then owner = player.instance end
	objectModule.add(part,owner)
	part:SetAttribute("Owner",player.instance.UserId)
	--local modules = objectInteraction.modulesByName[part.Name]
	--if modules then for _,module in pairs(modules) do if module.setup then module.setup(part) end end end
end

local function CreatePart(player,id,...)
	local player = GetPlayer(player)
	player:CheckFloodGate(4)
	local template = partStateModule.get_part_by_id(id)
	if not template then error(string.format("No part for id: "..tostring(id))) return end
	if
		not player:CheckLastPlaced() or 
		not player:CanUsePart(template)
	then
		return
	end
	local state = partStateModule.new()
	state.owner = player.instance
	state:deserialise(id,...)
	local part = state:create()
	ConfigurePart(player,part,true)
	wait()
	return part
end
_G.Remotes.Create.OnServerInvoke = CreatePart

local function _CreatePoly(player,part,nodes,connections)
	local player = GetPlayer(player)
	if
		player:CheckFloodGate(6) and
		player:CheckLastPlaced() and
		player:CanUsePart(part)
	then
		for index,node in pairs(nodes) do
			if node.connection then node.cframe = node.connection.CFrame:ToWorldSpace(node.cframe) end
		end
		for index,node in pairs(nodes) do
			if (nodes[1].cframe.Position - node.cframe.Position).Magnitude > 80 then return end
			if not player:InRange(node.cframe) then return end
		end 
		part = part:Clone()
		part.Parent = game.Workspace.Objects
		part:AddTag(player.parttag)
		if #nodes == 2 then
			local a = nodes[1].cframe
			local b = nodes[2].cframe
			local d = (a-b).Magnitude
			part.CFrame = CFrame.lookAt(a.Position,b.Position,a.UpVector) * CFrame.new(d * 0.5,0,0)
		elseif #nodes == 3 then
			_G.Methods.form_triangle(part.A,part.B,nodes[1].cframe.Position,nodes[2].cframe.Position,nodes[3].cframe.Position)
		end
		Weld(part.A,part.B)
		for _,connection in pairs(connections) do
			AttemptWeld(player,part.PrimaryPart,connection)
		end
		return part
	end
end
--_G.Remotes.Poly.OnServerInvoke = CreatePoly

local function TweakEffect(player,effect,rate,enabled)
	effect.Enabled = enabled
	effect.Rate = rate
end
_G.Remotes.Effects.OnServerEvent:Connect(TweakEffect)


local function ExplodePart(player,part)
	local player = GetPlayer(player)
	if
		not player:CheckFloodGate(1) or
		not (player:OwnsPart(part) or
		player:OwnsPart(part.Parent))
	then 
		return
	end
	_G.Methods.explode(part)
end
_G.Remotes.Explode.OnServerEvent:Connect(ExplodePart)

local function DeletePart(player,part,destruct)
	local player = GetPlayer(player)
	if 
		part and
		player:CheckFloodGate(1) and
		player:OwnsPart(part)
	then
		if destruct then
			for _,part in pairs(part:GetChildren()) do
				if part:IsA("BasePart") then
					for _,joint in pairs(part:GetJoints()) do
						if joint.Name == "BuiltWeldConstraint" then joint:Destroy() end
					end
				end
			end
		else	
			part:Destroy()
		end
	end
end
_G.Remotes.Delete.OnServerInvoke = DeletePart

local function ClearParts(player)
	local player = GetPlayer(player)
	if not player:CheckFloodGate(1) then return end
	for _,part in pairs(player:GetParts()) do
		part:Destroy()
	end
end
_G.Remotes.Clear.OnServerInvoke = ClearParts

local function ShareParts(player,id,parts)
	local player = GetPlayer(player)
	if not player:CheckFloodGate(1) then return end
	if not parts then parts = player:GetParts() end
	for _,part in pairs(parts) do
		_G.Storage.TeamBox:Clone().Parent = part
		part.TeamBox.Adornee = part
		_G.Debris:AddItem(part.TeamBox,0.5)
		if not id then 
			part:AddTag(players.public.parttag) 
		else
			part:AddTag(GetPlayer(id).parttag)
		end
	end
end
_G.Remotes.Collaborate.OnServerInvoke = ShareParts

local function EnableCombat(player,parts)
	local player = GetPlayer(player)
	if not player:CheckFloodGate(1) then return end
	if not parts then parts = player:GetParts() end
	for _,part in pairs(parts) do
		_G.Storage.DangerBox:Clone().Parent = part
		part.DangerBox.Adornee = part
		_G.Debris:AddItem(part.DangerBox,0.5)
		part:AddTag("Combat")
	end
end
_G.Remotes.Combat.OnServerInvoke = EnableCombat

local function SaveVehicle(player,data,key,group)
	player = GetPlayer(player)
	player:EvaluateSaves()
	if ((key ~= "autosave") and (player.max_saves < tonumber(key))) then
		player:Notify("Save Error","You do not have enough space.") 
		return
	end
	if not player:OwnsSave(group) then return end
	local success,value = SetStoreData(players[group].stores.saves,key,data)
	if not success then player:Notify("Vehicle Save Error",value) error(value) return end
	local length = #data
	local suffix = ""
	if length > 5000 then suffix = "K" length = math.floor(length/1000) end
	player:Notify("Vehicle Saved","Save success, remember to save regularly! Size: "..length..suffix.."C")
	players[group].saves[key] = data
	return success
end
_G.Remotes.Save.OnServerInvoke = SaveVehicle

local function GetSaveVersionInfo(group,key)
	local store = players[group].stores.saves
	local listSuccess, pages = pcall(function()
		return store:ListVersionsAsync(key)
	end)
	local result = {}
	if listSuccess then
		local items
		while true do
			items = pages:GetCurrentPage()
			for key, info in pairs(items) do
				if not info.IsDeleted then 
					result[info.Version] = os.date("%c",info.CreatedTime/1000)
				end
			end	
			if pages.IsFinished then break else pages:AdvanceToNextPageAsync() end
		end
	else
		warn(pages)
	end
	return result
end

SAVE_INFO_EMPTY = "Empty"
EMPTY_SAVE_INFO = {autosave = SAVE_INFO_EMPTY}
for i=1,DEFAULT_MAX_SAVES do EMPTY_SAVE_INFO[tostring(i)] = SAVE_INFO_EMPTY end
local function GetSaveInfo(player,group)
	player = GetPlayer(player)
	group = group or player.userid
	local key
	if group:match("(.-):(.+)") then group,key = group:match("(.-):(.+)") end 
	if not player:OwnsSave(group) then return end
	local max_saves = DEFAULT_MAX_SAVES
	if group == player.userid then player:EvaluateSaves() max_saves = player.max_saves end
	if key then
		return GetSaveVersionInfo(group,key)
	else
		local success,data = GetStoreData(players[group].stores.save_info,"info")
		if not success then
			player:Notify("Save Info Error: ","Your save info failed to load, this is a fatal error, try rejoining.") 
			return
		end
		local save_info = data or {}
		if not save_info.autosave then save_info.autosave = "Unused" end
		for index,item in ipairs(save_info) do
			local data = save_info[index]
			save_info[index] = nil
			save_info[tostring(index)] = data 
		end
		for i = 1,max_saves do if not save_info[tostring(i)] then save_info[tostring(i)] = "Empty" end end -- fill any missing slots
		return save_info
	end
end
_G.Remotes.GetSaveInfo.OnServerInvoke = GetSaveInfo

local function SetSaveInfo(player,data,group)
	player = GetPlayer(player)
	if not player:OwnsSave(group) then return end
	player:EvaluateSaves()
	for index,item in pairs(data) do 
		if not tonumber(index) and index ~= "autosave" then
			player:Notify("Malformed save data! Invalid string: "..index) 
			return 
		end
	end
	if #data > player.max_saves then 
		player:Notify("You do not have enough save slots! "..tostring(#data-1)) 
		return
	end
	SetStoreData(players[group].stores.save_info,"info",data)
	return data
end
_G.Remotes.SetSaveInfo.OnServerInvoke = SetSaveInfo

local function LoadPlayerSave(player,...)
	return GetPlayer(player):LoadSave(...)
end
_G.Remotes.Load.OnServerInvoke = LoadPlayerSave

local function ConfigurePlayerVehicle(player,model)
	for _,part in pairs(model:GetChildren()) do
		ConfigurePart(player,part)
	end
end

local function PlaceVehicle(player,cframe,key,group)
	local player = GetPlayer(player)
	if not player:CheckFloodGate(3) then return end
	for _,part in pairs(_G.Tags:GetTagged("P_"..player.userid)) do
		part:Destroy()
	end
	local raw = player:LoadSave(group,key)
	if not raw then return end
	local model = _G.Methods.saves.dataToModel(raw,true)
	if not model then return end
	model.Parent = game.Workspace
	local primary = model:FindFirstChildWhichIsA("VehicleSeat",true)
	if primary then 
		model.PrimaryPart = primary
	end
	model:PivotTo(cframe)
	--[[task.defer(function()
		if primary then _G.Methods.setNetworkOwner(primary,player.instance) end
	end)]]
	ConfigurePlayerVehicle(player,model)
end
_G.Remotes.Place.OnServerInvoke = PlaceVehicle

-- i hate this...
local function PlaceSubassembly(player,cframe,data,connection)
	local player = GetPlayer(player)
	if not player:CheckFloodGate(10) then return end
	local model = _G.Methods.saves.dataToModel(data,true)
	model:PivotTo(cframe)
	if player:OwnsPart(connection) then Weld(model.PrimaryPart,connection) end
	ConfigurePlayerVehicle(player,model)
end
_G.Remotes.PlaceSubassembly.OnServerInvoke = PlaceSubassembly

local function PlaySoundEffect(player,sound)
	if not GetPlayer(player):CheckFloodGate(1) then return end
	sound:Play()
end
_G.Remotes.PlaySoundEffect.OnServerEvent:Connect(PlaySoundEffect)

local function ChangeSoundEffect(player,sound,volume,pitch)
	if not GetPlayer(player):CheckFloodGate(1) then return end
	if sound then
		if volume then sound.Volume = volume end
		if pitch then sound.Pitch = pitch end
	end
end
_G.Remotes.ChangeSoundEffect.OnServerEvent:Connect(ChangeSoundEffect)

local function Paint(player,part,colour) -- Colour is spelt with a 'u' (pun intended)
	player = GetPlayer(player)
	if not player:CheckFloodGate(1) then return end
	if not player:OwnsPart(part) then return end
	_G.Methods.colorPart(part,colour)
end
_G.Remotes.Paint.OnServerEvent:Connect(Paint)

local function Admin(player)
	player = GetPlayer(player)
	if not player:CheckFloodGate(1) then return end
	return player:IsAdmin()
end
_G.Remotes.Admin.OnServerInvoke = Admin

local function SnapPart(player,weld)
	local player = GetPlayer(player)
	if not player:CheckFloodGate(1) then return end
	if weld.Parent and (player:OwnsPart(weld.Parent) or player:OwnsPart(weld.Parent.Parent)) and weld.Name == "BuiltWeldConstraint" then
		weld:Destroy()
		if weld.Part0 and weld.Part0.Parent:FindFirstChild("BombScript") then weld.Part0.Parent.BombScript.Enabled = true end
		if weld.Part1 and weld.Part1.Parent:FindFirstChild("BombScript") then weld.Part1.Parent.BombScript.Enabled = true end
	else
		print("failed",weld:GetFullName())
	end
end
_G.Remotes.Snap.OnServerEvent:Connect(SnapPart)

local function EditPart(player,part,edit,value)
	if not GetPlayer(player):CheckFloodGate(1) then return end
	if GetPlayer(player):OwnsPart(part) then
		if edit == "Size" then
			ResizePart(part,value)
		end
		if _G.IsNumber(value) then
			local min = edit:GetAttribute("Lower") or -math.huge
			local max = edit:GetAttribute("Upper") or math.huge
			edit.Value = math.clamp(value,min,max)
		else
			edit.Value = value
		end
		objectModule.update(part)
		--[[local modules = objectInteraction.modulesByName[part.Name] or {}
		for _,module in pairs(modules) do
			if module.setup then module.setup(part) end
		end]]
	end
end
_G.Remotes.Edit.OnServerInvoke = EditPart

--_G.Remotes.Sell

commands.new("get user id (.+)","get user id <name>: Get's a player's userid by name.",function(player,name)
	local player = GetPlayer(player)
	if name then
		player:Notify("Get User Id",game.Players:GetUserIdFromNameAsync(name))
	end	
end)
commands.new("open player (.+)","open player <userid>: Load a player's data on the server.",function(player,userid)
	local player = GetPlayer(player)
	if not player:IsAdmin() then player:Kick("Security") return end
	if userid then
		local name = game.Players:GetNameFromUserIdAsync(tonumber(userid))
		AddPlayer({UserId = tonumber(userid),DisplayName = name,Name = name})
		player:Notify("Loading Virtual Player",name)
	end	
end)
commands.new("set save ([^%s]+) (%d+) ([^%s+]) data:(.*)","set store <store> <userid> <key> <value>: Set the save slot data.",function(player,store,userid,key,value)
	local player = GetPlayer(player)
	if not player:IsAdmin() then player:Kick("Security") return end
	print("Modifying store: ",store,userid,key,value)
	SetStoreData(players[userid].stores.saves,key,value)
	player:Notify("Set Store Value","Success?")
end)
commands.new("read player stats%((.-)%)","",function(player,userid)
	local player = GetPlayer(player)
	if userid then
		local other = GetPlayer(tostring(userid))
		player:Notify(other.instance.DisplayName.." Statistics",_G.Http:JSONEncode(other.stats))
	end
end)
commands.new("restart server","restart server: Update to latest version.",function(player,userid)
	local player = GetPlayer(player)
	if not player:IsAdmin() then player:Kick("Security") return end
	_G.Remotes.Notification:FireAllClients("Server Restarting","Hold tight!")
	game:GetService('TeleportService'):TeleportPartyAsync(137149259787276,game.Players:GetPlayers(),{moveTo = game.PlaceId})
end)
commands.new("rejoin","rejoin: Makes you rejoin.",function(player,userid)
	local player = GetPlayer(player)
	_G.Remotes.Notification:FireClient(player.instance,"Rejoining","Hold tight!")
	game:GetService('TeleportService'):TeleportAsync(game.PlaceId,{player.instance})
end)


--[[local store,userid,key,value = message:match("setstore%((.-),(%d-),(.-),(.-)%)")
	if store then
		if player then
			print("Modifying store: ",store,userid,key,value)
			player.stores.saves:SetAsync(key,value)
		end
	end]]

local function SetMotor(character,motor,C0,C1) -- Allow clients to adjust their character motors
	if not character:FindFirstChildWhichIsA("Humanoid") then
		character = character.Character
	end
	if 
		not character or 
		not motor or
		not motor:IsDescendantOf(character) 
	then
		return 
	end

	motor.C0 = C0 motor.C1 = C1
end
_G.Remotes.MotorMoveClient.OnServerEvent:Connect(SetMotor)

local textService = game:GetService("TextService")
local function Chat(instance,message)
	local player = GetPlayer(instance)
	if not player:CheckFloodGate(5) then return end
	message = textService:FilterStringAsync(message,instance.UserId,Enum.TextFilterContext.PublicChat):GetNonChatStringForBroadcastAsync()
	_G.Remotes.Chat:FireAllClients(instance.DisplayName.." | "..instance.Name,message,player.stats.color)
	game.Chat:Chat(instance.Character,message)
end
_G.Remotes.Chat.OnServerEvent:Connect(Chat)

local function ApplyState(player,part,...)
	local player = GetPlayer(player)
	if not player:CheckFloodGate(2) then return end
	if not player:OwnsPart(part) then return end
	local state = partStateModule.new()
	state.owner = player.instance
	state.part = part
	state:deserialise(...)
	state:apply()
end
_G.Remotes.Apply.OnServerEvent:Connect(ApplyState)

spawn(function() -- Drain all flood gates every heartbeat
	while wait() do
		for id,player in pairs(players) do
			if _G.IsString(id) then player:DrainFloodGate(PLAYER_FLOOD_TICK) end
		end
	end
end)

spawn(function() -- Make the game rain for players every now and then
	while true do
		_G.Remotes.Rain:FireAllClients(false)
		wait(math.random(1,720))
		_G.Remotes.Rain:FireAllClients(true)
		wait(math.random(1,60))
	end
end)