From b17983dcf43a9cf16e2e61f50a11ccf7805358da Mon Sep 17 00:00:00 2001 From: billy-price Date: Sat, 12 Nov 2022 10:12:59 +0000 Subject: [PATCH] turned replay into replayutils --- aftman.toml | 4 +- default.project.json | 18 +- release.project.json | 19 +- selene.toml | 5 +- src/BoardRecorder/BoardReplay.lua | 113 ++++--- src/BoardRecorder/init.lua | 133 ++++++-- src/BoardRecorder/persist.lua | 160 ---------- src/BoardRecorder/remoteTokens.lua | 9 + src/BoardSerialiser/FigureSerialiser.lua | 146 +++++++++ src/BoardSerialiser/init.lua | 49 +++ src/BoardTalkRecorder.lua | 196 ------------ src/CharacterRecorder/CharacterReplay.lua | 193 ------------ src/CharacterRecorder/config.lua | 27 -- src/CharacterRecorder/init.lua | 86 ----- src/CharacterRecorder/persist.lua | 94 ------ src/CharacterRecorder/serialiser.lua | 58 ---- src/EventRecorder/EventReplay.lua | 43 +-- src/EventRecorder/init.lua | 58 +++- src/EventRecorder/persist.lua | 103 ------ src/HumanoidDescriptionSerialiser.lua | 130 ++++++++ src/Recorder.lua | 196 ------------ src/Replayer.lua | 134 -------- src/SoundReplay.lua | 121 +++++++ src/VRCharacterRecorder/VRCharacterReplay.lua | 185 ++--------- src/VRCharacterRecorder/init.lua | 58 ++-- src/VRCharacterRecorder/persist.lua | 126 -------- src/VRCharacterRecorder/serialiser.lua | 50 --- .../updateAnchoredFromInputs.lua | 25 +- src/dataSerialiser.lua | 166 ---------- src/init.lua | 12 + src/persist.lua | 298 ------------------ src/persistTools/chunker.lua | 72 ----- src/persistTools/safeSet.lua | 17 - src/persistTools/waitForBudget.lua | 8 - test.project.json | 25 ++ version.txt | 1 - wally.toml | 7 +- 37 files changed, 814 insertions(+), 2331 deletions(-) delete mode 100644 src/BoardRecorder/persist.lua create mode 100644 src/BoardRecorder/remoteTokens.lua create mode 100644 src/BoardSerialiser/FigureSerialiser.lua create mode 100644 src/BoardSerialiser/init.lua delete mode 100644 src/BoardTalkRecorder.lua delete mode 100644 src/CharacterRecorder/CharacterReplay.lua delete mode 100644 src/CharacterRecorder/config.lua delete mode 100644 src/CharacterRecorder/init.lua delete mode 100644 src/CharacterRecorder/persist.lua delete mode 100644 src/CharacterRecorder/serialiser.lua delete mode 100644 src/EventRecorder/persist.lua create mode 100644 src/HumanoidDescriptionSerialiser.lua delete mode 100644 src/Recorder.lua delete mode 100644 src/Replayer.lua create mode 100644 src/SoundReplay.lua delete mode 100644 src/VRCharacterRecorder/persist.lua delete mode 100644 src/VRCharacterRecorder/serialiser.lua delete mode 100644 src/dataSerialiser.lua create mode 100644 src/init.lua delete mode 100644 src/persist.lua delete mode 100644 src/persistTools/chunker.lua delete mode 100644 src/persistTools/safeSet.lua delete mode 100644 src/persistTools/waitForBudget.lua create mode 100644 test.project.json delete mode 100644 version.txt diff --git a/aftman.toml b/aftman.toml index db8383c..1b1753f 100644 --- a/aftman.toml +++ b/aftman.toml @@ -5,6 +5,4 @@ [tools] rojo = "rojo-rbx/rojo@7.2.1" selene = "Kampfkarren/selene@0.20.0" -wally = "UpliftGames/wally@0.3.1" -# wally = "UpliftGames/wally@0.3.1" -# rojo = "rojo-rbx/rojo@6.2.0" \ No newline at end of file +wally = "UpliftGames/wally@0.3.1" \ No newline at end of file diff --git a/default.project.json b/default.project.json index b38eea4..6e077de 100644 --- a/default.project.json +++ b/default.project.json @@ -1,16 +1,16 @@ { - "name": "replay", + "name": "replayutils", "tree": { - "$className": "DataModel", + "$path": "Packages", + "$className": "ModuleScript", - "ReplicatedStorage": { - "replay": { - "$path": "src", + "$properties": { + "Source": "return require(script.lib)" + }, + + "lib": { + "$path": "src" - "Packages": { - "$path": "Packages" - } - } } } diff --git a/release.project.json b/release.project.json index 7c45798..397f082 100644 --- a/release.project.json +++ b/release.project.json @@ -1,14 +1,21 @@ { - "name": "replay", + "name": "replayutils", "tree": { "$path": "src", - "Packages": { - "$path": "Packages" - }, + "Replay": { + "$path": "Packages", + "$className": "ModuleScript", - "version": { - "$path": "version.txt" + + "$properties": { + "Source": "return require(script.lib)" + }, + + "lib": { + "$path": "lib" + + } } } diff --git a/selene.toml b/selene.toml index 7851318..1f1e170 100644 --- a/selene.toml +++ b/selene.toml @@ -1,4 +1 @@ -std = "roblox" - -[config] -undefined_variable = { ignore_pattern = "(^_|rbx|Array|Set|Dictionary)" } \ No newline at end of file +std = "roblox" \ No newline at end of file diff --git a/src/BoardRecorder/BoardReplay.lua b/src/BoardRecorder/BoardReplay.lua index ee94c62..33ad966 100644 --- a/src/BoardRecorder/BoardReplay.lua +++ b/src/BoardRecorder/BoardReplay.lua @@ -1,39 +1,62 @@ -- Services -local replay = script.Parent.Parent -local metaboard = game:GetService("ServerScriptService").metaboard +local Replay = script.Parent.Parent -- Imports -local t = require(replay.Packages.t) - --- Helper functions -local persist = require(script.Parent.persist) +local t = require(Replay.Parent.t) local BoardReplay = {} BoardReplay.__index = BoardReplay -local check = t.strictInterface({ +export type ReplayArgs = { + + Board: any, + Origin: CFrame, +} + +local checkReplayArgs = t.strictInterface({ Board = t.any, - InitFigures = t.table, - InitNextFigureZIndex = t.number, - Timeline = t.table, + Origin = t.CFrame, }) -function BoardReplay.new(args) +function BoardReplay.new(record: {Timeline: {any}}, replayArgs: ReplayArgs) - assert(check(args)) + assert(checkReplayArgs(replayArgs)) - return setmetatable(args, BoardReplay) -end + local tokenToAuthorId = {} -function BoardReplay:Init() + for authorId, token in record.AuthorIdTokens do - for watcher in pairs(self.Board.Watchers) do - self.Board.Remotes.SetData:FireClient(watcher, self.InitFigures, {}, {}, self.InitNextFigureZIndex, nil, nil) + if tokenToAuthorId[token] then + + error("[BoardReplay] Non-distinct authorId tokens") + end + + tokenToAuthorId[token] = authorId end - self.Board:LoadData(self.InitFigures, {}, {}, self.InitNextFigureZIndex, nil, nil) - self.Board:DataChanged() + local tokenToRemoteName = {} + + for remoteName, token in record.RemoteNameTokens do + + if tokenToRemoteName[token] then + + error("[BoardReplay] Non-distinct remote name tokens") + end + + tokenToRemoteName[token] = remoteName + end + + return setmetatable({ + Record = record, + __tokenToAuthorId = tokenToAuthorId, + __tokenToRemoteName = tokenToRemoteName, + Board = replayArgs.Board, + Origin = replayArgs.Origin, + }, BoardReplay) +end + +function BoardReplay:Init() self.TimelineIndex = 1 self.Finished = false @@ -41,19 +64,45 @@ end function BoardReplay:PlayUpTo(playhead: number) - while self.TimelineIndex <= #self.Timeline do + while self.TimelineIndex <= #self.Record.Timeline do - local event = self.Timeline[self.TimelineIndex] + local event = self.Record.Timeline[self.TimelineIndex] if event[1] <= playhead then - local timeStamp, remoteName, args = unpack(event) + local remoteName = self.__tokenToRemoteName[event[2]] + local authorId = self.__tokenToAuthorId[event[3]] + local args = {} + + if remoteName == "InitDrawingTask" then + + local taskId, taskType, width, r, g, b, x, y = unpack(event, 4) + + local drawingTask = { + Id = taskId, + Type = taskType, + Curve = { + Type = "Curve", + Points = nil, + Width = width, + Color = Color3.new(r,g,b) + }, + Verified = true, + } + + args = {drawingTask, Vector2.new(x, y)} + elseif remoteName == "UpdateDrawingTask" then + + local x, y = unpack(event, 4) + + args = {Vector2.new(x, y)} + end for watcher in pairs(self.Board.Watchers) do - self.Board.Remotes[remoteName]:FireClient(watcher, unpack(args)) + self.Board.Remotes[remoteName]:FireClient(watcher, "replay-"..authorId, unpack(args)) end - self.Board["Process"..remoteName](self.Board, unpack(args)) + self.Board["Process"..remoteName](self.Board, "replay-"..authorId, unpack(args)) self.TimelineIndex += 1 continue @@ -62,24 +111,10 @@ function BoardReplay:PlayUpTo(playhead: number) break end - if self.TimelineIndex > #self.Timeline then + if self.TimelineIndex > #self.Record.Timeline then self.Finished = true end end -function BoardReplay.Restore(dataStore: DataStore, key: string, replayArgs) - - local restoredArgs = persist.Restore(dataStore, key, replayArgs.Board) - - return BoardReplay.new({ - - InitFigures = restoredArgs.InitFigures, - InitNextFigureZIndex = restoredArgs.InitNextFigureZIndex, - Timeline = restoredArgs.Timeline, - - Board = replayArgs.Board, - }) -end - return BoardReplay diff --git a/src/BoardRecorder/init.lua b/src/BoardRecorder/init.lua index cdb4e7d..74489d8 100644 --- a/src/BoardRecorder/init.lua +++ b/src/BoardRecorder/init.lua @@ -1,10 +1,9 @@ -- Services -local replay = script.Parent +local Replay = script.Parent -- Imports -local t = require(replay.Packages.t) -local BoardReplay = require(script.BoardReplay) -local persist = require(script.persist) +local t = require(Replay.Parent.t) +local remoteTokens = require(script.remoteTokens) local BoardRecorder = {} BoardRecorder.__index = BoardRecorder @@ -22,32 +21,90 @@ function BoardRecorder.new(args) return setmetatable(args, BoardRecorder) end +local NUM_LENGTH_EST = 20 -- Average is about 19.26 + function BoardRecorder:Start(startTime) -- Start time is passed as argument for consistency between recorders self.StartTime = startTime self.Timeline = {} + self._sizeAcc = 0 - self.InitFigures = self.Board:CommitAllDrawingTasks() - self.InitNextFigureZIndex = self.Board.NextFigureZIndex + self.RemoteNameTokens = remoteTokens + self.AuthorIdTokens = {} - local connections = {} + local newAuthorIdToken = function() + + local maxToken = 0 + for _, token in self.AuthorIdTokens do + + maxToken = math.max(maxToken, token) + end + + return maxToken + 1 + end - table.insert(connections, self.Board.Remotes.InitDrawingTask.OnServerEvent:Connect(function(player, drawingTask, canvasPos) + local connections = {} - drawingTask.Verified = true + + for remoteName, remoteToken in self.RemoteNameTokens do - table.insert(self.Timeline, {os.clock() - self.StartTime, "InitDrawingTask", {"replay-"..tostring(player.UserId), drawingTask, canvasPos}}) - end)) - - for _, remoteName in ipairs({"UpdateDrawingTask", "FinishDrawingTask", "Undo", "Redo", "Clear"}) do + if remoteName == "InitDrawingTask" then - local con = self.Board.Remotes[remoteName].OnServerEvent:Connect(function(player, ...) + table.insert(connections, self.Board.Remotes[remoteName].OnServerEvent:Connect(function(player, drawingTask, canvasPos) + + local now = os.clock() - self.StartTime + + local authorId = tostring(player.UserId) + local authorIdToken = self.AuthorIdTokens[authorId] or newAuthorIdToken() + self.AuthorIdTokens[authorId] = authorIdToken + + local taskId = drawingTask.Id + local taskType = drawingTask.Type + local width = drawingTask.Curve.Width + local color = drawingTask.Curve.Color + + self._sizeAcc += + NUM_LENGTH_EST -- now + + 1 -- remoteToken + + #tostring(authorIdToken) -- authorIdToken + + #taskId + 2 + + #taskType + 2 + + 6 * NUM_LENGTH_EST -- rest of the numbers + + 12 -- braces and commas + + table.insert(self.Timeline, {now, remoteToken, authorIdToken, taskId, taskType, width, color.R, color.G, color.B, canvasPos.X, canvasPos.Y}) + end)) + elseif remoteName == "UpdateDrawingTask" then - table.insert(self.Timeline, {os.clock() - self.StartTime, remoteName, {"replay-"..tostring(player.UserId), ...}}) - end) - - table.insert(connections, con) + table.insert(connections, self.Board.Remotes[remoteName].OnServerEvent:Connect(function(player, canvasPos) + + local now = os.clock() - self.StartTime + + local authorId = tostring(player.UserId) + local authorIdToken = self.AuthorIdTokens[authorId] or newAuthorIdToken() + self.AuthorIdTokens[authorId] = authorIdToken + + self._sizeAcc += 3 * NUM_LENGTH_EST + #tostring(authorIdToken) + 9 -- inc braces, commas, quotes, remoteToken + + table.insert(self.Timeline, {now, remoteToken, authorIdToken, canvasPos.X, canvasPos.Y}) + end)) + + else + + table.insert(connections, self.Board.Remotes[remoteName].OnServerEvent:Connect(function(player) + + local now = os.clock() - self.StartTime + + local authorId = tostring(player.UserId) + local authorIdToken = self.AuthorIdTokens[authorId] or newAuthorIdToken() + self.AuthorIdTokens[authorId] = authorIdToken + + self._sizeAcc += NUM_LENGTH_EST + #tostring(authorIdToken) + 7 -- inc braces, commas, quotes, remoteToken + + table.insert(self.Timeline, {now, remoteToken, authorIdToken}) + end)) + end end self.Connections = connections @@ -55,25 +112,45 @@ end function BoardRecorder:Stop() - for _, con in ipairs(self.Connections) do + for _, con in ipairs(self.Connections or {}) do con:Disconnect() end + + self.Connections = nil end -function BoardRecorder:CreateReplay() +function BoardRecorder:FlushTimelineToRecord() - return BoardReplay.new({ - - Board = self.Board, - InitFigures = self.InitFigures, - InitNextFigureZIndex = self.InitNextFigureZIndex, + local record = { + Timeline = self.Timeline, - }) + AuthorIdTokens = self.AuthorIdTokens, + RemoteNameTokens = self.RemoteNameTokens, + } + + self.Timeline = {} + self._sizeAcc = 0 + + return record end -function BoardRecorder:Store(dataStore: DataStore, key: string) +local SCAFFOLD = #[[{"Timeline":{},"AuthorIdTokes":{},"RemoteNameTokens":{}}]] + +function BoardRecorder:GetRecordSizeEstimate() + + local size = self._sizeAcc + + for authorId, token in self.AuthorIdTokens do + + size += #authorId + #tostring(token) + 3 -- quotes and comma + end + + for remoteName, token in self.RemoteNameTokens do + + size += #remoteName + #tostring(token) + 3 -- quotes and comma + end - return persist.Store(self, dataStore, key) + return size + SCAFFOLD end diff --git a/src/BoardRecorder/persist.lua b/src/BoardRecorder/persist.lua deleted file mode 100644 index 4c13e44..0000000 --- a/src/BoardRecorder/persist.lua +++ /dev/null @@ -1,160 +0,0 @@ --- Services -local replay = script.Parent.Parent -local metaboard = game:GetService("ServerScriptService").metaboard - --- Imports -local Persistence = require(metaboard.Persistence) - --- Helper functions -local chunker = require(replay.persistTools.chunker) -local waitForBudget = require(replay.persistTools.waitForBudget) -local safeSet = require(replay.persistTools.safeSet) -local dataSerialiser = require(script.Parent.Parent.dataSerialiser) - - -local function store(self, datastore: DataStore, key: string) - - local timelineData = table.create(#self.Timeline) - - for _, event in ipairs(self.Timeline) do - - local timestamp, remoteName, args = unpack(event) - - local serialisedArgs = table.create(#args) - - for _, arg in ipairs(args) do - - table.insert(serialisedArgs, dataSerialiser.Serialise(arg)) - end - - table.insert(timelineData, {timestamp, remoteName, serialisedArgs}) - end - - local chunks = chunker.Chunk(timelineData) - - local surfaceSize = self.Board:SurfaceSize() - - local data = { - - _FormatVersion = "Board-v1", - - InitBoardKey = key.."/init", - InitBoardEmpty = next(self.InitFigures) == nil and self.InitNextFigureZIndex == 0, - - BoardSurfaceCFrame = dataSerialiser.Serialise(self.Origin:Inverse() * self.Board:SurfaceCFrame()), - BoardSurfaceSize = dataSerialiser.Serialise(surfaceSize), - - TimelineChunkCount = #chunks, - TimelineFirstChunk = chunks[1], - } - - local allSuccess = true - - waitForBudget(Enum.DataStoreRequestType.SetIncrementAsync) - allSuccess = safeSet(datastore, key, data) and allSuccess - - for i=2, #chunks do - - waitForBudget(Enum.DataStoreRequestType.SetIncrementAsync) - allSuccess = safeSet(datastore, key..":"..i, chunks[i]) and allSuccess - end - - -- Don't bother wasting a key on an empty board - - if not data.InitBoardEmpty then - - -- TODO: HACK - local fakeBoard = { - - CommitAllDrawingTasks = function(_) - - return self.InitFigures - end, - AspectRatio = function(_) - - return self.Board:AspectRatio() - end, - NextFigureZIndex = self.InitNextFigureZIndex, - ClearCount = 0, - } - - allSuccess = Persistence.StoreWhenBudget(datastore, data.InitBoardKey, fakeBoard) and allSuccess - end - - - return allSuccess -end - -local function restore(dataStore: DataStore, key: string, board) - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local data = dataStore:GetAsync(key) - - assert(data._FormatVersion == "Board-v1", "Format version "..tostring(data._FormatVersion).." unrecognised") - - local timelineChunks = {} - - if data.TimelineFirstChunk then - - table.insert(timelineChunks, data.TimelineFirstChunk) - end - - for i=2, data.TimelineChunkCount do - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local chunk = dataStore:GetAsync(key..":"..i) - - table.insert(timelineChunks, chunk) - end - - local initFigures, initNextFigureZIndex do - - if data.InitBoardEmpty then - - initFigures = {} - initNextFigureZIndex = 0 - - else - - local success, result = Persistence.Restore(dataStore, data.InitBoardKey, board) - - if not success then - - error("[Replay] Restore failed.\n"..result) - end - - initFigures = result.Figures - initNextFigureZIndex = result.NextFigureZIndex - end - end - - local timelineData = chunker.Gather(timelineChunks) - - local timeline = table.create(#timelineData) - - for _, eventData in ipairs(timelineData) do - - local timestamp, remoteName, serialisedArgs = unpack(eventData) - - local args = table.create(#serialisedArgs) - - for _, serialisedArg in ipairs(serialisedArgs) do - - table.insert(args, dataSerialiser.Deserialise(serialisedArg)) - end - - table.insert(timeline, {timestamp, remoteName, args}) - end - - return { - InitFigures = initFigures, - InitNextFigureZIndex = initNextFigureZIndex, - Timeline = timeline, - } -end - -return { - - Store = store, - Restore = restore, -} \ No newline at end of file diff --git a/src/BoardRecorder/remoteTokens.lua b/src/BoardRecorder/remoteTokens.lua new file mode 100644 index 0000000..c87c164 --- /dev/null +++ b/src/BoardRecorder/remoteTokens.lua @@ -0,0 +1,9 @@ +return { + + InitDrawingTask = 1, + UpdateDrawingTask = 2, + FinishDrawingTask = 3, + Undo = 4, + Redo = 5, + Clear = 6, +} \ No newline at end of file diff --git a/src/BoardSerialiser/FigureSerialiser.lua b/src/BoardSerialiser/FigureSerialiser.lua new file mode 100644 index 0000000..05ccab6 --- /dev/null +++ b/src/BoardSerialiser/FigureSerialiser.lua @@ -0,0 +1,146 @@ +-- Figure Types + +export type AnyFigure = Line | Curve + +export type Line = { + Type: "Line", + P0: Vector2, + P1: Vector2, + Width: number, + Color: Color3, + ZIndex: number, +} + +export type Curve = { + Type: "Curve", + Points: {Vector2}, + Width: number, + Color: number, + ZIndex: number, +} + +-- Figure Masks + +export type AnyMask = LineMask| CurveMask + +export type LineMask = boolean +export type CurveMask = {boolean} + +local function serialiseVector2(vector: Vector2) + return { X = vector.X, Y = vector.Y } +end + +local function deserialiseVector2(vData: { X: number, Y: number }) + return Vector2.new(vData.X, vData.Y) +end + +local function serialiseColor3(color: Color3) + return { R = color.R, G = color.G, B = color.B } +end + +local function deserialiseColor3(cData: { R: number, G: number, B: number }) + return Color3.new(cData.R, cData.G, cData.B) +end + +local serialisers + +serialisers = { + + Line = function(line: Line) + return { + Type = "Line", + P0 = serialiseVector2(line.P0), + P1 = serialiseVector2(line.P1), + Width = line.Width, + Color = serialiseColor3(line.Color), + ZIndex = line.ZIndex, + Mask = line.Mask, + } + end, + + Curve = function(curve: Curve) + local serialisedPoints = table.create(#curve.Points) + + for i, point in ipairs(curve.Points) do + serialisedPoints[i] = serialiseVector2(point) + end + + return { + Type = "Curve", + Points = serialisedPoints, + Width = curve.Width, + Color = serialiseColor3(curve.Color), + ZIndex = curve.ZIndex, + Mask = curve.Mask, + } + end, + +} + +local deserialisers = { + + Curve = function(curveData) + + local deserialisedPoints = table.create(#curveData.Points) + + for i, pointData in ipairs(curveData.Points) do + deserialisedPoints[i] = deserialiseVector2(pointData) + end + + return { + Type = "Curve", + Points = deserialisedPoints, + Width = curveData.Width, + Color = deserialiseColor3(curveData.Color), + ZIndex = curveData.ZIndex, + Mask = curveData.Mask, + } + end, + + Line = function(lineData) + return { + Type = "Line", + P0 = deserialiseVector2(lineData.P0), + P1 = deserialiseVector2(lineData.P1), + Width = lineData.Width, + Color = deserialiseColor3(lineData.Color), + ZIndex = lineData.ZIndex, + Mask = lineData.Mask, + } + end, + +} + +return { + + Serialise = function(figure: AnyFigure) + return serialisers[figure.Type](figure) + end, + + Deserialise = function(figureData) + return deserialisers[figureData.Type](figureData) + end, + + FullyMasked = function(figure) + if figure.Type == "Curve" then + + if figure.Mask == nil then + return false + end + + for i=1, #figure.Points-1 do + if figure.Mask[tostring(i)] == nil then + return false + end + end + + return true + + else + + return figure.Mask == true + + end + end, + +} \ No newline at end of file diff --git a/src/BoardSerialiser/init.lua b/src/BoardSerialiser/init.lua new file mode 100644 index 0000000..b474fe1 --- /dev/null +++ b/src/BoardSerialiser/init.lua @@ -0,0 +1,49 @@ +local FigureSerialiser = require(script.FigureSerialiser) + +local function serialise(figures, nextFigureZIndex, surfaceCFrame, surfaceSize) + + local entries = {} + + for figureId, figure in pairs(figures) do + + if FigureSerialiser.FullyMasked(figure) then + + continue + end + + local serialisedFigure = FigureSerialiser.Serialise(figure) + + table.insert(entries, { figureId, serialisedFigure }) + end + + return { + + FigureEntries = entries, + NextFigureZIndex = nextFigureZIndex, + SurfaceCFrame = {surfaceCFrame:GetComponents()}, + SurfaceSize = {surfaceSize.X, surfaceSize.Y}, + } +end + +local function deserialise(data) + + local figures = {} + + for _, entry in ipairs(data.FigureEntries) do + + local figureId, serialisedFigure = unpack(entry) + + local figure = FigureSerialiser.Deserialise(serialisedFigure) + -- TODO: add to erase grid? + + figures[figureId] = figure + end + + return figures, data.NextFigureZIndex, CFrame.new(unpack(data.SurfaceCFrame)), Vector2.new(unpack(data.SurfaceSize)) +end + +return { + + Serialise = serialise, + Deserialise = deserialise, +} \ No newline at end of file diff --git a/src/BoardTalkRecorder.lua b/src/BoardTalkRecorder.lua deleted file mode 100644 index 550efa5..0000000 --- a/src/BoardTalkRecorder.lua +++ /dev/null @@ -1,196 +0,0 @@ --- Services -local replay = script.Parent -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerScriptService = game:GetService("ServerScriptService") -local metaboard = ServerScriptService.metaboard - ---Imports -local t = require(replay.Packages.t) -local GoodSignal = require(replay.Packages.GoodSignal) -local BoardRecorder = require(replay.BoardRecorder) -local CharacterRecorder = require(replay.CharacterRecorder) -local VRCharacterRecorder = require(replay.VRCharacterRecorder) -local EventRecorder = require(replay.EventRecorder) -local NexusVRCharacterModel = require(ReplicatedStorage:WaitForChild("NexusVRCharacterModel")) -local CharacterService = NexusVRCharacterModel:GetInstance("State.CharacterService") - --- Helper functions -local persist = require(replay.persist) - -local BoardTalkRecorder = {} -BoardTalkRecorder.__index = BoardTalkRecorder - -local check = t.strictInterface({ - - Origin = t.CFrame, - Boards = t.table, - Players = t.array(t.instanceOf("Player")), - Signals = t.values(t.union(t.typeof("RBXScriptSignal"), t.interface({ Connect = t.callback }))), - -}) - ---[[ - NOTE: The keys of the boards table are used as identifiers for each board - and are used in the datastore keys for each board (so keep them short) - The boards table can be a dictionary or an array (which will result in numeric keys). - - If it's an array. Ensure to keep the order consistent (don't use :GetChildren() anywhere). ---]] -function BoardTalkRecorder.new(args) - - assert(check(args)) - - local self = setmetatable(args, BoardTalkRecorder) - - self.BoardRecorders = {} - - for boardId, board in self.Boards do - - self.BoardRecorders[boardId] = BoardRecorder.new({ - - Board = board, - Origin = self.Origin - }) - end - - self.PlayerRecordData = {} - - for i, player in ipairs(self.Players) do - - local recordData = {} - - local isVR = CharacterService.Characters[player] ~= nil - - recordData.IsVR = isVR - - recordData.Character = (isVR and VRCharacterRecorder or CharacterRecorder).new({ - - Player = player, - CharacterId = tostring(player.UserId), - Origin = self.Origin, - }) - - if isVR then - - local character = player.Character or player.CharacterAdded:Wait() - - local chalk = character:FindFirstChild("MetaChalk") - or player.Backpack:FindFirstChild("MetaChalk") - or error("[Replay] "..player.DisplayName.." has no chalk") - - recordData.Chalk = EventRecorder.new({ - - Signal = chalk.AncestryChanged, - ProcessArgs = function(...) - - return chalk.Parent == player.Character - end, - }) - - recordData.StartWithChalk = chalk.Parent == character, - end - - self.PlayerRecordData[tostring(player.UserId)] = recordData - end - - return self -end - -function BoardTalkRecorder:__allRecorders() - - local recorders = {} - - for _, boardRecorder in self.BoardRecorders do - - table.insert(recorders, boardRecorder) - end - - for i, recordData in ipairs(self.PlayerRecordData) do - - table.insert(recorders, recordData.Character) - if recordData.Chalk then - table.insert(recorders, recordData.Chalk) - end - end - - return recorders -end - -function BoardTalkRecorder:Start() - - -- Globally agreed start time across recorders - self.StartTime = os.clock() - - for _, recorder in self:__allRecorders() do - - recorder:Start(self.StartTime) - end -end - -function BoardTalkRecorder:Stop() - - for _, recorder in self:__allRecorders() do - - recorder:Stop() - end -end - -function BoardTalkRecorder:CreateReplays() - - local replays = {} - - for _, boardRecorder in self.BoardRecorders do - - table.insert(replays, boardRecorder:CreateReplay()) - end - - for _, player in ipairs(self.Players) do - - local character do - - player.Character or player.CharacterAdded:Wait() - character = player.Character:Clone() - end - - local heldChalk = character:FindFirstChild("MetaChalk") - if heldChalk then - - heldChalk:Destroy() - end - - local recordData = self.PlayerRecordData[tostring(player.UserId)] - - table.insert(replays, recordData.Character:CreateReplay({ - - Character = character, - })) - - if recordData.Chalk then - - local chalk = ServerScriptService.ManageVRChalk.MetaChalk:Clone() - local chalkCallback = function(equipped) - - chalk.Parent = equipped and character or nil - end - - -- initialise chalk - chalkCallback(chalk.StartWithChalk) - - table.insert(replays, recordData.Chalk:CreateReplay({ - - Callback = chalkCallback, - })) - end - end - - return replays -end - -function BoardTalkRecorder:Store(datastore: DataStore, replayId: number, replayName: string, force: boolean, retryOnFail: boolean) - - return persist.Store(self, datastore, replayId, replayName, force, retryOnFail) -end - -function BoardTalkRecorder.Restore() - -return BoardTalkRecorder \ No newline at end of file diff --git a/src/CharacterRecorder/CharacterReplay.lua b/src/CharacterRecorder/CharacterReplay.lua deleted file mode 100644 index ca10b79..0000000 --- a/src/CharacterRecorder/CharacterReplay.lua +++ /dev/null @@ -1,193 +0,0 @@ --- Service -local TweenService = game:GetService("TweenService") -local replay = script.Parent.Parent - --- Imports -local t = require(replay.Packages.t) - --- Helper functions -local persist = require(script.Parent.persist) -local config = require(script.Parent.config) - -local CharacterReplay = {} -CharacterReplay.__index = CharacterReplay - -local checkCharacter = t.instanceOf("Model", { - - ["Humanoid"] = t.instanceOf("Humanoid"), - - ["HumanoidRootPart"] = t.instanceIsA("BasePart"), - ["Head"] = t.instanceIsA("BasePart"), - ["RightLowerArm"] = t.instanceIsA("BasePart"), - ["RightUpperArm"] = t.instanceIsA("BasePart"), - ["RightUpperLeg"] = t.instanceIsA("BasePart"), - ["RightLowerLeg"] = t.instanceIsA("BasePart"), - ["RightFoot"] = t.instanceIsA("BasePart"), - ["LeftUpperLeg"] = t.instanceIsA("BasePart"), - ["LeftLowerLeg"] = t.instanceIsA("BasePart"), - ["LeftFoot"] = t.instanceIsA("BasePart"), - ["UpperTorso"] = t.instanceIsA("BasePart"), - ["LowerTorso"] = t.instanceIsA("BasePart"), - ["LeftUpperArm"] = t.instanceIsA("BasePart"), - ["LeftLowerArm"] = t.instanceIsA("BasePart"), - ["LeftHand"] = t.instanceIsA("BasePart"), - ["RightHand"] = t.instanceIsA("BasePart"), -}) - -local check = t.strictInterface({ - - Timeline = t.table, - Origin = t.CFrame, - SoundTimeline = t.optional(t.table), - Character = checkCharacter, -}) - -function CharacterReplay.new(args) - - assert(check(args)) - - return setmetatable(args, CharacterReplay) -end - -local tweenInfo = TweenInfo.new(1/config.FPS, Enum.EasingStyle.Linear) - -local function update(origin: CFrame, character: Model, charCFrames, instantly: boolean?) - - if instantly then - - for i, partName in ipairs(config.PartOrder) do - - character[partName].CFrame = origin * charCFrames[i] - end - - return - end - - for i, partName in ipairs(config.PartOrder) do - - TweenService:Create(character[partName], tweenInfo, { - CFrame = origin * charCFrames[i] - }):Play() - end -end - -function CharacterReplay:Init() - - for _, child in ipairs(self.Character:GetChildren()) do - - if child:IsA("BasePart") then - - child.Anchored = true - end - end - - self.Character.Parent = workspace - - if #self.Timeline >= 1 then - - update(self.Origin, self.Character, self.Timeline[1][2], true) - end - - -- Sound - - local soundQueue = {} - - for i, soundData in ipairs(self.SoundTimeline or {}) do - - local timestamp, sound = unpack(soundData) - - table.insert(soundQueue, {timestamp, sound:Clone()}) - end - - table.sort(soundQueue, function(a, b) - - return a[1] < b[1] - end) - - self.SoundQueue = soundQueue - - self.SoundQueueIndex = 1 - self.TimelineIndex = 1 - self.Finished = false -end - -function CharacterReplay:PlayUpTo(playhead: number) - - -- Character - - while self.TimelineIndex <= #self.Timeline do - - local event = self.Timeline[self.TimelineIndex] - - if event[1] <= playhead then - - local timeStamp, charCFrames = unpack(event) - - update(self.Origin, self.Character, charCFrames, false) - - self.TimelineIndex += 1 - continue - end - - break - end - - -- Sound - - while self.SoundQueueIndex <= #self.SoundQueue do - - if self.SoundQueue[self.SoundQueueIndex][1] <= playhead then - - local timestamp, sound = unpack(self.SoundQueue[self.SoundQueueIndex]) - - local delta = timestamp - playhead - - sound.TimePosition = sound.TimePosition + delta - sound:Resume() - - self.SoundQueueIndex += 1 - - continue - end - - break - end - - if self.TimelineIndex > #self.Timeline and self.SoundQueueIndex > #self.SoundQueue then - - self.Finished = true - end -end - -function CharacterReplay.Restore(dataStore: DataStore, key: string, replayArgs) - - local restoredArgs = persist.Restore(dataStore, key) - - local characterId = restoredArgs.CharacterId - local character = replayArgs.CharactersById[tostring(characterId)] - - if not character then - - error("No Character Model given with ID: "..tostring(characterId)) - end - - assert(checkCharacter(character)) - - local archivable = character.Archivable - character.Archivable = true - local clone = character:Clone() - character.Archivable = archivable - - clone.Name = "replay-"..characterId - - return CharacterReplay.new({ - - Character = clone, - Origin = replayArgs.Origin, - - Timeline = restoredArgs.Timeline, - SoundTimeline = restoredArgs.SoundTimeline, - }) -end - -return CharacterReplay diff --git a/src/CharacterRecorder/config.lua b/src/CharacterRecorder/config.lua deleted file mode 100644 index b27a881..0000000 --- a/src/CharacterRecorder/config.lua +++ /dev/null @@ -1,27 +0,0 @@ -return { - - -- This order is completely arbitrary. We encode in this order for the sake - -- of compression (not necessary TODO) - PartOrder = { - - "HumanoidRootPart", - "Head", - "RightLowerArm", - "RightUpperArm", - "RightUpperLeg", - "RightLowerLeg", - "RightFoot", - "LeftUpperLeg", - "LeftLowerLeg", - "LeftFoot", - "UpperTorso", - "LowerTorso", - "LeftUpperArm", - "LeftLowerArm", - "LeftHand", - "RightHand", - }, - - FPS = 20, - -} diff --git a/src/CharacterRecorder/init.lua b/src/CharacterRecorder/init.lua deleted file mode 100644 index f8da88e..0000000 --- a/src/CharacterRecorder/init.lua +++ /dev/null @@ -1,86 +0,0 @@ --- Services -local RunService = game:GetService("RunService") -local replay = script.Parent - --- Imports -local t = require(replay.Packages.t) -local CharacterReplay = require(script.CharacterReplay) -local persist = require(script.persist) -local config = require(script.config) - -local CharacterRecorder = {} -CharacterRecorder.__index = CharacterRecorder - -local check = t.strictInterface({ - - Origin = t.CFrame, - Player = t.instanceOf("Player"), - CharacterId = t.union(t.string, t.number), -}) - -function CharacterRecorder.new(args) - - assert(check(args)) - - return setmetatable(args, CharacterRecorder) -end - -local function capture(originInverse: CFrame, character: Model) - - if not character then - - return nil - end - - local charCFrames = table.create(#config.PartOrder) - - for i, partName in ipairs(config.PartOrder) do - - charCFrames[i] = originInverse * character[partName].CFrame - end - - return charCFrames -end - - -function CharacterRecorder:Start(startTime) - - local originInverse = self.Origin:Inverse() - - -- Start time is passed as argument for consistency between recorders - self.StartTime = startTime - self.Timeline = {{0, capture(originInverse, self.Player.Character)}} - - self.CharacterConnection = RunService.Heartbeat:Connect(function() - local now = os.clock() - self.StartTime - local lastFrameTime = self.Timeline[#self.Timeline][1] - - if lastFrameTime + 1/config.FPS <= now then - - table.insert(self.Timeline, {now, capture(originInverse, self.Player.Character)}) - end - end) -end - -function CharacterRecorder:Stop() - - self.CharacterConnection:Disconnect() -end - -function CharacterRecorder:CreateReplay(replayArgs) - - return CharacterReplay.new({ - - Character = replayArgs.Character, - - Timeline = self.Timeline, - Origin = self.Origin, - }) -end - -function CharacterRecorder:Store(dataStore: DataStore, key: string) - - return persist.Store(self, dataStore, key) -end - -return CharacterRecorder diff --git a/src/CharacterRecorder/persist.lua b/src/CharacterRecorder/persist.lua deleted file mode 100644 index 8119d17..0000000 --- a/src/CharacterRecorder/persist.lua +++ /dev/null @@ -1,94 +0,0 @@ --- Services -local replay = script.Parent.Parent - --- Helper functions -local serialiser = require(script.Parent.serialiser) -local chunker = require(replay.persistTools.chunker) -local waitForBudget = require(replay.persistTools.waitForBudget) -local safeSet = require(replay.persistTools.safeSet) - -local function store(self, dataStore, key) - - local timelineData = serialiser.Serialise(self.Timeline) - local timelineChunks = chunker.Chunk(timelineData) - - local data = { - - _FormatVersion = "Character-v1", - - CharacterId = self.CharacterId, - - TimelineKey = key.."/timeline", -- Format that remembers itself! - } - - local timelineData = { - - TimelineChunkCount = #timelineChunks, - TimelineFirstChunk = timelineChunks[1], - } - - local allSuccess = true - - waitForBudget(Enum.DataStoreRequestType.SetIncrementAsync) - allSuccess = safeSet(dataStore, key, data) and allSuccess - - waitForBudget(Enum.DataStoreRequestType.SetIncrementAsync) - allSuccess = safeSet(dataStore, data.TimelineKey, timelineData) and allSuccess - - for i=2, #timelineChunks do - - waitForBudget(Enum.DataStoreRequestType.SetIncrementAsync) - allSuccess = safeSet(dataStore, data.TimelineKey..":"..i, timelineChunks[i]) and allSuccess - end - - return allSuccess -end - -local function restore(dataStore: DataStore, key: string) - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local data = dataStore:GetAsync(key) - - assert(data._FormatVersion == "Character-v1", "Format version "..tostring(data._FormatVersion).." unrecognised") - - -- Check everything is present - assert(data.CharacterId) - assert(data.TimelineKey) - - -- Timeline - - local timelineChunks do - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local timelineData = dataStore:GetAsync(data.TimelineKey) - - timelineChunks = table.create(timelineData.TimelineChunkCount) - - if timelineData.TimelineFirstChunk then - - table.insert(timelineChunks, timelineData.TimelineFirstChunk) - end - - for i=2, timelineData.TimelineChunkCount do - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local chunk = dataStore:GetAsync(data.TimelineKey..":"..i) - - table.insert(timelineChunks, chunk) - end - end - - local timelineData = chunker.Gather(timelineChunks) - local timeline = serialiser.Deserialise(timelineData) - - return { - CharacterId = data.CharacterId, - Timeline = timeline, - } -end - -return { - - Store = store, - Restore = restore, -} \ No newline at end of file diff --git a/src/CharacterRecorder/serialiser.lua b/src/CharacterRecorder/serialiser.lua deleted file mode 100644 index 276daaf..0000000 --- a/src/CharacterRecorder/serialiser.lua +++ /dev/null @@ -1,58 +0,0 @@ -local function serialiseCFrame(cframe: CFrame) - - return table.pack(cframe:GetComponents()) -end - -local function deserialiseCFrame(data) - - return CFrame.new(table.unpack(data)) -end - -local function serialise(timeline) - - local timelineData = table.create(#timeline) - - for _, event in ipairs(timeline) do - - local timestamp, charCframes = unpack(event) - - local serialisedCharCFrames = {} - - for _, cframe in ipairs(charCframes) do - - table.insert(serialisedCharCFrames, serialiseCFrame(cframe)) - end - - table.insert(timelineData, {timestamp, serialisedCharCFrames}) - end - - return timelineData -end - -local function deserialise(timelineData) - - local timeline = table.create(#timelineData) - - for _, eventData in ipairs(timelineData) do - - local timestamp, charCframeData = unpack(eventData) - - local charCFrames = {} - - for _, cframeData in ipairs(charCframeData) do - - table.insert(charCFrames, deserialiseCFrame(cframeData)) - end - - table.insert(timeline, {timestamp, charCFrames}) - end - - return timeline -end - - -return { - - Serialise = serialise, - Deserialise = deserialise, -} diff --git a/src/EventRecorder/EventReplay.lua b/src/EventRecorder/EventReplay.lua index 8c6e620..f08f083 100644 --- a/src/EventRecorder/EventReplay.lua +++ b/src/EventRecorder/EventReplay.lua @@ -1,26 +1,27 @@ -- Services -local replay = script.Parent.Parent +local Replay = script.Parent.Parent -- Imports -local t = require(replay.Packages.t) - --- Helper functions -local persist = require(script.Parent.persist) +local t = require(Replay.Parent.t) local EventReplay = {} EventReplay.__index = EventReplay -local check = t.strictInterface({ +local checkRecord = t.strictInterface({ - Callback = t.callback, Timeline = t.table, }) -function EventReplay.new(args) +function EventReplay.new(record, callback: () -> ()) - assert(check(args)) + assert(checkRecord(record)) + assert(t.callback(callback)) - return setmetatable(args, EventReplay) + return setmetatable({ + + Record = record, + Callback = callback, + }, EventReplay) end function EventReplay:Init() @@ -31,15 +32,13 @@ end function EventReplay:PlayUpTo(playhead: number) - while self.TimelineIndex <= #self.Timeline do + while self.TimelineIndex <= #self.Record.Timeline do - local event = self.Timeline[self.TimelineIndex] + local event = self.Record.Timeline[self.TimelineIndex] if event[1] <= playhead then - local timeStamp, args = unpack(event) - - self.Callback(unpack(args)) + self.Callback(unpack(event, 2)) self.TimelineIndex += 1 continue @@ -48,22 +47,10 @@ function EventReplay:PlayUpTo(playhead: number) break end - if self.TimelineIndex > #self.Timeline then + if self.TimelineIndex > #self.Record.Timeline then self.Finished = true end end -function EventReplay.Restore(dataStore: DataStore, key: string, replayArgs) - - local restoredArgs = persist.Restore(dataStore, key) - - return EventReplay.new({ - - Timeline = restoredArgs.Timeline, - - Callback = replayArgs.Callback, - }) -end - return EventReplay diff --git a/src/EventRecorder/init.lua b/src/EventRecorder/init.lua index 07083c0..6cb1427 100644 --- a/src/EventRecorder/init.lua +++ b/src/EventRecorder/init.lua @@ -1,11 +1,8 @@ -- Services -local replay = script.Parent +local Replay = script.Parent -- Imports -local t = require(replay.Packages.t) -local EventReplay = require(script.EventReplay) -local persist = require(script.persist) -local dataSerialiser = require(replay.dataSerialiser) +local t = require(Replay.Parent.t) local EventRecorder = {} EventRecorder.__index = EventRecorder @@ -28,6 +25,7 @@ function EventRecorder:Start(startTime) -- Start time is passed as argument for consistency between recorders self.StartTime = startTime self.Timeline = {} + self._sizeAcc = 0 self.Connection = self.Signal:Connect(function(...) @@ -37,7 +35,7 @@ function EventRecorder:Start(startTime) if self.ProcessArgs then - processedArgs = table.pack(self.ProcessArgs(...)) + processedArgs = {self.ProcessArgs(...)} else @@ -46,15 +44,35 @@ function EventRecorder:Start(startTime) end end - for i=1, #processedArgs do + for i, arg in ipairs(processedArgs) do - if not dataSerialiser.CanSerialise(processedArgs[i]) then + if i~= #processedArgs then - warn("[Replay] EventRecorder will not be able to serialise args["..i.."] = "..tostring(processedArgs[i])) + -- Comma + self._sizeAcc += 1 + end + + local argType = typeof(arg) + + if argType == "number" then + + self._sizeAcc += #tostring(arg) + elseif argType == "boolean" then + + self._sizeAcc += arg and 4 or 5 -- #"true" == 4, #"false" = 5 + elseif argType == "string" then + + self._sizeAcc += #arg + 2 -- inc quotes + elseif argType == "nil" then + + self._sizeAcc += 4 -- #"null" = 0 + else + + error(("[Replay] EventRecorder: Processed arg[%d] = %s is not a number | boolean | string | nil"):format(i, tostring(arg))) end end - table.insert(self.Timeline, {now, processedArgs}) + table.insert(self.Timeline, {now, unpack(processedArgs)}) end) end @@ -66,18 +84,24 @@ function EventRecorder:Stop() end end -function EventRecorder:CreateReplay(replayArgs) +function EventRecorder:FlushTimelineToRecord() - return EventReplay.new({ - - Callback = replayArgs.Callback, + local record = { + Timeline = self.Timeline, - }) + } + + self.Timeline = {} + self._sizeAcc = 0 + + return record end -function EventRecorder:Store(dataStore: DataStore, key: string) +local SCAFFOLD = #[[{"Timeline":{}}]] + +function EventRecorder:GetRecordSizeEstimate() - return persist.Store(self, dataStore, key) + return SCAFFOLD + self._sizeAcc end diff --git a/src/EventRecorder/persist.lua b/src/EventRecorder/persist.lua deleted file mode 100644 index f755322..0000000 --- a/src/EventRecorder/persist.lua +++ /dev/null @@ -1,103 +0,0 @@ --- Services -local replay = script.Parent.Parent - --- Imports - --- Helper functions -local chunker = require(replay.persistTools.chunker) -local waitForBudget = require(replay.persistTools.waitForBudget) -local safeSet = require(replay.persistTools.safeSet) -local dataSerialiser = require(replay.dataSerialiser) - -local function store(self, datastore: DataStore, key: string) - - local timelineData = table.create(#self.Timeline) - - for _, event in ipairs(self.Timeline) do - - local timestamp, args = unpack(event) - local serialisedArgs = {} - - for _, arg in ipairs(args) do - - table.insert(serialisedArgs, dataSerialiser.Serialise(arg)) - end - - table.insert(timelineData, {timestamp, serialisedArgs}) - end - - local chunks = chunker.Chunk(timelineData) - - local data = { - - _FormatVersion = "Event-v1", - - TimelineChunkCount = #chunks, - TimelineFirstChunk = chunks[1], - } - - local allSuccess = true - - waitForBudget(Enum.DataStoreRequestType.SetIncrementAsync) - allSuccess = safeSet(datastore, key, data) and allSuccess - - for i=2, #chunks do - - waitForBudget(Enum.DataStoreRequestType.SetIncrementAsync) - allSuccess = safeSet(datastore, key..":"..i, chunks[i]) and allSuccess - end - - return allSuccess -end - -local function restore(dataStore: DataStore, key: string) - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local data = dataStore:GetAsync(key) - - assert(data._FormatVersion == "Event-v1", "Format version "..tostring(data._FormatVersion).." unrecognised") - - local timelineChunks = {} - - if data.TimelineFirstChunk then - - table.insert(timelineChunks, data.TimelineFirstChunk) - end - - for i=2, data.TimelineChunkCount do - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local chunk = dataStore:GetAsync(key..":"..i) - - table.insert(timelineChunks, chunk) - end - - local timelineData = chunker.Gather(timelineChunks) - - local timeline = table.create(#timelineData) - - for _, eventData in ipairs(timelineData) do - - local timestamp, serialisedArgs = unpack(eventData) - - local args = {} - - for _, serialisedArg in ipairs(serialisedArgs) do - - table.insert(args, dataSerialiser.Deserialise(serialisedArg)) - end - - table.insert(timeline, {timestamp, args}) - end - - return { - - Timeline = timeline, - } -end - -return { - - Store = store, - Restore = restore, -} \ No newline at end of file diff --git a/src/HumanoidDescriptionSerialiser.lua b/src/HumanoidDescriptionSerialiser.lua new file mode 100644 index 0000000..452c190 --- /dev/null +++ b/src/HumanoidDescriptionSerialiser.lua @@ -0,0 +1,130 @@ +-- From from AvatarEditor.AvatarEditorSerialize by https://github.com/phoebethewitch + +local AvatarEditorSerialize = {} + +local propertiesToSerialize = { + "BackAccessory", + "BodyTypeScale", + "ClimbAnimation", + "DepthScale", + "Face", + "FaceAccessory", + "FallAnimation", + "FrontAccessory", + "GraphicTShirt", + "HairAccessory", + "HatAccessory", + "Head", + "HeadColor", + "HeadScale", + "HeightScale", + "IdleAnimation", + "JumpAnimation", + "LeftArm", + "LeftArmColor", + "LeftLeg", + "LeftLegColor", + "NeckAccessory", + "Pants", + "ProportionScale", + "RightArm", + "RightArmColor", + "RightLeg", + "RightLegColor", + "RunAnimation", + "Shirt", + "ShouldersAccessory", + "SwimAnimation", + "Torso", + "TorsoColor", + "WaistAccessory", + "WalkAnimation", + "WidthScale", +} + +local AcessoryTypeToEnum = {} + +for _, enum in ipairs(Enum.AccessoryType:GetEnumItems()) do + AcessoryTypeToEnum[enum.Name] = enum +end + +function AvatarEditorSerialize.Serialize(description: HumanoidDescription) + local serialized = { + properties = {}, + emotes = {}, + equippedEmotes = {}, + layeredClothing = {}, + } + + -- properties + for _, property in ipairs(propertiesToSerialize) do + local value = description[property] + if typeof(value) == "Color3" then + serialized.properties[property] = { + "Color3", + value.R, + value.G, + value.B + } + else + serialized.properties[property] = description[property] + end + end + + -- emotes + serialized.emotes = description:GetEmotes() + + -- equipped emotes + serialized.equippedEmotes = description:GetEquippedEmotes() + + -- layered clothing + serialized.layeredClothing = description:GetAccessories(false) + + for i, accessory in ipairs(serialized.layeredClothing) do + serialized.layeredClothing[i].AccessoryType = accessory.AccessoryType.Name + end + + return serialized +end + +function AvatarEditorSerialize.Deserialize(serialized: { properties: {}, emotes: {}, equippedEmotes: {}, layeredClothing: {} }) + local description = Instance.new("HumanoidDescription") + + for _, property in ipairs(propertiesToSerialize) do + local value = serialized.properties[property] + if value then + if typeof(value) == "table" then + if value[1] == "Color3" then + description[property] = Color3.new(value[2], value[3], value[4]) + end + else + description[property] = value + end + end + end + + description:SetEmotes(serialized.emotes) + + description:SetEquippedEmotes(serialized.equippedEmotes) + + local layeredClothing = {} + if serialized.layeredClothing then + layeredClothing = table.clone(serialized.layeredClothing) + + for i, accessory in ipairs(layeredClothing) do + accessory = table.clone(accessory) + layeredClothing[i] = accessory + accessory.AccessoryType = AcessoryTypeToEnum[accessory.AccessoryType] + end + end + + description:SetAccessories(layeredClothing, false) + + return description +end + +return { + + Serialise = AvatarEditorSerialize.Serialize, + Deserialise = AvatarEditorSerialize.Deserialize, +} diff --git a/src/Recorder.lua b/src/Recorder.lua deleted file mode 100644 index 617252e..0000000 --- a/src/Recorder.lua +++ /dev/null @@ -1,196 +0,0 @@ --- Services -local replay = script.Parent - ---Imports -local t = require(replay.Packages.t) -local BoardRecorder = require(replay.BoardRecorder) -local CharacterRecorder = require(replay.CharacterRecorder) -local VRCharacterRecorder = require(replay.VRCharacterRecorder) -local EventRecorder = require(replay.EventRecorder) - --- Helper functions -local persist = require(replay.persist) - -local Recorder = {} -Recorder.__index = Recorder - -local check = t.strictInterface({ - - Origin = t.CFrame, - Boards = t.table, - VRPlayers = t.array(t.instanceOf("Player")), - Players = t.array(t.instanceOf("Player")), - Signals = t.values(t.union(t.typeof("RBXScriptSignal"), t.interface({ Connect = t.callback }))), - -}) - ---[[ - NOTE: The keys of the boards table are used as identifiers for each board - and are used in the datastore keys for each board (so keep them short) - The boards table can be a dictionary or an array (which will result in numeric keys). - - If it's an array. Ensure to keep the order consistent (don't use :GetChildren() anywhere). ---]] -function Recorder.new(args) - - assert(check(args)) - - local self = setmetatable(args, Recorder) - - self.BoardRecorders = {} - self.VRCharacterRecorders = table.create(#self.VRPlayers) - self.CharacterRecorders = table.create(#self.Players) - self.EventRecorders = {} - - for boardId, board in self.Boards do - - self.BoardRecorders[boardId] = BoardRecorder.new({ - - Board = board, - Origin = self.Origin - }) - end - - for i, player in ipairs(self.VRPlayers) do - - table.insert(self.VRCharacterRecorders, VRCharacterRecorder.new({ - - Player = player, - CharacterId = player.UserId, - Origin = self.Origin - })) - end - - for i, player in ipairs(self.Players) do - - table.insert(self.CharacterRecorders, CharacterRecorder.new({ - - -- TODO: Make this actually take a player not a character - Player = player, - CharacterId = player.UserId, - Origin = self.Origin - })) - end - - for signalId, signal in self.Signals do - - self.EventRecorders[signalId] = EventRecorder.new({ - - Signal = signal - }) - end - - return self -end - -function Recorder:__allRecorders() - - local recorders = {} - - for _, boardRecorder in self.BoardRecorders do - - table.insert(recorders, boardRecorder) - end - - for _, vrCharacterRecorder in ipairs(self.VRCharacterRecorders) do - - table.insert(recorders, vrCharacterRecorder) - end - - for _, characterRecorder in ipairs(self.CharacterRecorders) do - - table.insert(recorders, characterRecorder) - end - - for _, eventRecorder in self.EventRecorders do - - table.insert(recorders, eventRecorder) - end - - return recorders -end - -function Recorder:Start() - - -- Globally agreed start time across recorders - self.StartTime = os.clock() - - for _, recorder in self:__allRecorders() do - - recorder:Start(self.StartTime) - end -end - -function Recorder:Stop() - - for _, recorder in self:__allRecorders() do - - recorder:Stop() - end -end - -function Recorder:CreateReplays(args) - - local eventCallbacks = args.EventCallbacks or {} - - local replays = {} - - for _, boardRecorder in self.BoardRecorders do - - table.insert(replays, boardRecorder:CreateReplay()) - end - - for i, vrCharacterRecorder in ipairs(self.VRCharacterRecorders) do - - local player = self.VRPlayers[i] - - local archivable = player.Character.Archivable - player.Character.Archivable = true - local clone = player.Character:Clone() - player.Character.Archivable = archivable - - table.insert(replays, vrCharacterRecorder:CreateReplay({ - - Character = clone, - })) - end - - for i, characterRecorder in ipairs(self.CharacterRecorders) do - - local player = self.Players[i] - - local archivable = player.Character.Archivable - player.Character.Archivable = true - local clone = player.Character:Clone() - player.Character.Archivable = archivable - - table.insert(replays, characterRecorder:CreateReplay({ - - Character = clone - })) - end - - for signalId, eventRecorder in self.EventRecorders do - - local callback = (eventCallbacks or {})[signalId] - - if not callback then - - error("[replay] No callback given for signalId "..signalId) - end - - table.insert(replays, eventRecorder:CreateReplay({ - - Callback = callback, - })) - end - - return replays -end - -function Recorder:Store(datastore: DataStore, replayId: number, replayName: string, force: boolean, retryOnFail: boolean) - - return persist.Store(self, datastore, replayId, replayName, force, retryOnFail) -end - -return Recorder \ No newline at end of file diff --git a/src/Replayer.lua b/src/Replayer.lua deleted file mode 100644 index 20d1170..0000000 --- a/src/Replayer.lua +++ /dev/null @@ -1,134 +0,0 @@ --- Services -local RunService = game:GetService("RunService") -local replay = script.Parent - --- Imports -local t = require(replay.Packages.t) - -local persist = require(script.Parent.persist) - -local Replayer = {} -Replayer.__index = Replayer - -local checkReplay = t.interface({ - - Init = t.optional(t.callback), - PlayUpTo = t.callback, - Stop = t.optional(t.callback), -}) - -local check = t.strictInterface({ - - Init = t.optional(t.callback), - Replays = t.array(checkReplay), -}) - - - -function Replayer.new(args) - - assert(check(replays)) - - return setmetatable({ - - Replays = replays - }, Replayer) -end - -function Replayer:Play() - - if self.Init then - - self.Init() - end - - self.Playhead = 0 - self.Paused = false - self.StartTime = os.clock() - - for _, replay in ipairs(self.Replays) do - - if replay.Init then - - replay:Init() - end - end - - -- The non-nil status of this field tells you whether the replay is active - self.PlayConnection = RunService.Heartbeat:Connect(function() - - if self.Paused then - return - end - - self.Playhead = os.clock() - self.StartTime - - local allFinished = true - - for _, replay in ipairs(self.Replays) do - - if not replay.Finished then - - replay:PlayUpTo(self.Playhead) - end - - allFinished = allFinished and replay.Finished - end - - if allFinished then - - self.PlayConnection:Disconnect() - self.PlayConnection = nil - end - - end) -end - -function Replayer:Pause() - - self.Paused = true -end - -function Replayer:Resume() - - self.Paused = false -end - -function Replayer:Stop() - - if self.PlayConnection then - - self.PlayConnection:Disconnect() - self.PlayConnection = nil - end - - for _, replay in self:__allReplays() do - - if replay.Stop then - - replay:Stop() - end - end -end - -function Replayer.Restore(dataStore, replayId: string, liveData) - - local origin: CFrame = liveData.Origin - local boards = liveData.Boards - local chalk: Tool = liveData.Chalk - local charactersById: {[string]: Model} = liveData.CharactersById - local eventCallbacks: {[string]: (any...) -> any} = liveData.EventCallbacks - - local replays = persist.Restore(dataStore, replayId, { - - Origin = origin, - Boards = boards, - Chalk = chalk, - CharactersById = charactersById, - EventCallbacks = eventCallbacks, - }) - - return Replayer.new(replays) -end - -return Replayer diff --git a/src/SoundReplay.lua b/src/SoundReplay.lua new file mode 100644 index 0000000..6f8e144 --- /dev/null +++ b/src/SoundReplay.lua @@ -0,0 +1,121 @@ +-- Services +local ContentProvider = game:GetService("ContentProvider") +-- local Replay = script.Parent + +-- Imports +-- local t = require(Replay.Parent.t) + +local SoundReplay = {} +SoundReplay.__index = SoundReplay + +function SoundReplay.new(record, replayArgs) + + local self = setmetatable({ + + Record = record, + SoundProps = replayArgs.SoundProps, + }, SoundReplay) + + self.Sounds = {} + + local finishTimestamp + + for _, event in ipairs(self.Record.Timeline) do + + local timestamp, assetId, startOffset = unpack(event) + + local sound = Instance.new("Sound") + sound.SoundId = assetId + + ContentProvider:PreloadAsync({sound}) + + if not sound.IsLoaded then + + -- TODO: This is a hack to make it actually preload. + sound.Parent = workspace + sound.Loaded:Wait() + + finishTimestamp = math.max(unpack({timestamp + sound.TimeLength - startOffset, finishTimestamp})) + + sound.Parent = nil + end + + for key, value in self.SoundProps do + + sound[key] = value + end + + + + table.insert(self.Sounds, sound) + end + + self.FinishTimestamp = finishTimestamp + + return self +end + +function SoundReplay:Destroy() + + for _, sound in ipairs(self.Sounds) do + + sound:Destroy() + end +end + +function SoundReplay:Init() + + self.TimelineIndex = 1 + self.Finished = false +end + +function SoundReplay:PlayUpTo(playhead: number) + + for i=self.TimelineIndex, #self.Record.Timeline do + + local timestamp, _assetId, startOffset = unpack(self.Record.Timeline[i]) + local sound = self.Sounds[i] + + local delta = playhead - timestamp + + if delta >= 0 then + + if delta + startOffset < sound.TimeLength and not sound.IsPlaying then + + sound.TimePosition = startOffset + delta + sound:Resume() + self.TimelineIndex = i + 1 + end + end + end + + -- Check finished + + if playhead >= self.FinishTimestamp then + + self.Finished = true + end +end + +function SoundReplay:Resume() + + self.TimelineIndex = 1 +end + +function SoundReplay:Pause() + + for _, sound in ipairs(self.Sounds) do + + sound:Pause() + end +end + +function SoundReplay:Stop() + + for _, sound in ipairs(self.Sounds) do + + sound:Pause() + end +end + +return SoundReplay \ No newline at end of file diff --git a/src/VRCharacterRecorder/VRCharacterReplay.lua b/src/VRCharacterRecorder/VRCharacterReplay.lua index 1f2dd66..69f4d48 100644 --- a/src/VRCharacterRecorder/VRCharacterReplay.lua +++ b/src/VRCharacterRecorder/VRCharacterReplay.lua @@ -1,16 +1,14 @@ -- Services local ReplicatedStorage = game:GetService("ReplicatedStorage") -local replay = script.Parent.Parent +local Replay = script.Parent.Parent -- Imports -local t = require(replay.Packages.t) +local t = require(Replay.Parent.t) local NexusVRCharacterModel = require(ReplicatedStorage:WaitForChild("NexusVRCharacterModel")) local Character = NexusVRCharacterModel:GetResource("Character") -local UpdateInputs = NexusVRCharacterModel:GetResource("UpdateInputs") -- Helper functions local updateAnchoredFromInputs = require(script.Parent.updateAnchoredFromInputs) -local persist = require(script.Parent.persist) local VRCharacterReplay = {} VRCharacterReplay.__index = VRCharacterReplay @@ -37,18 +35,26 @@ local checkCharacter = t.instanceOf("Model", { ["RightHand"] = t.instanceIsA("BasePart"), }) -local check = t.strictInterface({ +local checkReplayArgs = t.strictInterface({ - Timeline = t.table, Origin = t.CFrame, Character = checkCharacter, }) -function VRCharacterReplay.new(args) +local checkRecord = t.strictInterface({ Timeline = t.array(t.table) }) - assert(check(args)) +function VRCharacterReplay.new(record, replayArgs) - return setmetatable(args, VRCharacterReplay) + assert(checkRecord(record)) + assert(checkReplayArgs(replayArgs)) + + return setmetatable({ + + Record = record, + Origin = replayArgs.Origin, + Character = replayArgs.Character, + NexusCharacter = Character.new(replayArgs.Character) + }, VRCharacterReplay) end function VRCharacterReplay:Init() @@ -61,101 +67,27 @@ function VRCharacterReplay:Init() end end - self.Character.Parent = workspace - - self.NexusCharacter = Character.new(self.Character) - - if #self.Timeline >= 1 then - - local HeadControllerCFrame, LeftHandControllerCFrame, RightHandControllerCFrame = unpack(self.Timeline[1][2]) - - updateAnchoredFromInputs(self.NexusCharacter, self.Origin * HeadControllerCFrame, self.Origin * LeftHandControllerCFrame, self.Origin * RightHandControllerCFrame, true) - end - - -- Sound - - local soundQueue = {} - - for i, soundData in ipairs(self.SoundTimeline or {}) do - - local timestamp, sound = unpack(soundData) - - table.insert(soundQueue, {timestamp, sound:Clone()}) - end - - table.sort(soundQueue, function(a, b) - - return a[1] < b[1] - end) - - self.SoundQueue = soundQueue - -- Initial values self.TimelineIndex = 1 - self.ChalkTimelineIndex = 1 - self.SoundQueueIndex = 1 self.Finished = false end -local BLACKLIST = {"HumanoidRootPart", "OrbEar"} -local TRANSPARENCY_FACTOR = 1/5 - -local _originalTransparency = {} - -local function updateChalk(chalk, character, equipped) - - chalk.Parent = equipped and character or nil - - for _, desc in ipairs(character:GetDescendants()) do - - if desc:IsA("BasePart") and not table.find(BLACKLIST, desc.Name) and not desc:IsDescendantOf(chalk) then - - if not _originalTransparency[desc] then - _originalTransparency[desc] = desc.Transparency - end - - desc.Transparency = equipped and (1 - TRANSPARENCY_FACTOR * (1 - _originalTransparency[desc])) or _originalTransparency[desc] - end - end -end - function VRCharacterReplay:PlayUpTo(playhead: number) - -- Sound - - while self.SoundQueueIndex <= #self.SoundQueue do - - if self.SoundQueue[self.SoundQueueIndex][1] <= playhead then - - local timestamp, sound = unpack(self.SoundQueue[self.SoundQueueIndex]) - - local delta = timestamp - playhead - - sound.TimePosition = sound.TimePosition + delta - sound:Resume() - - self.SoundQueueIndex += 1 - - continue - end - - break - end + while self.TimelineIndex <= #self.Record.Timeline do - -- Character - - while self.TimelineIndex <= #self.Timeline do - - local event = self.Timeline[self.TimelineIndex] + local event = self.Record.Timeline[self.TimelineIndex] if event[1] <= playhead then - - local timeStamp, charCFrames = unpack(event) - - local HeadControllerCFrame, LeftHandControllerCFrame, RightHandControllerCFrame = unpack(charCFrames) - - updateAnchoredFromInputs(self.NexusCharacter, self.Origin * HeadControllerCFrame, self.Origin * LeftHandControllerCFrame, self.Origin * RightHandControllerCFrame) + + updateAnchoredFromInputs( + self.NexusCharacter, + self.Origin * CFrame.new(unpack(event, 2, 4)) * CFrame.fromEulerAnglesXYZ(unpack(event, 5, 7)), + self.Origin * CFrame.new(unpack(event, 8, 10)) * CFrame.fromEulerAnglesXYZ(unpack(event, 11, 13)), + self.Origin * CFrame.new(unpack(event, 14, 16)) * CFrame.fromEulerAnglesXYZ(unpack(event, 17, 19)), + false + ) self.TimelineIndex += 1 continue @@ -164,77 +96,12 @@ function VRCharacterReplay:PlayUpTo(playhead: number) break end - -- Chalk - - while self.ChalkTimelineIndex <= #self.ChalkTimeline do - - local event = self.ChalkTimeline[self.ChalkTimelineIndex] - - if event[1] <= playhead then - - updateChalk(self.Chalk, self.Character, event[2]) - - self.ChalkTimelineIndex += 1 - continue - end - - break - end - -- Check finished - if self.TimelineIndex > #self.Timeline - and self.ChalkTimelineIndex > #self.ChalkTimeline - and self.SoundQueueIndex > #self.SoundQueue - - then + if self.TimelineIndex > #self.Record.Timeline then self.Finished = true end end -function VRCharacterReplay:Stop() - - for _, soundData in ipairs(self.SoundQueue) do - - local _, sound = unpack(soundData) - - sound:Stop() - end -end - -function VRCharacterReplay.Restore(metadata, data, replayArgs) - - local restoredArgs = persist.Restore(dataStore, key) - - local characterId = restoredArgs.CharacterId - local character = replayArgs.CharactersById[tostring(characterId)] - - if not character then - - error("No Character Model given with ID: "..tostring(characterId)) - end - - assert(checkCharacter(character)) - - local archivable = character.Archivable - character.Archivable = true - local clone = character:Clone() - character.Archivable = archivable - - clone.Name = "replay-"..characterId - - return VRCharacterReplay.new({ - - Timeline = restoredArgs.Timeline, - - Character = clone, - Chalk = replayArgs.Chalk, - SoundTimeline = restoredArgs.SoundTimeline, - Origin = replayArgs.Origin, - }) -end - - - return VRCharacterReplay \ No newline at end of file diff --git a/src/VRCharacterRecorder/init.lua b/src/VRCharacterRecorder/init.lua index 8cd551b..356463b 100644 --- a/src/VRCharacterRecorder/init.lua +++ b/src/VRCharacterRecorder/init.lua @@ -1,14 +1,11 @@ -- Services local ReplicatedStorage = game:GetService("ReplicatedStorage") -local replay = script.Parent +local Replay = script.Parent -- Imports -local t = require(replay.Packages.t) +local t = require(Replay.Parent.t) local NexusVRCharacterModel = require(ReplicatedStorage:WaitForChild("NexusVRCharacterModel")) -local Character = NexusVRCharacterModel:GetResource("Character") local UpdateInputs = NexusVRCharacterModel:GetResource("UpdateInputs") -local VRCharacterReplay = require(script.VRCharacterReplay) -local persist = require(script.persist) local VRCharacterRecorder = {} VRCharacterRecorder.__index = VRCharacterRecorder @@ -17,7 +14,6 @@ local check = t.strictInterface({ Origin = t.CFrame, Player = t.instanceOf("Player"), - CharacterId = t.union(t.string, t.number), }) function VRCharacterRecorder.new(args) @@ -34,13 +30,26 @@ function VRCharacterRecorder:Start(startTime) self.Timeline = {} self.CharacterConnection = UpdateInputs.OnServerEvent:Connect(function(player, HeadCFrame, LeftHandCFrame, RightHandCFrame) + + local now = os.clock() - self.StartTime + if player ~= self.Player then return end + + local headRel = self.Origin:Inverse() * HeadCFrame + local headRx, headRy, headRz = headRel:ToEulerAnglesXYZ() + local leftHandRel = self.Origin:Inverse() * LeftHandCFrame + local leftRx, leftRy, leftRz = leftHandRel:ToEulerAnglesXYZ() + local rightHandRel = self.Origin:Inverse() * RightHandCFrame + local rightRx, rightRy, rightRz = rightHandRel:ToEulerAnglesXYZ() - local now = os.clock() - self.StartTime - - table.insert(self.Timeline, {now, {self.Origin:Inverse() * HeadCFrame, self.Origin:Inverse() * LeftHandCFrame, self.Origin:Inverse() * RightHandCFrame}}) + table.insert(self.Timeline, {now, + + headRel.Position.X, headRel.Position.Y, headRel.Position.Z, headRx, headRy, headRz, + leftHandRel.Position.X, leftHandRel.Position.Y, leftHandRel.Position.Z, leftRx, leftRy, leftRz, + rightHandRel.Position.X, rightHandRel.Position.Y, rightHandRel.Position.Z, rightRx, rightRy, rightRz, + }) end) end @@ -52,25 +61,24 @@ function VRCharacterRecorder:Stop() end end -function VRCharacterRecorder:CreateReplay(replayArgs) - - return VRCharacterReplay.new({ - - Timeline = self.Timeline, - Origin = self.Origin, - - Character = replayArgs.Character, - }) -end +function VRCharacterRecorder:FlushTimelineToRecord() -function VRCharacterRecorder:Serialise() - - local metadata = { + local record = { - CharacterId = self.CharacterId, + Timeline = self.Timeline, } - local data = self.Timeline + self.Timeline = {} + + return record +end + +local NUM_LENGTH_EST = 20 -- Average is about 19.26 +local SCAFFOLD = #[[{"Timeline":{}}]] + +function VRCharacterRecorder:GetRecordSizeEstimate() + -- events * (maxsize * numNums + commas + eventbraces + eventComma) + return #self.Timeline * (NUM_LENGTH_EST * 19 + 18 + 2 + 1) + SCAFFOLD end -return VRCharacterRecorder +return VRCharacterRecorder \ No newline at end of file diff --git a/src/VRCharacterRecorder/persist.lua b/src/VRCharacterRecorder/persist.lua deleted file mode 100644 index de3ffd8..0000000 --- a/src/VRCharacterRecorder/persist.lua +++ /dev/null @@ -1,126 +0,0 @@ --- Services -local replay = script.Parent.Parent - --- Helper functions -local serialiser = require(script.Parent.serialiser) -local chunker = require(replay.persistTools.chunker) -local waitForBudget = require(replay.persistTools.waitForBudget) -local safeSet = require(replay.persistTools.safeSet) - -local function store(self, dataStore, key) - - local timelineData, chalkTimelineData = serialiser.Serialise(self.Timeline, self.ChalkTimeline) - local timelineChunks = chunker.Chunk(timelineData) - local chalkTimelineChunks = chunker.Chunk(chalkTimelineData) - - local data = { - - _FormatVersion = "VRCharacter-v1", - - CharacterId = self.CharacterId, - - TimelineKey = key.."/timeline", -- Format that remembers itself! - ChalkTimelineKey = key.."/chalkTimeline", - - ChalkTimelineChunkCount = #chalkTimelineChunks, - ChalkTimelineFirstChunk = chalkTimelineChunks[1], - } - - local timelineData = { - - TimelineChunkCount = #timelineChunks, - TimelineFirstChunk = timelineChunks[1], - } - - local allSuccess = true - - waitForBudget(Enum.DataStoreRequestType.SetIncrementAsync) - allSuccess = safeSet(dataStore, data.TimelineKey, timelineData) and allSuccess - - for i=2, #timelineChunks do - - waitForBudget(Enum.DataStoreRequestType.SetIncrementAsync) - allSuccess = safeSet(dataStore, data.TimelineKey..":"..i, timelineChunks[i]) and allSuccess - end - - allSuccess = safeSet(dataStore, key, data) and allSuccess - - -- It is *very* unlikely that there is more than 4MB of equipping/unequipping chalk - for i=2, #chalkTimelineChunks do - - warn("[Replay] Unexpectedly large number of chalk timeline entries") - - waitForBudget(Enum.DataStoreRequestType.SetIncrementAsync) - allSuccess = safeSet(dataStore, data.ChalkTimelineKey..":"..i, chalkTimelineChunks[i]) and allSuccess - end - - return allSuccess -end - -local function restore(dataStore: DataStore, key: string) - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local data = dataStore:GetAsync(key) - - assert(data._FormatVersion == "VRCharacter-v1", "Format version "..tostring(data._FormatVersion).." unrecognised") - - -- ChalkTimeline - - local chalkTimelineChunks do - - chalkTimelineChunks = table.create(data.ChalkTimelineChunkCount) - - if data.ChalkTimelineFirstChunk then - - table.insert(chalkTimelineChunks, data.ChalkTimelineFirstChunk) - end - - for i=2, data.ChalkTimelineChunkCount do - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local chunk = dataStore:GetAsync(data.ChalkTimelineKey..":"..i) - - table.insert(chalkTimelineChunks, chunk) - end - end - - -- Timeline - - local timelineChunks do - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local timelineData = dataStore:GetAsync(data.TimelineKey) - - timelineChunks = table.create(timelineData.TimelineChunkCount) - - if timelineData.TimelineFirstChunk then - - table.insert(timelineChunks, timelineData.TimelineFirstChunk) - end - - for i=2, timelineData.TimelineChunkCount do - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local chunk = dataStore:GetAsync(data.TimelineKey..":"..i) - - table.insert(timelineChunks, chunk) - end - end - - local timelineData = chunker.Gather(timelineChunks) - local chalkTimelineData = chunker.Gather(chalkTimelineChunks) - local timeline, chalkTimeline = serialiser.Deserialise(timelineData, chalkTimelineData) - - return { - - Timeline = timeline, - ChalkTimeline = chalkTimeline, - CharacterId = data.CharacterId, - } -end - -return { - - Store = store, - Restore = restore, -} \ No newline at end of file diff --git a/src/VRCharacterRecorder/serialiser.lua b/src/VRCharacterRecorder/serialiser.lua deleted file mode 100644 index 41ed517..0000000 --- a/src/VRCharacterRecorder/serialiser.lua +++ /dev/null @@ -1,50 +0,0 @@ -local function serialiseCFrame(cframe: CFrame) - - return table.pack(cframe:GetComponents()) -end - -local function deserialiseCFrame(data) - - return CFrame.new(table.unpack(data)) -end - -local function serialise(timeline, chalkTimeline) - - local timelineData = table.create(#timeline) - - for _, event in ipairs(timeline) do - - local timestamp, charCframes = unpack(event) - - table.insert(timelineData, {timestamp, {serialiseCFrame(charCframes[1]), serialiseCFrame(charCframes[2]), serialiseCFrame(charCframes[3])}}) - end - - -- Nothing to serialise - local chalkTimelineData = chalkTimeline - - return timelineData, chalkTimelineData -end - -local function deserialise(timelineData, chalkTimelineData) - - local timeline = table.create(#timelineData) - - for _, eventData in ipairs(timelineData) do - - local timestamp, charCframeData = unpack(eventData) - - table.insert(timeline, {timestamp, {deserialiseCFrame(charCframeData[1]), deserialiseCFrame(charCframeData[2]), deserialiseCFrame(charCframeData[3])}}) - end - - -- Nothing to deserialise - local chalkTimeline = chalkTimelineData - - return timeline, chalkTimeline -end - - -return { - - Serialise = serialise, - Deserialise = deserialise, -} diff --git a/src/VRCharacterRecorder/updateAnchoredFromInputs.lua b/src/VRCharacterRecorder/updateAnchoredFromInputs.lua index 3f8a549..a7a8d0f 100644 --- a/src/VRCharacterRecorder/updateAnchoredFromInputs.lua +++ b/src/VRCharacterRecorder/updateAnchoredFromInputs.lua @@ -18,7 +18,7 @@ end --[[ This is derived from Character:UpdateFromInputs() in NexusVRCharacterModel. - It sets the parts to the target cframes, instead of tweening them there. + It tweens or sets the cframes directly, instead of using motors --]] return function(nexusCharacter, HeadControllerCFrame: CFrame, LeftHandControllerCFrame: CFrame, RightHandControllerCFrame: CFrame, instantly: boolean?) @@ -70,27 +70,4 @@ return function(nexusCharacter, HeadControllerCFrame: CFrame, LeftHandController tweenToCFrame(self.Parts.RightFoot, RightFootCFrame, instantly) tweenToCFrame(self.Parts.HumanoidRootPart, TargetHumanoidRootPartCFrame, instantly) - - -- self.Parts.Head.CFrame = HeadCFrame - - -- self.Parts.LowerTorso.CFrame = LowerTorsoCFrame - -- self.Parts.UpperTorso.CFrame = UpperTorsoCFrame - - -- self.Parts.LeftUpperArm.CFrame = LeftUpperArmCFrame - -- self.Parts.LeftLowerArm.CFrame = LeftLowerArmCFrame - -- self.Parts.LeftHand.CFrame = LeftHandCFrame - - -- self.Parts.RightUpperArm.CFrame = RightUpperArmCFrame - -- self.Parts.RightLowerArm.CFrame = RightLowerArmCFrame - -- self.Parts.RightHand.CFrame = RightHandCFrame - - -- self.Parts.LeftUpperLeg.CFrame = LeftUpperLegCFrame - -- self.Parts.LeftLowerLeg.CFrame = LeftLowerLegCFrame - -- self.Parts.LeftFoot.CFrame = LeftFootCFrame - - -- self.Parts.RightUpperLeg.CFrame = RightUpperLegCFrame - -- self.Parts.RightLowerLeg.CFrame = RightLowerLegCFrame - -- self.Parts.RightFoot.CFrame = RightFootCFrame - - -- self.Parts.HumanoidRootPart.CFrame = TargetHumanoidRootPartCFrame end \ No newline at end of file diff --git a/src/dataSerialiser.lua b/src/dataSerialiser.lua deleted file mode 100644 index 5972c07..0000000 --- a/src/dataSerialiser.lua +++ /dev/null @@ -1,166 +0,0 @@ -local function serialise(data) - - local typ = typeof(data) - - if typ == "number" or typ == "string" or typ == "boolean" or typ == "nil" then - - return data - end - - local encData = { - - _dataType = typ - } - - if typ == "table" then - - for key, value in data do - - if key == "_dataType" then - - error("Table cannot have '_dataType' as key (reserved for deserialising)") - end - - encData[key] = serialise(value) - end - - return encData - end - - if typ == "BrickColor" then - - encData.Name = data.Name - return encData - end - - if typ == "CFrame" then - - encData.Comps = data:GetComponents() - return encData - end - - if typ == "Color3" then - - encData.R = data.R - encData.G = data.G - encData.B = data.B - return encData - end - - if typ == "Vector2" then - - encData.X = data.X - encData.Y = data.Y - return encData - end - - if typ == "Vector3" then - - encData.X = data.X - encData.Y = data.Y - encData.Z = data.Z - return encData - end - - error("[Replay] Cannot serialise "..tostring(typ)) -end - -local function deserialise(encData) - - local encDataType = typeof(encData) - - if encDataType == "number" or encDataType == "string" or encDataType == "boolean" or encDataType == "nil" then - - return encData - end - - local typ = encData._dataType - - if typ == "table" then - - local data = {} - - for key, value in encData do - - if key == "_dataType" then - continue - end - - data[key] = deserialise(value) - end - - return data - end - - if typ == "BrickColor" then - - return BrickColor.new(encData.Name) - end - - if typ == "CFrame" then - - return CFrame.new(encData.Comps) - end - - if typ == "Color3" then - - return Color3.new(encData.R, encData.G, encData.B) - end - - if typ == "Vector2" then - - return Vector2.new(encData.X, encData.Y) - end - - if typ == "Vector3" then - - return Vector3.new(encData.X, encData.Y, encData.Z) - end - - error("[Replay] Deserialiser _dataType: "..tostring(typ).." not recognised") -end - -local _dataTypes = { - "number", - "string", - "boolean", - "nil", - "table", - "BrickColor", - "CFrame", - "Color3", - "Vector2", - "Vector3", -} - -local CAN_ENCODE = {} - -for _, typ in ipairs(_dataTypes) do - - CAN_ENCODE[typ] = true -end - -local function canSerialise(data) - - if typeof(data) == "table" then - - for _, value in data do - - if not canSerialise(data) then - - return false - end - end - - return true - end - - return CAN_ENCODE[typeof(data)] -end - -return { - - Serialise = serialise, - Deserialise = deserialise, - CanSerialise = canSerialise, -} \ No newline at end of file diff --git a/src/init.lua b/src/init.lua new file mode 100644 index 0000000..f10463c --- /dev/null +++ b/src/init.lua @@ -0,0 +1,12 @@ +return { + + BoardRecorder = require(script.BoardRecorder), + BoardReplay = require(script.BoardRecorder.BoardReplay), + VRCharacterRecorder = require(script.VRCharacterRecorder), + VRCharacterReplay = require(script.VRCharacterRecorder.VRCharacterReplay), + EventRecorder = require(script.EventRecorder), + EventReplay = require(script.EventRecorder.EventReplay), + SoundReplay = require(script.SoundReplay), + HumanoidDescriptionSerialiser = require(script.HumanoidDescriptionSerialiser), + BoardSerialiser = require(script.BoardSerialiser) +} \ No newline at end of file diff --git a/src/persist.lua b/src/persist.lua deleted file mode 100644 index 2317bb4..0000000 --- a/src/persist.lua +++ /dev/null @@ -1,298 +0,0 @@ --- Imports -local BoardReplay = require(script.Parent.BoardRecorder.BoardReplay) -local VRCharacterReplay = require(script.Parent.VRCharacterRecorder.VRCharacterReplay) -local CharacterReplay = require(script.Parent.CharacterRecorder.CharacterReplay) -local EventReplay = require(script.Parent.EventRecorder.EventReplay) - --- Helper functions -local waitForBudget = require(script.Parent.persistTools.waitForBudget) -local safeSet = require(script.Parent.persistTools.safeSet) - -local function store(self, dataStore: DataStore, replayId: number, replayName: string, force: boolean, retryOnFail: boolean) - - if not force then - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local data = dataStore:GetAsync("ReplayIndex/"..replayId) - - if data then - - local errormsg = "[Replay] Key: ReplayIndex/"..replayId.." already in use. Use force=true to silence" - warn(errormsg) - - return false, errormsg, data - end - end - - local boardsData = {} - - for boardId, board in self.Boards do - - boardsData[boardId] = { - - Name = board:FullName(), - Key = "ReplayData/"..replayId.."/Boards/"..boardId, - } - end - - local vrCharactersData = {} - - for i, vrPlayer in ipairs(self.VRPlayers) do - - table.insert(vrCharactersData, { - - CharacterId = tostring(vrPlayer.UserId), - Key = "ReplayData/"..replayId.."/VRCharacters/"..i - }) - end - - local charactersData = {} - - for i, player in ipairs(self.Players) do - - table.insert(charactersData, { - - CharacterId = tostring(player.UserId), - Key = "ReplayData/"..replayId.."/Characters/"..i - }) - end - - local eventsData = {} - - for signalId, signal in self.Signals do - - eventsData[signalId] = { - - SignalId = signalId, - Key = "ReplayData/"..replayId.."/Events/"..signalId, - } - end - - local data = { - - _FormatVersion = "Replay-v1", - - Name = replayName, - - BoardReplays = boardsData, - VRCharacterReplays = vrCharactersData, - CharacterReplays = charactersData, - EventReplays = eventsData, - } - - local allSuccess = true - - while true do - - waitForBudget(Enum.DataStoreRequestType.SetIncrementAsync) - allSuccess = safeSet(dataStore, "ReplayIndex/"..replayId, data) and allSuccess - - if retryOnFail and not allSuccess then - - warn("[Replay] Replay "..replayId..", "..replayName..", failed. Retrying in 5sec.") - task.wait(5) - continue - end - - for boardId, board in self.Boards do - - local key = data.BoardReplays[boardId].Key - local boardRecorder = self.BoardRecorders[boardId] - - allSuccess = boardRecorder:Store(dataStore, key) and allSuccess - end - - if retryOnFail and not allSuccess then - - warn("[Replay] Replay "..replayId..", "..replayName..", failed. Retrying in 5sec.") - task.wait(5) - continue - end - - for i, vrPlayer in ipairs(self.VRPlayers) do - - local replayData = data.VRCharacterReplays[i] - local key = replayData.Key - local vrCharacterRecorder = self.VRCharacterRecorders[i] - - allSuccess = vrCharacterRecorder:Store(dataStore, key) and allSuccess - end - - if retryOnFail and not allSuccess then - - warn("[Replay] Replay "..replayId..", "..replayName..", failed. Retrying in 5sec.") - task.wait(5) - continue - end - - for i, player in ipairs(self.Players) do - - local replayData = data.CharacterReplays[i] - local key = replayData.Key - local characterRecorder = self.CharacterRecorders[i] - - allSuccess = characterRecorder:Store(dataStore, key) and allSuccess - end - - if retryOnFail and not allSuccess then - - warn("[Replay] Replay "..replayId..", "..replayName..", failed. Retrying in 5sec.") - task.wait(5) - continue - end - - for signalId, signal in self.Signals do - - local replayData = data.EventReplays[signalId] - local key = replayData.Key - local eventRecorder = self.EventRecorders[signalId] - - allSuccess = eventRecorder:Store(dataStore, key) and allSuccess - end - - if retryOnFail and not allSuccess then - - warn("[Replay] Replay "..replayId..", "..replayName..", failed. Retrying in 5sec.") - task.wait(5) - continue - end - - break - end - - if allSuccess then - - print("[Replay] Successfully stored replay "..replayName.." at ReplayIndex/"..replayId) - - else - - warn("[Replay] Replay "..replayId..", "..replayName..", failed. It may be possible to partially recover it by inspecting keys") - end -end - -local function restore(dataStore: DataStore, replayId: string, liveData) - - local origin: CFrame = liveData.Origin - local boards = liveData.Boards - local chalk: Tool = liveData.Chalk - local charactersById: {[string]: Model} = liveData.CharactersById - local eventCallbacks: {[string]: () -> nil} = liveData.EventCallbacks - - local replays = {} - - local success, errormsg = xpcall(function() - - waitForBudget(Enum.DataStoreRequestType.GetAsync) - local data = dataStore:GetAsync("ReplayIndex/"..replayId) - - if not data then - - return nil - end - - assert(data._FormatVersion == "Replay-v1", "Format version "..tostring(data._FormatVersion).." unrecognised") - - -- Board Replays - - for boardId, replayData in data.BoardReplays or {} do - - local key = replayData.Key - local replay = BoardReplay.Restore(dataStore, key, { - Board = boards[boardId], - }) - - table.insert(replays, replay) - end - - -- VRCharacter Replays - - for i, replayData in ipairs(data.VRCharacterReplays or {}) do - - local key = replayData.Key - local soundTimelineData = replayData.SoundTimeline or {} - - local soundTimeline = {} - - for i, soundData in ipairs(soundTimelineData) do - - local timestamp = soundData.Timestamp - local startPosition = soundData.StartPosition or 0 - local soundId = soundData.SoundId - - local sound = Instance.new("Sound") - sound.SoundId = soundId - sound.TimePosition = startPosition - - table.insert(soundTimeline, {timestamp, sound}) - end - - table.insert(replays, VRCharacterReplay.Restore(dataStore, key, { - - Origin = origin, - Chalk = chalk:Clone(), - SoundTimeline = soundTimeline, - CharactersById = charactersById, - })) - end - - -- Character Replays - - for i, replayData in ipairs(data.CharacterReplays or {}) do - - local key = replayData.Key - local soundTimelineData = replayData.SoundTimeline or {} - - local soundTimeline = {} - - for i, soundData in ipairs(soundTimelineData) do - - local timestamp = soundData.Timestamp - local startPosition = soundData.StartPosition or 0 - local soundId = soundData.SoundId - - local sound = Instance.new("Sound") - sound.SoundId = soundId - sound.TimePosition = startPosition - - table.insert(soundTimeline, {timestamp, sound}) - end - - table.insert(replays, CharacterReplay.Restore(dataStore, key, { - - Origin = origin, - Chalk = chalk:Clone(), - SoundTimeline = soundTimeline, - CharactersById = charactersById, - })) - end - - -- Event Replays - - for signalId, replayData in (data.EventReplays or {}) do - - local key = replayData.Key - local replay = EventReplay.Restore(dataStore, key, { - - Callback = eventCallbacks[signalId] - }) - - table.insert(replays, replay) - end - - end, debug.traceback) - - if not success then - - error("[Replay] Restore failed for replayId "..replayId.."\n"..errormsg) - end - - print("[Replay] Successfully restored Replay with Id: "..replayId) - - return replays -end - -return { - - Store = store, - Restore = restore, -} \ No newline at end of file diff --git a/src/persistTools/chunker.lua b/src/persistTools/chunker.lua deleted file mode 100644 index ef0774d..0000000 --- a/src/persistTools/chunker.lua +++ /dev/null @@ -1,72 +0,0 @@ -local HttpService = game:GetService("HttpService") - -local DEFAULT_MAX_CHUNK_SIZE = 3900000 - -local function chunk(entries, maxChunkSize: number?) - - maxChunkSize = maxChunkSize or DEFAULT_MAX_CHUNK_SIZE - - local lines = table.create(#entries) - - for _, entry in ipairs(entries) do - - local entryData = HttpService:JSONEncode(entry).."\n" - - if #entryData > maxChunkSize then - error("Entry exceeds max chunk size") - end - - table.insert(lines, entryData) - end - - local chunks = {} do - - local i = 1 - while i <= #lines do - - local chunkSize = 0 - local j = i - - while j <= #lines and chunkSize <= maxChunkSize do - - chunkSize += lines[j]:len() - j += 1 - end - - -- entries i through j-1 don't exceed the chunk limit when concatenated - - table.insert(chunks, table.concat(lines, "", i, j - 1)) - - i = j - end - end - - return chunks -end - -local function gather(chunks) - - local entries = {} - - for _, chunk in ipairs(chunks) do - - local j = 1 - - while j < chunk:len() do - - local k = chunk:find("\n", j + 1) - local entry = HttpService:JSONDecode(chunk:sub(j, k - 1)) - table.insert(entries, entry) - - j = k + 1 - end - end - - return entries -end - -return { - - Chunk = chunk, - Gather = gather, -} \ No newline at end of file diff --git a/src/persistTools/safeSet.lua b/src/persistTools/safeSet.lua deleted file mode 100644 index d4ce0ca..0000000 --- a/src/persistTools/safeSet.lua +++ /dev/null @@ -1,17 +0,0 @@ -return function(dataStore: DataStore, key: string, data) - - local success, errormsg = xpcall(function() - - dataStore:SetAsync(key, data) - - return true - - end, debug.traceback) - - if not success then - - warn("[Replay] SetAsync fail for key "..key.."\n"..errormsg) - end - - return success -end \ No newline at end of file diff --git a/src/persistTools/waitForBudget.lua b/src/persistTools/waitForBudget.lua deleted file mode 100644 index 59eddc2..0000000 --- a/src/persistTools/waitForBudget.lua +++ /dev/null @@ -1,8 +0,0 @@ -local DataStoreService = game:GetService("DataStoreService") - -return function(requestType: Enum.DataStoreRequestType) - - while DataStoreService:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.GetAsync) <= 0 do - task.wait() - end -end \ No newline at end of file diff --git a/test.project.json b/test.project.json new file mode 100644 index 0000000..c9f4efa --- /dev/null +++ b/test.project.json @@ -0,0 +1,25 @@ +{ + "name": "replayutils", + "tree": { + "$className": "DataModel", + + "ReplicatedStorage": { + + "ReplayUtils": { + "$path": "Packages", + "$className": "ModuleScript", + + "$properties": { + "Source": "return require(script.lib)" + }, + + "lib": { + "$path": "src" + + } + + } + } + + } +} \ No newline at end of file diff --git a/version.txt b/version.txt deleted file mode 100644 index f98262b..0000000 --- a/version.txt +++ /dev/null @@ -1 +0,0 @@ -v0.1.0-alpha \ No newline at end of file diff --git a/wally.toml b/wally.toml index e709f1a..bf4dcce 100644 --- a/wally.toml +++ b/wally.toml @@ -1,9 +1,8 @@ [package] -name = "metauni/replay" -version = "0.1.0" +name = "metauni/replayutils" +version = "0.2.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" [dependencies] -t = "osyrisrblx/t@3.0.0" -GoodSignal = "stravant/goodsignal@0.2.1" \ No newline at end of file +t = "osyrisrblx/t@3.0.0" \ No newline at end of file