435 lines
13 KiB
Lua
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} |