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 : 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 : 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 : 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)