Files
PlaneBuilding/scripts/modules/ObjectModule.lua
T

435 lines
13 KiB
Lua

-- Module for handling all game objects
require(game.ReplicatedFirst.ReadyModule)
local input = require(_G.Modules.InputModule)
-- Note that closures absorb upvalues. I'm trying this as a programming paradigm here.
local modulesByName = {}
local modules = {} -- [Name] = {...module}
local predicates = {}
local objects = {} -- Table of globally considered parts (responsible for considering) (consider = input and background tasks)
local owning = {} -- Table of locally owned parts (responsible for all processing)
-- Get the object from a part and do something to it
local function asObject(part,callback)
if part.Parent:GetAttribute("PartId") then return callback(part.Parent) -- Since parts can be INSIDE of objects
elseif part:GetAttribute("PartId") then return callback(part) end
end
local processOwnership
local context = {
controls = {},
root = nil -- Where the system is controlling from (Seat)
}
context.message = function(instance,name,...)
local stuff = {...}
asObject(instance,function(object)
local data = objects[object]
if not data then return end
if name == "" then name = "default" end
name = name or "default"
for _,module in pairs(modules[object.Name] or {}) do
if module.messages then
if module.messages[name] then
if objects[object] then
module.messages[name](data,instance,context,table.unpack(stuff))
end
end
end
end
end)
end
context.connected = function(wire_tag,end_tag,root,callback)
local searched = {}
local search = {root}
while #search > 0 do
local new_search = {}
for _,search_part in pairs(search) do
local connected = search_part:GetConnectedParts()
for _,part in pairs(connected) do
if not searched[part] then
searched[part] = true
if part:HasTag(wire_tag) then
table.insert(new_search,part)
if part:GetAttribute("PartId") and part:HasTag("Glow") then part.Material = Enum.Material.Neon end
end
if (not end_tag) or part:HasTag(end_tag) or part.Parent:HasTag(end_tag) then
asObject(part,function(object)
callback(part)
end)
end
end
end
end
search = new_search
end
end
local queues = {}
context.queue = function(this,tag,delta,valid)
-- Init
valid = valid or owning
local queue = queues[tag]
if queue then queue = {last = tick()} queues[tag] = queue end
if not queue[this] then queue[this] = this table.insert(queue,1,this) end
-- Get next in line
local first = queue[1]
while not valid[first] do table.remove(queue,1) first = queue[1] end -- Remove removed objects (lol)
-- Are you ready!?
if tick() - queue.last > delta then -- Aye aye captain!!
queue.last = tick() -- Oooo
table.remove(queue,1)
table.insert(queue,this)
return true -- hhhh
else
return false
end
end
context.frequency = function(this,tag,delta)
if not this[tag] then this[tag] = tick() end
if tick() - this[tag] > delta then
this[tag] = tick()
return true
else
return false
end
end
context.asObject = asObject
if _G.IsServer then
context.replicate = function(this,object,module,...)
module.replicate(this,object,context,...)
end
elseif _G.IsClient then
context.replicate = function(this,object,module,...)
if module.replicate then module.replicate(this,object,context,...) end
if tick() > module.next_replication then
module.next_replication = tick() + module.replication_delta
_G.Remotes.ObjectReplicate:FireServer(object,module.name,...)
end
end
end
local thisPlayer
if _G.IsServer then
thisPlayer = nil
elseif _G.IsClient then
thisPlayer = game.Players.LocalPlayer
end
-- Make object data from an instance (it's shared, so modules can communicate easily)
local function new(object)
local this = {}
for module,predicate in pairs(predicates) do
if predicate(object) then
local tab = modules[object.Name]
if not tab then tab = {} modules[object.Name] = tab end
tab[module] = module
end
end
for _,module in pairs(modules[object.Name] or {}) do
_G.Protected(module.new,this,object,context)
end
objects[object] = this
return this
end
local function remove(object)
local this = objects[object]
if not this then return end
for _,module in pairs(modules[object.Name] or {}) do
if module.remove then _G.Protected(module.remove,this,object,context) end
end
objects[object] = nil
owning[object] = nil
end
local own
-- Add an object to be considered by this client or server
local function add(instance,owner)
-- If this is not an instance or we already considered it then ignore
local data = objects[instance]
if data then return data end
if not instance:GetAttribute("PartId") then return end
data = new(instance)
objects[instance] = data
instance.Destroying:Connect(function()
remove(instance)
end)
if _G.IsServer then
if owner then
data.owner = owner
_G.Protected(function()
if instance:IsA("Model") then
instance.PrimaryPart:SetNetworkOwner(owner)
if instance:FindFirstChild("Hinge") then
instance.Hinge:SetNetworkOwner(owner)
end
else
instance:SetNetworkOwner(owner)
end
end)
else
own(instance)
end
else
if owner then own(instance) end
end
return data
end
local function update(instance)
local data = objects[instance]
if data then
for _,module in pairs(modules[instance.Name] or {}) do
if module.update then _G.Protected(module.update,data,instance,context) end
end
end
end
-- Make the client drop ownership and continue to run idle tasks
local function sleep(object)
if add(object) then owning[object] = nil end
end
-- Make the client own the object and take responsibility for processing
own = function(object)
if add(object) then owning[object] = objects[object] end
end
-- Get all parts connected to an instance, determine if the player is still connected in any way
local hrp = {HumanoidRootPart = true}
local function playerInSeat(seat)
if (seat:IsA("Seat") or seat:IsA("VehicleSeat")) and seat.Occupant then
return game.Players:GetPlayerFromCharacter(seat.Occupant.Parent)
else
local seat = seat:FindFirstChildWhichIsA("Seat") or seat:FindFirstChildWhichIsA("VehicleSeat")
if seat and seat.Occupant then
return game.Players:GetPlayerFromCharacter(seat.Occupant.Parent)
end
end
end
local vs = {VehicleSeat = true}
local function maxRankSeat(seat1,seat2)
if not seat1 then return seat2 end
if not seat2 then return seat1 end
local priority1 = seat1:GetAttribute("Priority") or 0
local priority2 = seat2:GetAttribute("Priority") or 0
-- A seat has priority over the other
if priority1 == priority2 then
local class1 = vs[seat1.Name]
local class2 = vs[seat2.Name]
-- One seat is a VehicleSeat and the other isn't
if class1 and not class2 then return seat1
elseif class2 and not class1 then return seat2
else
local owner1 = game.Players:GetPlayerByUserId(seat1:GetAttribute("Owner") or 0) == playerInSeat(seat1)
local owner2 = game.Players:GetPlayerByUserId(seat2:GetAttribute("Owner") or 0) == playerInSeat(seat2)
-- One player is seating in a seat that they placed themselves
if owner1 and not owner2 then return seat1
elseif owner2 and not owner1 then return seat2
else return seat1 -- Since it was probably closest
end
end
elseif priority1 > priority2 then return seat1 else return seat2 end
end
local function getParts(instance)
local searchedParts = {} -- Searched connected instance parts
local objectsFound = {} -- Found connected objects
local assemblyRoots = {} -- Found AssemblyRootParts
local playersSeated = {} -- Players that are connected to this vehicle in some way and their associated seat
-- Recursive function to look through all the parts
local function search(part)
-- DRY
local function add(instance)
searchedParts[instance] = true
local root = instance.AssemblyRootPart
if not assemblyRoots[root] then assemblyRoots[root] = root end
asObject(instance,function(object)
if instance:FindFirstChild("SeatWeld") then
local hrp = instance.SeatWeld.Part1
local player = game.Players:GetPlayerFromCharacter(hrp.Parent)
if player then
-- Players could be in multiple seats so just pick the highest one
playersSeated[player] = maxRankSeat(playersSeated[player],object)
end
end
objectsFound[object] = object
end)
end
-- If not found, add it, otherwise return
if not searchedParts[part] then
add(part)
else return end
-- For all the connected parts do the same thing, but not 'search' since it causes a bunch of excess checking
for _,part in pairs(part:GetConnectedParts(true)) do
-- If not found, add it, otherwise continue
if not searchedParts[part] then
add(part)
else continue end
-- If the part is connected to a Character
local joints = part:GetJoints()
for _,joint in pairs(joints) do
if joint:IsA("HingeConstraint") or joint:IsA("PrismaticConstraint") then
-- Also check joints since their network ownership is funky otherwise
if joint.Attachment0 and joint.Attachment1 then
search(joint.Attachment0.Parent)
search(joint.Attachment1.Parent)
end
end
end
end
end
search(instance)
return objectsFound,playersSeated,assemblyRoots
end
-- Out of a list of player-seats get the highest priority player seated (nil == server)
local function maxPlayerSeated(playersSeated)
local maxPlayer,maxSeat = nil
for player,seat in pairs(playersSeated) do
if maxRankSeat(maxSeat,seat) then maxPlayer = player maxSeat = seat end
end
return maxPlayer
end
local function mark(object)
if object:FindFirstChild("Mark") then object.Mark:Destroy() end
local box = Instance.new("SelectionBox")
box.Parent = object
box.Adornee = object
box.Name = "Mark"
return box
end
-- This sort of runs on every client, so maybe it's far too slow?
processOwnership = function(seat)
local objectsFound,playersSeated,assemblyRoots = getParts(seat)
local ownerPlayer = maxPlayerSeated(playersSeated)
-- The server needs to change the network owner in this case
if _G.IsServer then
_G.Remotes.OwnershipReplicate:FireAllClients(seat)
for _,root in pairs(assemblyRoots) do
local success,result = pcall(function()
root:SetNetworkOwner(ownerPlayer)
end)
if not success then warn(result) end
end
for object in pairs(objectsFound) do
local data = objects[object]
if data then data.owner = ownerPlayer end
end
if ownerPlayer then
for object in pairs(objectsFound) do
sleep(object)
end
end
end
if ownerPlayer == thisPlayer then
for _,object in pairs(objectsFound) do -- Connected and Owning
--if _G.IsClient then mark(object).Color3 = Color3.fromRGB(0,255,0) end
own(object)
end
elseif playersSeated[thisPlayer] then -- Connected and not Owning
for _,object in pairs(objects) do
if objectsFound[object] then
--if _G.IsClient then mark(object).Color3 = Color3.fromRGB(255,0,0) end
sleep(object)
end
end
elseif _G.IsClient then -- Disconnected and not Owning
for object,data in pairs(objects) do
if objectsFound[object] then
--if _G.IsClient then mark(object).Color3 = Color3.fromRGB(0,0,255) end
remove(object)
end
end
end
if _G.IsClient then
local ourSeat = playersSeated[thisPlayer]
if ourSeat then
input.seat = ourSeat
context.seat = ourSeat
elseif input.seat == asObject(seat,function(...) return ... end) then
input.seat = nil
context.seat = nil
end
end
end
local function objectsProcess(delta)
context.delta = delta
context.cursor = input.cursor
for object,data in pairs(owning) do
for _,module in pairs(modules[object.Name] or {}) do
if module.run then if not module.run(data,object,context) then remove(object) end end
end
end
for object,data in pairs(objects) do
for _,module in pairs(modules[object.Name] or {}) do
if module.idle then if not module.idle(data,object,context) then remove(object) end end
end
end
for _,module in pairs(modulesByName) do
if module.loop then
module.loop(context)
end
end
end
if _G.IsServer then
-- Clients and servers will send this to update information between each other (should be handled by values)
_G.Remotes.ObjectReplicate.OnServerEvent:Connect(function(player,object,name,...)
local data = objects[object]
if data and data.owner == player then
modulesByName[name].replicate(data,object,context,...)
end
end)
game.Players.PlayerRemoving:Connect(function(player)
for object,data in pairs(objects) do
if data.owner == player then
own(object)
end
end
end)
elseif _G.IsClient then
-- Respond to processing events from the server
_G.Remotes.OwnershipReplicate.OnClientEvent:Connect(function(seat)
processOwnership(seat)
end)
end
for _,instance in pairs(script.Parent.Objects:GetChildren()) do
local module = require(instance)
modulesByName[instance.Name] = module
module.name = instance.Name
module.next_replication = tick()
module.replication_delta = module.replication_delta or 0.5
if module.manifest.predicate then
predicates[module] = module.manifest.predicate
else
for _,name in pairs(module.manifest) do
modules[name] = modules[name] or {}
table.insert(modules[name],module)
end
end
end
if _G.IsServer then
for _,instance in pairs(game.Workspace.Objects:GetChildren()) do
add(instance)
end
end
spawn(function()
_G.Run.Heartbeat:Connect(function(delta)
if _G.IsClient then context.controls = input.values end
objectsProcess(delta)
end)
end)
return {processOwnership = processOwnership, add = add, update = update, own = own}