Files
PlaneBuilding/scripts/server/ServerScriptNew
T

793 lines
24 KiB
Plaintext

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)