diff --git a/games/integration/modules/Server/GameServiceServer.lua b/games/integration/modules/Server/GameServiceServer.lua index c437dbe1cb..d3c27b3c5e 100644 --- a/games/integration/modules/Server/GameServiceServer.lua +++ b/games/integration/modules/Server/GameServiceServer.lua @@ -15,6 +15,7 @@ function GameServiceServer:Init(serviceBag) self._serviceBag:GetService(require("IKService")) -- Internal + self._serviceBag:GetService(require("GameTranslator")) self._serviceBag:GetService(require("GameBindersServer")) end diff --git a/games/integration/modules/Client/GameTranslator.lua b/games/integration/modules/Shared/GameTranslator.lua similarity index 100% rename from games/integration/modules/Client/GameTranslator.lua rename to games/integration/modules/Shared/GameTranslator.lua diff --git a/src/animations/src/Shared/AnimationSlotPlayer.lua b/src/animations/src/Shared/AnimationSlotPlayer.lua index 7dcc901656..118c75b852 100644 --- a/src/animations/src/Shared/AnimationSlotPlayer.lua +++ b/src/animations/src/Shared/AnimationSlotPlayer.lua @@ -9,6 +9,7 @@ local AnimationUtils = require("AnimationUtils") local ValueObject = require("ValueObject") local Maid = require("Maid") local EnumUtils = require("EnumUtils") +local RbxAssetUtils = require("RbxAssetUtils") local AnimationSlotPlayer = setmetatable({}, BaseObject) AnimationSlotPlayer.ClassName = "AnimationSlotPlayer" @@ -17,14 +18,10 @@ AnimationSlotPlayer.__index = AnimationSlotPlayer function AnimationSlotPlayer.new(animationTarget) local self = setmetatable(BaseObject.new(), AnimationSlotPlayer) - self._animationTarget = ValueObject.new(nil) - self._maid:GiveTask(self._animationTarget) - - self._defaultFadeTime = ValueObject.new(0.1, "number") - self._maid:GiveTask(self._defaultFadeTime) - - self._defaultAnimationPriority = ValueObject.new(nil) - self._maid:GiveTask(self._defaultAnimationPriority) + self._animationTarget = self._maid:Add(ValueObject.new(nil)) + self._defaultFadeTime = self._maid:Add(ValueObject.new(0.1, "number")) + self._defaultAnimationPriority = self._maid:Add(ValueObject.new(nil)) + self._currentAnimationTrackData = self._maid:Add(ValueObject.new(nil)) if animationTarget then self:SetAnimationTarget(animationTarget) @@ -47,6 +44,45 @@ function AnimationSlotPlayer:SetAnimationTarget(animationTarget) self._animationTarget:Mount(animationTarget) end +function AnimationSlotPlayer:AdjustSpeed(id, speed) + assert(RbxAssetUtils.isConvertableToRbxAsset(id), "Bad id") + assert(type(speed) == "number", "Bad number") + + local animationId = RbxAssetUtils.toRbxAssetId(id) + + local topMaid = Maid.new() + + topMaid:GiveTask(self._currentAnimationTrackData:ObserveBrio(function(data) + return data and data.animationId == animationId + end):Subscribe(function(brio) + if brio:IsDead() then + return + end + + local data = brio:GetValue() + local maid = brio:ToMaid() + + data.track:AdjustSpeed(speed) + + -- TODO: Use stack here? + -- TODO: Probably need rogue property mechanisms + maid:GiveTask(function() + if math.abs(data.track.Speed - speed) <= 1e-3 then + data.track:AdjustSpeed(data.originalSpeed) + end + end) + end)) + + -- TODO: Probably per-a-track instead of global like this + self._maid._currentSpeedAdjustment = topMaid + + return function() + if self._maid._currentSpeedAdjustment == topMaid then + self._maid._currentSpeedAdjustment = nil + end + end +end + function AnimationSlotPlayer:Play(id, fadeTime, weight, speed, priority) fadeTime = fadeTime or self._defaultFadeTime.Value priority = priority or self._defaultAnimationPriority.Value @@ -54,6 +90,8 @@ function AnimationSlotPlayer:Play(id, fadeTime, weight, speed, priority) local topMaid = Maid.new() + local animationId = RbxAssetUtils.toRbxAssetId(id) + topMaid:GiveTask(self._animationTarget:ObserveBrio(function(target) return target ~= nil end):Subscribe(function(brio) @@ -64,8 +102,23 @@ function AnimationSlotPlayer:Play(id, fadeTime, weight, speed, priority) local animationTarget = brio:GetValue() local maid = brio:ToMaid() - local track = AnimationUtils.playAnimation(animationTarget, id, fadeTime, weight, speed, priority) + local track = AnimationUtils.playAnimation(animationTarget, animationId, fadeTime, weight, speed, priority) if track then + local data = { + animationId = animationId; + track = track; + originalSpeed = speed; + originalWeight = weight; + originalPriority = priority; + } + + self._currentAnimationTrackData.Value = data + maid:GiveTask(function() + if self._currentAnimationTrackData.Value == data then + self._currentAnimationTrackData.Value = nil + end + end) + maid:GiveTask(function() track:AdjustWeight(0, fadeTime or self._defaultFadeTime.Value) end) diff --git a/src/binder/src/Shared/Binder.lua b/src/binder/src/Shared/Binder.lua index 60af1bd902..dc9bdcc209 100644 --- a/src/binder/src/Shared/Binder.lua +++ b/src/binder/src/Shared/Binder.lua @@ -76,6 +76,10 @@ function Binder.new(tagName, constructor, ...) self._defaultClassType = "Folder" self.ServiceName = self._tagName .. "Binder" + if Binder.isBinder(self._constructor) then + error("Cannot make a binder that constructs another binder") + end + if select("#", ...) > 0 then self._args = { ... } end diff --git a/src/blend/src/Shared/Blend/Blend.lua b/src/blend/src/Shared/Blend/Blend.lua index 9d4d2e91cf..c57d8308cf 100644 --- a/src/blend/src/Shared/Blend/Blend.lua +++ b/src/blend/src/Shared/Blend/Blend.lua @@ -23,6 +23,7 @@ local StepUtils = require("StepUtils") local ValueBaseUtils = require("ValueBaseUtils") local ValueObject = require("ValueObject") local ValueObjectUtils = require("ValueObjectUtils") +local RxBrioUtils = require("RxBrioUtils") local Blend = {} @@ -624,6 +625,54 @@ function Blend.Children(parent, value) end end +--[=[ + Allows you to add [CollectionService] tags to a Blend object. + + ```lua + Blend.New "ScreenGui" { + [Blend.Tags] = { "Hide", "ScreenGui" }; + }; + ``` + + @param parent Instance + @param value any + @return Observable +]=] +function Blend.Tags(parent, value) + assert(typeof(parent) == "Instance", "Bad parent") + + local observe = Blend._observeTags(value, parent) + + if observe then + return observe:Pipe({ + Rx.tap(function(tag) + if type(tag) == "string" then + parent:AddTag(tag) + else + error("Bad tag") + end + end); + }) + else + return Rx.EMPTY + end +end + + +function Blend._observeTags(tags) + if type(tags) == "string" then + return Rx.of(tags) + elseif type(tags) == "table" then + if Observable.isObservable(tags) then + return tags + else + error("Bad tags") + end + else + error("Bad tags") + end +end + --[=[ Mounts Blend objects into an existing instance. @@ -647,33 +696,41 @@ end maid:GiveTask(Blend.mount(frame, { Size = UDim2.new(0.5, 0, 0.5, 0); - Blend.Find "MyUIScaleName" { + Blend.Find "UIScale" { Scale = 2; }; })) ``` - @param name string + :::tip + + ::: + + @param className string @return function ]=] -function Blend.Find(name) - assert(type(name) == "string", "Bad name") +function Blend.Find(className) + assert(type(className) == "string", "Bad className") return function(props) assert(type(props) == "table", "Bad props") + assert(type(props.Name) == "string", "No props.Name") - local mountProps = props - local className - if props.ClassName then - className = props.ClassName - mountProps = table.clone(props) - mountProps.ClassName = nil - else - className = "Instance" - end + -- Return observable and assume we're being used in anexternal context + -- TODO: Maybe not this + if props.Parent then + local propertyObservable = Blend.toPropertyObservable(props.Parent) or Rx.of(props.Parent) - return function(parent) - return RxInstanceUtils.observeChildrenOfNameBrio(parent, className, name):Pipe({ + return propertyObservable:Pipe({ + RxBrioUtils.toBrio(); + RxBrioUtils.where(function(parent) + return parent ~= nil + end); + RxBrioUtils.switchMapBrio(function(parent) + assert(typeof(parent) == "Instance", "Bad parent retrieved during find spec") + + return RxInstanceUtils.observeChildrenOfNameBrio(parent, className, props.Name) + end); Rx.flatMap(function(brio) if brio:IsDead() then return @@ -681,22 +738,50 @@ function Blend.Find(name) local maid = brio:ToMaid() local instance = brio:GetValue() - maid:GiveTask(Blend.mount(instance, mountProps)) + maid:GiveTask(Blend.mount(instance, props)) - -- Dead after mounting? Clean up... - -- Probably caused by name change. if brio:IsDead() then maid:DoCleaning() end - -- Avoid emitting anything else so we don't get cleaned up - return Rx.EMPTY + -- Emit back found value (we're used in property scenario) + return Rx.of(instance) end); }) end + + -- Return callback + return function(parent) + -- TODO: Swap based upon name + -- TODO: Avoid assigning name + return RxInstanceUtils.observeChildrenOfNameBrio(parent, className, props.Name):Pipe({ + Blend._mountToFinding(props); + }) + end end end +function Blend._mountToFinding(props) + return Rx.flatMap(function(brio) + if brio:IsDead() then + return + end + + local maid = brio:ToMaid() + local instance = brio:GetValue() + maid:GiveTask(Blend.mount(instance, props)) + + -- Dead after mounting? Clean up... + -- Probably caused by name change. + if brio:IsDead() then + maid:DoCleaning() + end + + -- Avoid emitting anything else so we don't get cleaned up + return Rx.EMPTY + end); +end + --[=[ An event emitter that emits the instance that was actually created. This is useful for a variety of things. @@ -1018,8 +1103,9 @@ end maid:GiveTask(Blend.mount(frame, { BackgroundTransparency = 1; - -- All items named InventoryItem - Blend.Find "InventoryItem" { + -- All items named InventoryFrame + Blend.Find "Frame" { + Name = "InventoryFrame" -- Apply the following properties Blend.New "UIScale" { diff --git a/src/blend/src/Shared/Test/BlendFind.story.lua b/src/blend/src/Shared/Test/BlendFind.story.lua index ca152824fe..648a90df32 100644 --- a/src/blend/src/Shared/Test/BlendFind.story.lua +++ b/src/blend/src/Shared/Test/BlendFind.story.lua @@ -52,9 +52,11 @@ return function(target) CornerRadius = UDim.new(0.05, 0); }; - Blend.Find "CenterFrame" { - Blend.Find "MyUIScale" { - ClassName = "UIScale"; + Blend.Find "Frame" { + Name = "CenterFrame"; + + Blend.Find "UIScale" { + Name = "MyUIScale"; Scale = Blend.Computed(percentVisible, function(percent) return 0.8 + 0.2*percent diff --git a/src/brio/src/Shared/RxBrioUtils.lua b/src/brio/src/Shared/RxBrioUtils.lua index 1a9c0bb088..f97c4117cd 100644 --- a/src/brio/src/Shared/RxBrioUtils.lua +++ b/src/brio/src/Shared/RxBrioUtils.lua @@ -16,6 +16,28 @@ local Rx = require("Rx") local RxBrioUtils = {} +--[=[ + Creates a new observable wrapping the brio + + @param callback function + @return Observable +]=] +function RxBrioUtils.ofBrio(callback) + return Observable.new(function(sub) + local maid = Maid.new() + + if type(callback) == "function" then + local brio = maid:Add(Brio.new(callback(maid))) + sub:Fire(brio) + else + local brio = maid:Add(Brio.new(callback)) + sub:Fire(brio) + end + + return maid + end) +end + --[=[ Takes a result and converts it to a brio if it is not one. diff --git a/src/buttonhighlightmodel/src/Client/ButtonHighlightModel.lua b/src/buttonhighlightmodel/src/Client/ButtonHighlightModel.lua index bf3d569765..ca6eccdd4b 100644 --- a/src/buttonhighlightmodel/src/Client/ButtonHighlightModel.lua +++ b/src/buttonhighlightmodel/src/Client/ButtonHighlightModel.lua @@ -58,8 +58,15 @@ function ButtonHighlightModel.new(button, onUpdate) self._onUpdate = onUpdate - self._interactionEnabled = ValueObject.new(true) - self._maid:GiveTask(self._interactionEnabled) + self._interactionEnabled = self._maid:Add(ValueObject.new(true, "boolean")) + self._isSelected = self._maid:Add(ValueObject.new(false, "boolean")) + self._isMouseOrTouchOver = self._maid:Add(ValueObject.new(false, "boolean")) + self._isMouseDown = self._maid:Add(ValueObject.new(false, "boolean")) + self._numFingerDown = self._maid:Add(ValueObject.new(0, "number")) + self._isChoosen = self._maid:Add(ValueObject.new(false, "boolean")) + self._isMouseOver = self._maid:Add(ValueObject.new(false, "boolean")) + self._isKeyDown = self._maid:Add(ValueObject.new(false, "boolean")) + self._isHighlighted = self._maid:Add(ValueObject.new(false, "boolean")) --[=[ @prop InteractionEnabledChanged Signal @@ -68,9 +75,6 @@ function ButtonHighlightModel.new(button, onUpdate) ]=] self.InteractionEnabledChanged = self._interactionEnabled.Changed - self._isSelected = ValueObject.new(false) - self._maid:GiveTask(self._isSelected) - --[=[ @prop IsSelectedChanged Signal @readonly @@ -78,9 +82,6 @@ function ButtonHighlightModel.new(button, onUpdate) ]=] self.IsSelectedChanged = self._isSelected.Changed - self._isMouseOrTouchOver = ValueObject.new(false) - self._maid:GiveTask(self._isMouseOrTouchOver) - --[=[ @prop IsMouseOrTouchOverChanged Signal @readonly @@ -88,27 +89,6 @@ function ButtonHighlightModel.new(button, onUpdate) ]=] self.IsMouseOrTouchOverChanged = self._isMouseOrTouchOver.Changed - self._isMouseDown = Instance.new("BoolValue") - self._isMouseDown.Value = false - self._maid:GiveTask(self._isMouseDown) - - self._numFingerDown = Instance.new("IntValue") - self._numFingerDown.Value = 0 - self._maid:GiveTask(self._numFingerDown) - - self._isChoosen = ValueObject.new(false) - self._maid:GiveTask(self._isChoosen) - - self._isMouseOver = Instance.new("BoolValue") - self._isMouseOver.Value = false - self._maid:GiveTask(self._isMouseOver) - - self._isKeyDown = ValueObject.new(false) - self._maid:GiveTask(self._isKeyDown) - - self._isHighlighted = ValueObject.new(false) - self._maid:GiveTask(self._isHighlighted) - --[=[ @prop IsHighlightedChanged Signal @readonly @@ -116,8 +96,7 @@ function ButtonHighlightModel.new(button, onUpdate) ]=] self.IsHighlightedChanged = self._isHighlighted.Changed - self._isPressed = ValueObject.new(false) - self._maid:GiveTask(self._isPressed) + self._isPressed = self._maid:Add(ValueObject.new(false)) --[=[ @prop IsPressedChanged Signal @@ -210,6 +189,12 @@ function ButtonHighlightModel:SetButton(button: Instance) end self._maid._buttonMaid = maid + + return function() + if self._maid._buttonMaid == maid then + self._maid._buttonMaid = nil + end + end end --[=[ @@ -371,9 +356,7 @@ end @param interactionEnabled boolean ]=] function ButtonHighlightModel:SetInteractionEnabled(interactionEnabled) - assert(type(interactionEnabled) == "boolean", "Bad interactionEnabled") - - self._interactionEnabled.Value = interactionEnabled + self._interactionEnabled:Mount(interactionEnabled) end --[=[ diff --git a/src/chatproviderservice/src/Server/ChatProviderService.lua b/src/chatproviderservice/src/Server/ChatProviderService.lua index 3943fe9395..fadfffb44c 100644 --- a/src/chatproviderservice/src/Server/ChatProviderService.lua +++ b/src/chatproviderservice/src/Server/ChatProviderService.lua @@ -34,6 +34,7 @@ function ChatProviderService:Init(serviceBag) -- Internal self._serviceBag:GetService(require("ChatProviderCommandService")) + self._serviceBag:GetService(require("ChatProviderTranslator")) -- Binders self._serviceBag:GetService(require("ChatTag")) diff --git a/src/chatproviderservice/src/Client/ChatProviderTranslator.lua b/src/chatproviderservice/src/Shared/ChatProviderTranslator.lua similarity index 100% rename from src/chatproviderservice/src/Client/ChatProviderTranslator.lua rename to src/chatproviderservice/src/Shared/ChatProviderTranslator.lua diff --git a/src/clienttranslator/README.md b/src/clienttranslator/README.md index 3a22684878..f45ff5c14f 100644 --- a/src/clienttranslator/README.md +++ b/src/clienttranslator/README.md @@ -23,6 +23,12 @@ npm install @quenty/clienttranslator --save ## Usage Usage is designed to be simple. +## Easy-use scenario + +1. Call Translate(data, "blah") on anything +2. Translation is magically replicated to clients and can be saved +3. Only one place needed to save the data + ## Adding localization files Add files to ReplicatedStorage/i18n. Files will be in string values, and be valid JSON. This allows lookup like this: diff --git a/src/clienttranslator/package.json b/src/clienttranslator/package.json index 7500f228f4..34fa046ca3 100644 --- a/src/clienttranslator/package.json +++ b/src/clienttranslator/package.json @@ -32,6 +32,7 @@ "@quenty/promise": "file:../promise", "@quenty/pseudolocalize": "file:../pseudolocalize", "@quenty/rx": "file:../rx", + "@quenty/string": "file:../string", "@quenty/table": "file:../table" }, "publishConfig": { diff --git a/src/clienttranslator/src/Shared/JsonToLocalizationTable.lua b/src/clienttranslator/src/Shared/Conversion/JsonToLocalizationTable.lua similarity index 91% rename from src/clienttranslator/src/Shared/JsonToLocalizationTable.lua rename to src/clienttranslator/src/Shared/Conversion/JsonToLocalizationTable.lua index 34ea600b36..64035b27cf 100644 --- a/src/clienttranslator/src/Shared/JsonToLocalizationTable.lua +++ b/src/clienttranslator/src/Shared/Conversion/JsonToLocalizationTable.lua @@ -11,7 +11,8 @@ local RunService = game:GetService("RunService") local JsonToLocalizationTable = {} -local LOCALIZATION_TABLE_NAME = "GeneratedJSONTable" +local LOCALIZATION_TABLE_NAME_CLIENT = "GeneratedJSONTable_Client" +local LOCALIZATION_TABLE_NAME_SERVER = "GeneratedJSONTable_Server" --[[ Recursively iterates through the object to construct strings and add it to the localization table @@ -69,11 +70,18 @@ end @return string -- The locale ]=] function JsonToLocalizationTable.getOrCreateLocalizationTable() - local localizationTable = LocalizationService:FindFirstChild(LOCALIZATION_TABLE_NAME) + local localizationTableName + if RunService:IsServer() then + localizationTableName = LOCALIZATION_TABLE_NAME_SERVER + else + localizationTableName = LOCALIZATION_TABLE_NAME_CLIENT + end + + local localizationTable = LocalizationService:FindFirstChild(localizationTableName) if not localizationTable then localizationTable = Instance.new("LocalizationTable") - localizationTable.Name = LOCALIZATION_TABLE_NAME + localizationTable.Name = localizationTableName if RunService:IsRunning() then localizationTable.Parent = LocalizationService diff --git a/src/clienttranslator/src/Client/JSONTranslator.lua b/src/clienttranslator/src/Shared/JSONTranslator.lua similarity index 89% rename from src/clienttranslator/src/Client/JSONTranslator.lua rename to src/clienttranslator/src/Shared/JSONTranslator.lua index 307b1f5714..2dd7543fa6 100644 --- a/src/clienttranslator/src/Client/JSONTranslator.lua +++ b/src/clienttranslator/src/Shared/JSONTranslator.lua @@ -18,15 +18,16 @@ local require = require(script.Parent.loader).load(script) local Players = game:GetService("Players") local RunService = game:GetService("RunService") +local Blend = require("Blend") local JsonToLocalizationTable = require("JsonToLocalizationTable") -local PseudoLocalize = require("PseudoLocalize") local LocalizationServiceUtils = require("LocalizationServiceUtils") -local Promise = require("Promise") -local Observable = require("Observable") local Maid = require("Maid") -local Blend = require("Blend") +local Observable = require("Observable") +local Promise = require("Promise") +local PseudoLocalize = require("PseudoLocalize") local Rx = require("Rx") local RxInstanceUtils = require("RxInstanceUtils") +local TranslationKeyUtils = require("TranslationKeyUtils") local JSONTranslator = {} JSONTranslator.ClassName = "JSONTranslator" @@ -73,7 +74,7 @@ function JSONTranslator.new(translatorName, ...) self._englishTranslator = self._localizationTable:GetTranslator("en") self._fallbacks = {} - if RunService:IsRunning() then + if RunService:IsRunning() and RunService:IsClient() then self._promiseTranslator = LocalizationServiceUtils.promiseTranslator(Players.LocalPlayer) else self._promiseTranslator = Promise.resolved(self._englishTranslator) @@ -86,6 +87,10 @@ function JSONTranslator.new(translatorName, ...) return self end +function JSONTranslator:Init(serviceBag) + self._serviceBag = assert(serviceBag, "No serviceBag") +end + --[=[ Observes the current locale id for this translator. @@ -126,6 +131,25 @@ function JSONTranslator:SetEntryValue(translationKey, source, context, localeId, end end +function JSONTranslator:ObserveTranslation(prefix, text, argData) + assert(type(prefix) == "string", "Bad text") + assert(type(text) == "string", "Bad text") + + return self:ObserveFormatByKey(self:ToTranslationKey(prefix, text), argData) +end + +function JSONTranslator:ToTranslationKey(prefix, text) + assert(type(prefix) == "string", "Bad text") + assert(type(text) == "string", "Bad text") + + local translationKey = TranslationKeyUtils.getTranslationKey(prefix, text) + local context = ("automatic.%s"):format(translationKey) + + self:SetEntryValue(translationKey, text, context, "en", text) + + return translationKey +end + --[=[ Gets the current localeId of the translator if it's initialized, or a default if it is not. @@ -136,7 +160,7 @@ function JSONTranslator:GetLocaleId() local translator = self._promiseTranslator:Wait() return translator.LocaleId else - warn("[JSONTranslator] - Translator is not loaded yet, returning english") + warn("[JSONTranslator.GetLocaleId] - Translator is not loaded yet, returning english") return "en" end end @@ -218,7 +242,7 @@ function JSONTranslator:ObserveFormatByKey(key, argData) return Observable.new(function(sub) local maid = Maid.new() - maid:GivePromise(self._promiseTranslator:Then(function(translator) + maid:GivePromise(self._promiseTranslator):Then(function(translator) if argObservable then maid:GiveTask(Rx.combineLatest({ localeId = RxInstanceUtils.observeProperty(translator, "LocaleId"); @@ -231,7 +255,7 @@ function JSONTranslator:ObserveFormatByKey(key, argData) sub:Fire(self:FormatByKey(key, nil)) end)) end - end)) + end) return maid end) @@ -322,7 +346,7 @@ function JSONTranslator:_formatByKeyTestMode(key, args) if err then warn(err) else - warn("Failed to localize '" .. key .. "'") + warn("[JSONTranslator._formatByKeyTestMode] - Failed to localize '" .. key .. "'") end return key diff --git a/src/clienttranslator/src/Client/LocalizationServiceUtils.lua b/src/clienttranslator/src/Shared/Utils/LocalizationServiceUtils.lua similarity index 100% rename from src/clienttranslator/src/Client/LocalizationServiceUtils.lua rename to src/clienttranslator/src/Shared/Utils/LocalizationServiceUtils.lua diff --git a/src/clienttranslator/src/Shared/Utils/TranslationKeyUtils.lua b/src/clienttranslator/src/Shared/Utils/TranslationKeyUtils.lua new file mode 100644 index 0000000000..00716045d2 --- /dev/null +++ b/src/clienttranslator/src/Shared/Utils/TranslationKeyUtils.lua @@ -0,0 +1,18 @@ +--[=[ + @class TranslationKeyUtils +]=] + +local require = require(script.Parent.loader).load(script) + +local String = require("String") + +local TranslationKeyUtils = {} + +function TranslationKeyUtils.getTranslationKey(prefix, text) + local firstWordsBeginning = string.sub(string.gsub(text, "%s", ""), 1, 20) + local firstWords = String.toLowerCamelCase(firstWordsBeginning) + + return prefix .. "." .. firstWords +end + +return TranslationKeyUtils \ No newline at end of file diff --git a/src/color3utils/package.json b/src/color3utils/package.json index e8123d8927..445860b032 100644 --- a/src/color3utils/package.json +++ b/src/color3utils/package.json @@ -30,6 +30,7 @@ "dependencies": { "@quenty/blend": "file:../blend", "@quenty/loader": "file:../loader", + "@quenty/maid": "file:../maid", "@quenty/math": "file:../math", "@quenty/rx": "file:../rx", "@quenty/valueobject": "file:../valueobject" diff --git a/src/color3utils/src/Shared/luv/LuvColor3Utils.lua b/src/color3utils/src/Shared/luv/LuvColor3Utils.lua index 1f4ef1bb1c..b4153f7ecc 100644 --- a/src/color3utils/src/Shared/luv/LuvColor3Utils.lua +++ b/src/color3utils/src/Shared/luv/LuvColor3Utils.lua @@ -33,7 +33,8 @@ function LuvColor3Utils.lerp(color0, color1, t) local l0, u0, v0 = unpack(LuvColor3Utils.fromColor3(color0)) local l1, u1, v1 = unpack(LuvColor3Utils.fromColor3(color1)) - local l = Math.lerp(l0, l1, t) + local shortest_angle = ((((l1 - l0) % 360) + 540) % 360) - 180 + local l = l0 + shortest_angle*t local u = Math.lerp(u0, u1, t) local v = Math.lerp(v0, v1, t) @@ -41,6 +42,16 @@ function LuvColor3Utils.lerp(color0, color1, t) end end +function LuvColor3Utils.desaturate(color0, proportion) + local l0, u0, v0 = unpack(LuvColor3Utils.fromColor3(color0)) + return LuvColor3Utils.toColor3({l0, u0*math.clamp(1 - proportion, 0, 1), v0}) +end + +function LuvColor3Utils.darken(color0, proportion) + local l0, u0, v0 = unpack(LuvColor3Utils.fromColor3(color0)) + return LuvColor3Utils.toColor3({l0, u0, v0*math.clamp(1 - proportion, 0, 1)}) +end + --[=[ Converts from Color3 to LUV @param color3 Color3 diff --git a/src/color3utils/src/Shared/luv/LuvColor3Utils.story.lua b/src/color3utils/src/Shared/luv/LuvColor3Utils.story.lua new file mode 100644 index 0000000000..b1c2015185 --- /dev/null +++ b/src/color3utils/src/Shared/luv/LuvColor3Utils.story.lua @@ -0,0 +1,39 @@ +--[[ + @class LuvColor3Utils.story +]] + +local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script) + +local Maid = require("Maid") +local LuvColor3Utils = require("LuvColor3Utils") + +return function(target) + local maid = Maid.new() + + local start = Color3.fromRGB(184, 127, 100) + local finish = Color3.fromRGB(16, 60, 76) + + for i=0, 100 do + local frame = Instance.new("Frame") + frame.BackgroundColor3 = LuvColor3Utils.lerp(start, finish, i/100) + frame.Size = UDim2.fromScale(1/(100 + 1), 0.5) + frame.Position = UDim2.fromScale(i/(100 + 1), 0) + frame.BorderSizePixel = 0 + frame.Parent = target + maid:GiveTask(frame) + end + + for i=0, 100 do + local frame = Instance.new("Frame") + frame.BackgroundColor3 = start:Lerp(finish, i/100) + frame.Size = UDim2.fromScale(1/(100 + 1), 0.5) + frame.Position = UDim2.fromScale(i/(100 + 1), 0.5) + frame.BorderSizePixel = 0 + frame.Parent = target + maid:GiveTask(frame) + end + + return function() + maid:DoCleaning() + end +end \ No newline at end of file diff --git a/src/colorpalette/src/Shared/ColorPalette.lua b/src/colorpalette/src/Shared/ColorPalette.lua index eaf4199698..a0b27ce491 100644 --- a/src/colorpalette/src/Shared/ColorPalette.lua +++ b/src/colorpalette/src/Shared/ColorPalette.lua @@ -184,7 +184,7 @@ function ColorPalette:_toGradeObservable(grade, fallbackColorSource) end end -function ColorPalette:_toVividnessObservable(vividness, grade, fallbackColorSource) +function ColorPalette:_toVividnessObservable(vividness, grade, colorOrObservable) if type(vividness) == "string" then return self._gradePalette:ObserveVividness(vividness) elseif type(vividness) == "number" then @@ -199,25 +199,14 @@ function ColorPalette:_toVividnessObservable(vividness, grade, fallbackColorSour end if type(grade) == "string" then - -- Fall back to the grade value + -- Fall back to the grade value's vividness return self._gradePalette:ObserveVividness(grade) - end - - -- Otherwise fall back to name of color - if type(fallbackColorSource) == "string" then - return (self._gradePalette:ObserveVividness(fallbackColorSource)) - elseif typeof(fallbackColorSource) == "Color3" then - local luvColor = LuvColor3Utils.fromColor3(fallbackColorSource) - return Rx.of(luvColor[2]) - elseif Observable.isObservable(fallbackColorSource) then - return fallbackColorSource:Pipe({ - Rx.map(function(value) - local luvColor = LuvColor3Utils.fromColor3(value) - return luvColor[2] - end) - }) + elseif type(colorOrObservable) == "string" then + -- Fall back to color + return self._gradePalette:ObserveVividness(colorOrObservable) else - error("Bad fallbackColorSource argument") + -- Vividness is pretty optional + return Rx.of(nil) end end @@ -234,8 +223,8 @@ end function ColorPalette:_toVividness(vividness, grade, name) if type(vividness) == "string" then return self._gradePalette:GetVividness(vividness) - elseif type(grade) == "number" then - return grade + elseif type(vividness) == "number" then + return vividness elseif type(grade) == "string" then -- Fall back to the grade value return self._gradePalette:GetVividness(grade) diff --git a/src/colorpalette/src/Shared/ColorPalette.story.lua b/src/colorpalette/src/Shared/ColorPalette.story.lua index c49dec6048..b1405c3941 100644 --- a/src/colorpalette/src/Shared/ColorPalette.story.lua +++ b/src/colorpalette/src/Shared/ColorPalette.story.lua @@ -10,7 +10,7 @@ local ColorPickerStoryUtils = require("ColorPickerStoryUtils") local ColorPalette = require("ColorPalette") local ValueObject = require("ValueObject") -local DARK_MODE_ENABLED = false +local DARK_MODE_ENABLED = true return function(target) local maid = Maid.new() @@ -53,12 +53,14 @@ return function(target) palette:SetColorGrade("highlight", 70, 1) palette:SetColorGrade("action", palette:ObserveColorBaseGradeBetween("action", 70, 100), 0.1) palette:SetColorGrade("mouseOver", -15) + palette:SetVividness("text", 0.5) + palette:SetVividness("action", 0.5) end if DARK_MODE_ENABLED then - light() - else dark() + else + light() end local function sampleGui(labelText) @@ -225,6 +227,12 @@ return function(target) Blend.New "UIPadding" { PaddingTop = UDim.new(0, 10); }; + + Blend.New "Frame" { + Name = "TestCustomColor"; + Size = UDim2.new(0, 30, 0, 30); + BackgroundColor3 = palette:ObserveColor(Color3.new(0, 0, 1), "text"); + }; } }):Subscribe()) diff --git a/src/colorpalette/src/Shared/Grade/ColorGradePalette.lua b/src/colorpalette/src/Shared/Grade/ColorGradePalette.lua index 94febc8ef2..72a55395ac 100644 --- a/src/colorpalette/src/Shared/Grade/ColorGradePalette.lua +++ b/src/colorpalette/src/Shared/Grade/ColorGradePalette.lua @@ -167,7 +167,17 @@ function ColorGradePalette:_observeGradeFromName(gradeName) if typeof(gradeName) == "Color3" then return Rx.of(ColorGradeUtils.getGrade(gradeName)) elseif Observable.isObservable(gradeName) then - return gradeName + return gradeName:Pipe({ + Rx.map(function(value) + if typeof(value) == "Color3" then + return ColorGradeUtils.getGrade(value) + elseif typeof(value) == "number" then + return value + else + error("Bad grade value") + end + end) + }) end local gradeObservable = self._grades[gradeName] diff --git a/src/cooldown/src/Shared/Model/CooldownTrackerModel.lua b/src/cooldown/src/Shared/Model/CooldownTrackerModel.lua index a4b3b375de..1db2345ea1 100644 --- a/src/cooldown/src/Shared/Model/CooldownTrackerModel.lua +++ b/src/cooldown/src/Shared/Model/CooldownTrackerModel.lua @@ -6,6 +6,7 @@ local require = require(script.Parent.loader).load(script) local BaseObject = require("BaseObject") local ValueObject = require("ValueObject") +local DuckTypeUtils = require("DuckTypeUtils") local CooldownTrackerModel = setmetatable({}, BaseObject) CooldownTrackerModel.ClassName = "CooldownTrackerModel" @@ -34,6 +35,10 @@ function CooldownTrackerModel.new() return self end +function CooldownTrackerModel.isCooldownTrackerModel(value) + return DuckTypeUtils.isImplementation(CooldownTrackerModel, value) +end + function CooldownTrackerModel:IsCoolingDown() return self._currentCooldownModel.Value ~= nil end diff --git a/src/counter/README.md b/src/counter/README.md new file mode 100644 index 0000000000..52d9c980be --- /dev/null +++ b/src/counter/README.md @@ -0,0 +1,23 @@ +## Counter + + + +Helps count a total value + + + +## Installation + +``` +npm install @quenty/counter --save +``` diff --git a/src/counter/default.project.json b/src/counter/default.project.json new file mode 100644 index 0000000000..637543f9c5 --- /dev/null +++ b/src/counter/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "counter", + "tree": { + "$path": "src" + } +} diff --git a/src/counter/package.json b/src/counter/package.json new file mode 100644 index 0000000000..6a8c821ffd --- /dev/null +++ b/src/counter/package.json @@ -0,0 +1,36 @@ +{ + "name": "@quenty/counter", + "version": "1.0.0", + "description": "Helps count a total value", + "keywords": [ + "Roblox", + "Nevermore", + "Lua", + "counter" + ], + "bugs": { + "url": "https://github.com/Quenty/NevermoreEngine/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/Quenty/NevermoreEngine.git", + "directory": "src/counter/" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/quenty" + }, + "license": "MIT", + "contributors": [ + "Quenty" + ], + "dependencies": { + "@quenty/loader": "file:../loader", + "@quenty/maid": "file:../maid", + "@quenty/rx": "file:../rx", + "@quenty/valueobject": "file:../valueobject" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/src/counter/src/Shared/Counter.lua b/src/counter/src/Shared/Counter.lua new file mode 100644 index 0000000000..4994f578d1 --- /dev/null +++ b/src/counter/src/Shared/Counter.lua @@ -0,0 +1,105 @@ +--[=[ + @class Counter +]=] + +local require = require(script.Parent.loader).load(script) + +local BaseObject = require("BaseObject") +local Maid = require("Maid") +local Observable = require("Observable") +local ValueObject = require("ValueObject") + +local Counter = setmetatable({}, BaseObject) +Counter.ClassName = "Counter" +Counter.__index = Counter + +--[=[ + Creates a new counter + + @return Counter +]=] +function Counter.new() + local self = setmetatable(BaseObject.new(), Counter) + + self._count = self._maid:Add(ValueObject.new(0, "number")) + + self.Changed = assert(self._count.Changed, "Bad .Changed") + + return self +end + + +--[=[ + Returns the current count + + @return number +]=] +function Counter:GetValue() + return self._count.Value +end + +--[=[ + Observes the current count + + @return number +]=] +function Counter:Observe() + return self._count:Observe() +end + + +--[=[ + Adds an amount to the counter. + + @param amount number | Observable + @return MaidTask +]=] +function Counter:Add(amount) + if type(amount) == "number" then + self._count.Value = self._count.Value + amount + + return function() + self._count.Value = self._count.Value - amount + end + elseif Observable.isObservable(amount) then + return self:_addObservable(amount) + else + error("Bad amount") + end +end + +function Counter:_addObservable(observeAmount) + assert(Observable.isObservable(observeAmount), "Bad observeAmount") + + local lastCount = 0 + + local maid = Maid.new() + maid:GiveTask(observeAmount:Subscribe(function(count) + assert(type(count) == "number", "Bad count") + local delta = count - lastCount + lastCount = count + + self._count.Value = self._count.Value + delta + end)) + + maid:GiveTask(function() + if not self._count.Destroy then + return + end + + local delta = lastCount + lastCount = 0 + self._count.Value = self._count.Value - delta + end) + + self._maid[maid] = maid + maid:GiveTask(function() + self._maid[maid] = nil + end) + + return function() + self._maid[maid] = nil + end +end + +return Counter \ No newline at end of file diff --git a/src/counter/src/node_modules.project.json b/src/counter/src/node_modules.project.json new file mode 100644 index 0000000000..46233dac4f --- /dev/null +++ b/src/counter/src/node_modules.project.json @@ -0,0 +1,7 @@ +{ + "name": "node_modules", + "globIgnorePaths": [ "**/.package-lock.json" ], + "tree": { + "$path": { "optional": "../node_modules" } + } +} \ No newline at end of file diff --git a/src/counter/test/default.project.json b/src/counter/test/default.project.json new file mode 100644 index 0000000000..0ede9ed78a --- /dev/null +++ b/src/counter/test/default.project.json @@ -0,0 +1,11 @@ +{ + "name": "CounterTest", + "tree": { + "$className": "DataModel", + "ServerScriptService": { + "counter": { + "$path": ".." + } + } + } +} \ No newline at end of file diff --git a/src/gameconfig/src/Client/Config/Asset/GameConfigAssetClient.lua b/src/gameconfig/src/Client/Config/Asset/GameConfigAssetClient.lua index 72364c38d8..8592898611 100644 --- a/src/gameconfig/src/Client/Config/Asset/GameConfigAssetClient.lua +++ b/src/gameconfig/src/Client/Config/Asset/GameConfigAssetClient.lua @@ -19,14 +19,11 @@ GameConfigAssetClient.__index = GameConfigAssetClient @return GameConfigAssetClient ]=] function GameConfigAssetClient.new(obj, serviceBag) - local self = setmetatable(GameConfigAssetBase.new(obj), GameConfigAssetClient) + local self = setmetatable(GameConfigAssetBase.new(obj, serviceBag), GameConfigAssetClient) self._serviceBag = assert(serviceBag, "No serviceBag") self._configTranslator = self._serviceBag:GetService(GameConfigTranslator) - self:_setupEntrySet(self:ObserveNameTranslationKey(), self:ObserveCloudName()) - self:_setupEntrySet(self:ObserveDescriptionTranslationKey(), self:ObserveCloudDescription()) - return self end @@ -51,28 +48,4 @@ function GameConfigAssetClient:_setupEntrySet(observeTranslationKey, observeTran end)) end ---[=[ - Observes the translated name - @return Observable -]=] -function GameConfigAssetClient:ObserveTranslatedName() - return self:ObserveNameTranslationKey():Pipe({ - Rx.switchMap(function(key) - return self._configTranslator:ObserveFormatByKey(key) - end) - }) -end - ---[=[ - Observes the translated description - @return Observable -]=] -function GameConfigAssetClient:ObserveTranslatedDescription() - return self:ObserveDescriptionTranslationKey():Pipe({ - Rx.switchMap(function(key) - return self._configTranslator:ObserveFormatByKey(key) - end) - }) -end - return GameConfigAssetClient \ No newline at end of file diff --git a/src/gameconfig/src/Server/Config/Asset/GameConfigAsset.lua b/src/gameconfig/src/Server/Config/Asset/GameConfigAsset.lua index 3e34ff3d06..85b198a53d 100644 --- a/src/gameconfig/src/Server/Config/Asset/GameConfigAsset.lua +++ b/src/gameconfig/src/Server/Config/Asset/GameConfigAsset.lua @@ -5,15 +5,44 @@ local require = require(script.Parent.loader).load(script) local GameConfigAssetBase = require("GameConfigAssetBase") +local GameConfigTranslator = require("GameConfigTranslator") +local Rx = require("Rx") local GameConfigAsset = setmetatable({}, GameConfigAssetBase) GameConfigAsset.ClassName = "GameConfigAsset" GameConfigAsset.__index = GameConfigAsset function GameConfigAsset.new(obj, serviceBag) - local self = setmetatable(GameConfigAssetBase.new(obj), GameConfigAsset) + local self = setmetatable(GameConfigAssetBase.new(obj, serviceBag), GameConfigAsset) self._serviceBag = assert(serviceBag, "No serviceBag") + self._translator = self._serviceBag:GetService(GameConfigTranslator) + + self._maid:GiveTask(Rx.combineLatest({ + assetKey = self:ObserveAssetKey(); + assetType = self:ObserveAssetType(); + text = self:ObserveCloudName(); + }):Subscribe(function(state) + if state.text then + local prefix = string.format("assets.%s.%s.name", state.assetType, state.assetKey) + self:SetNameTranslationKey(self._translator:ToTranslationKey(prefix, state.text)) + else + self:SetNameTranslationKey(nil) + end + end)) + + self._maid:GiveTask(Rx.combineLatest({ + assetKey = self:ObserveAssetKey(); + assetType = self:ObserveAssetType(); + text = self:ObserveCloudDescription(); + }):Subscribe(function(state) + if state.text then + local prefix = string.format("assets.%s.%s.description", state.assetType, state.assetKey) + self:SetDescriptionTranslationKey(self._translator:ToTranslationKey(prefix, state.text)) + else + self:SetDescriptionTranslationKey(nil) + end + end)) return self end diff --git a/src/gameconfig/src/Server/GameConfigService.lua b/src/gameconfig/src/Server/GameConfigService.lua index cdb50fe0f6..cf041ea162 100644 --- a/src/gameconfig/src/Server/GameConfigService.lua +++ b/src/gameconfig/src/Server/GameConfigService.lua @@ -32,6 +32,7 @@ function GameConfigService:Init(serviceBag) -- Internal self._serviceBag:GetService(require("GameConfigCommandService")) + self._serviceBag:GetService(require("GameConfigTranslator")) self._binders = self._serviceBag:GetService(require("GameConfigBindersServer")) -- Setup picker diff --git a/src/gameconfig/src/Shared/Config/Asset/GameConfigAssetBase.lua b/src/gameconfig/src/Shared/Config/Asset/GameConfigAssetBase.lua index 5a3b1371b9..a3369e91fa 100644 --- a/src/gameconfig/src/Shared/Config/Asset/GameConfigAssetBase.lua +++ b/src/gameconfig/src/Shared/Config/Asset/GameConfigAssetBase.lua @@ -11,6 +11,8 @@ local RxInstanceUtils = require("RxInstanceUtils") local GameConfigAssetConstants = require("GameConfigAssetConstants") local Promise = require("Promise") local GameConfigAssetUtils = require("GameConfigAssetUtils") +local AttributeValue = require("AttributeValue") +local GameConfigTranslator = require("GameConfigTranslator") local GameConfigAssetBase = setmetatable({}, BaseObject) GameConfigAssetBase.ClassName = "GameConfigAssetBase" @@ -19,14 +21,57 @@ GameConfigAssetBase.__index = GameConfigAssetBase --[=[ Constructs a new GameConfigAssetBase. Should be done via binder. This is a base class. @param obj Folder + @param serviceBag ServiceBag @return GameConfigAssetBase ]=] -function GameConfigAssetBase.new(obj) +function GameConfigAssetBase.new(obj, serviceBag) local self = setmetatable(BaseObject.new(obj), GameConfigAssetBase) + self._serviceBag = assert(serviceBag, "No serviceBag") + self._nameTranslationKey = AttributeValue.new(self._obj, "NameTranslationKey", "assets.name.unknown") + self._descriptionTranslationKey = AttributeValue.new(self._obj, "DescriptionTranslationKey", "assets.description.unknown") + self._configTranslator = self._serviceBag:GetService(GameConfigTranslator) + return self end + +--[=[ + Observes the translated name + @return Observable +]=] +function GameConfigAssetBase:ObserveTranslatedName() + return self:ObserveNameTranslationKey():Pipe({ + Rx.switchMap(function(key) + return self._configTranslator:ObserveFormatByKey(key) + end) + }) +end + +--[=[ + Observes the translated description + @return Observable +]=] +function GameConfigAssetBase:ObserveTranslatedDescription() + return self:ObserveDescriptionTranslationKey():Pipe({ + Rx.switchMap(function(key) + return self._configTranslator:ObserveFormatByKey(key) + end) + }) +end + +function GameConfigAssetBase:SetNameTranslationKey(nameTranslationKey) + assert(type(nameTranslationKey) == "string" or nameTranslationKey == nil, "Bad nameTranslationKey") + + self._nameTranslationKey.Value = nameTranslationKey or "assets.name.unknown" +end + +function GameConfigAssetBase:SetDescriptionTranslationKey(descriptionTranslationKey) + assert(type(descriptionTranslationKey) == "string" or descriptionTranslationKey == nil, "Bad descriptionTranslationKey") + + self._descriptionTranslationKey.Value = descriptionTranslationKey or "assets.description.unknown" +end + --[=[ Gets the asset id @return number @@ -127,7 +172,7 @@ end @return Observable ]=] function GameConfigAssetBase:ObserveNameTranslationKey() - return self:_observeTranslationKey("name") + return self._nameTranslationKey:Observe() end --[=[ @@ -135,7 +180,7 @@ end @return Observable ]=] function GameConfigAssetBase:ObserveDescriptionTranslationKey() - return self:_observeTranslationKey("description") + return self._descriptionTranslationKey:Observe() end --[=[ @@ -171,18 +216,6 @@ function GameConfigAssetBase:ObserveCloudIconImageAssetId() return self:_observeCloudProperty("IconImageAssetId", "number") end -function GameConfigAssetBase:_observeTranslationKey(postfix) - return self:ObserveState():Pipe({ - Rx.map(function(state) - if type(state) == "table" and type(state.assetType) == "string" and type(state.assetKey) == "string" then - return ("cloud.%s.%s.%s"):format(state.assetType, state.assetKey, postfix) - else - return nil - end - end) - }) -end - function GameConfigAssetBase:_observeCloudProperty(propertyName, expectedType) assert(type(propertyName) == "string", "Bad propertyName") assert(type(expectedType) == "string", "Bad expectedType") diff --git a/src/gameconfig/src/Shared/Config/Config/GameConfigBase.lua b/src/gameconfig/src/Shared/Config/Config/GameConfigBase.lua index f731f64e50..2ea6f35eb2 100644 --- a/src/gameconfig/src/Shared/Config/Config/GameConfigBase.lua +++ b/src/gameconfig/src/Shared/Config/Config/GameConfigBase.lua @@ -4,18 +4,17 @@ local require = require(script.Parent.loader).load(script) +local AttributeValue = require("AttributeValue") local BaseObject = require("BaseObject") -local RxAttributeUtils = require("RxAttributeUtils") -local AttributeUtils = require("AttributeUtils") +local GameConfigAssetTypes = require("GameConfigAssetTypes") +local GameConfigAssetTypeUtils = require("GameConfigAssetTypeUtils") local GameConfigConstants = require("GameConfigConstants") -local RxInstanceUtils = require("RxInstanceUtils") local GameConfigUtils = require("GameConfigUtils") +local ObservableMapSet = require("ObservableMapSet") +local Rx = require("Rx") local RxBinderUtils = require("RxBinderUtils") -local GameConfigAssetTypes = require("GameConfigAssetTypes") local RxBrioUtils = require("RxBrioUtils") -local Rx = require("Rx") -local ObservableMapSet = require("ObservableMapSet") -local GameConfigAssetTypeUtils = require("GameConfigAssetTypeUtils") +local RxInstanceUtils = require("RxInstanceUtils") local GameConfigBase = setmetatable({}, BaseObject) GameConfigBase.ClassName = "GameConfigBase" @@ -29,17 +28,12 @@ GameConfigBase.__index = GameConfigBase function GameConfigBase.new(folder: Instance) local self = setmetatable(BaseObject.new(folder), GameConfigBase) - AttributeUtils.initAttribute(self._obj, GameConfigConstants.GAME_ID_ATTRIBUTE, game.GameId) + self._gameId = AttributeValue.new(self._obj, GameConfigConstants.GAME_ID_ATTRIBUTE, game.GameId) -- Setup observable indexes - self._assetTypeToAssetConfig = ObservableMapSet.new() - self._maid:GiveTask(self._assetTypeToAssetConfig) - - self._assetKeyToAssetConfig = ObservableMapSet.new() - self._maid:GiveTask(self._assetKeyToAssetConfig) - - self._assetIdToAssetConfig = ObservableMapSet.new() - self._maid:GiveTask(self._assetIdToAssetConfig) + self._assetTypeToAssetConfig = self._maid:Add(ObservableMapSet.new()) + self._assetKeyToAssetConfig = self._maid:Add(ObservableMapSet.new()) + self._assetIdToAssetConfig = self._maid:Add(ObservableMapSet.new()) self._assetTypeToAssetKeyMappings = {} self._assetTypeToAssetIdMappings = {} @@ -215,7 +209,7 @@ end @return Observable ]=] function GameConfigBase:ObserveGameId() - return RxAttributeUtils.observeAttribute(self._obj, GameConfigConstants.GAME_ID_ATTRIBUTE, game.GameId) + return self._gameId:Observe() end --[=[ @@ -223,7 +217,7 @@ end @return number ]=] function GameConfigBase:GetGameId() - return AttributeUtils.getAttribute(self._obj, GameConfigConstants.GAME_ID_ATTRIBUTE, game.GameId) + return self._gameId.Value end --[=[ diff --git a/src/gameconfig/src/Shared/Config/Picker/GameConfigPicker.lua b/src/gameconfig/src/Shared/Config/Picker/GameConfigPicker.lua index ac61b65fc9..777cc949e0 100644 --- a/src/gameconfig/src/Shared/Config/Picker/GameConfigPicker.lua +++ b/src/gameconfig/src/Shared/Config/Picker/GameConfigPicker.lua @@ -29,8 +29,7 @@ function GameConfigPicker.new(gameConfigBinder, gameConfigAssetBinder) self._gameConfigBinder = assert(gameConfigBinder, "No gameConfigBinder") self._gameConfigAssetBinder = assert(gameConfigAssetBinder, "No gameConfigAssetBinder") - self._gameIdToConfigSet = ObservableMapSet.new() - self._maid:GiveTask(self._gameIdToConfigSet) + self._gameIdToConfigSet = self._maid:Add(ObservableMapSet.new()) self._maid:GiveTask(RxBinderUtils.observeAllBrio(self._gameConfigBinder) :Subscribe(function(brio) diff --git a/src/gameconfig/src/Client/GameConfigTranslator.lua b/src/gameconfig/src/Shared/GameConfigTranslator.lua similarity index 66% rename from src/gameconfig/src/Client/GameConfigTranslator.lua rename to src/gameconfig/src/Shared/GameConfigTranslator.lua index 7c2944c4a8..f263c2f752 100644 --- a/src/gameconfig/src/Client/GameConfigTranslator.lua +++ b/src/gameconfig/src/Shared/GameConfigTranslator.lua @@ -5,4 +5,13 @@ local require = require(script.Parent.loader).load(script) -return require("JSONTranslator").new("GameConfigTranslator", "en", {}) \ No newline at end of file +return require("JSONTranslator").new("GameConfigTranslator", "en", { + assetKeys = { + name = { + unknown = "???"; + }; + description = { + unknown = "???"; + }; + }; +}) \ No newline at end of file diff --git a/src/gameproductservice/package.json b/src/gameproductservice/package.json index 23ada33510..d6fd4afd95 100644 --- a/src/gameproductservice/package.json +++ b/src/gameproductservice/package.json @@ -40,6 +40,7 @@ "@quenty/observablecollection": "file:../observablecollection", "@quenty/playerbinder": "file:../playerbinder", "@quenty/promise": "file:../promise", + "@quenty/promisemaid": "file:../promisemaid", "@quenty/receiptprocessing": "file:../receiptprocessing", "@quenty/remoting": "file:../remoting", "@quenty/rx": "file:../rx", diff --git a/src/gameproductservice/src/Client/Manager/PlayerProductManagerClient.lua b/src/gameproductservice/src/Client/Manager/PlayerProductManagerClient.lua index e640106b58..3b51d24bc4 100644 --- a/src/gameproductservice/src/Client/Manager/PlayerProductManagerClient.lua +++ b/src/gameproductservice/src/Client/Manager/PlayerProductManagerClient.lua @@ -26,11 +26,8 @@ function PlayerProductManagerClient.new(obj, serviceBag) self._gameConfigServiceClient = self._serviceBag:GetService(GameConfigServiceClient) if self._obj == Players.LocalPlayer then - self._remoting = Remoting.new(self._obj, "PlayerProductManager") - self._maid:GiveTask(self._remoting) - - self._marketeer = PlayerMarketeer.new(self._obj, self._gameConfigServiceClient:GetConfigPicker()) - self._maid:GiveTask(self._marketeer) + self._remoting = self._maid:Add(Remoting.new(self._obj, "PlayerProductManager")) + self._marketeer = self._maid:Add(PlayerMarketeer.new(self._obj, self._gameConfigServiceClient:GetConfigPicker())) self._marketeer:GetAssetTrackerOrError(GameConfigAssetTypes.PRODUCT):SetReceiptProcessingExpected(true) diff --git a/src/gameproductservice/src/Server/Manager/PlayerProductManager.lua b/src/gameproductservice/src/Server/Manager/PlayerProductManager.lua index f0304613d0..d3a2e83abf 100644 --- a/src/gameproductservice/src/Server/Manager/PlayerProductManager.lua +++ b/src/gameproductservice/src/Server/Manager/PlayerProductManager.lua @@ -34,15 +34,13 @@ function PlayerProductManager.new(player, serviceBag) self._gameConfigService = self._serviceBag:GetService(GameConfigService) self._receiptProcessingService = self._serviceBag:GetService(ReceiptProcessingService) - self._marketeer = PlayerMarketeer.new(self._obj, self._gameConfigService:GetConfigPicker()) - self._maid:GiveTask(self._marketeer) + self._marketeer = self._maid:Add(PlayerMarketeer.new(self._obj, self._gameConfigService:GetConfigPicker())) -- Expect configuration on receipt processing self._marketeer:GetAssetTrackerOrError(GameConfigAssetTypes.PRODUCT):SetReceiptProcessingExpected(true) - self._remoting = Remoting.new(self._obj, "PlayerProductManager") + self._remoting = self._maid:Add(Remoting.new(self._obj, "PlayerProductManager")) self._remoting:DeclareEvent("NotifyReceiptProcessed") - self._maid:GiveTask(self._remoting) self._maid:GiveTask(self._remoting.NotifyPromptFinished:Connect(function(...) self:_handlePromptFinished(...) diff --git a/src/gameproductservice/src/Shared/Trackers/PlayerAssetMarketTracker.lua b/src/gameproductservice/src/Shared/Trackers/PlayerAssetMarketTracker.lua index 9f947b5a95..1a5570bfbb 100644 --- a/src/gameproductservice/src/Shared/Trackers/PlayerAssetMarketTracker.lua +++ b/src/gameproductservice/src/Shared/Trackers/PlayerAssetMarketTracker.lua @@ -32,7 +32,9 @@ function PlayerAssetMarketTracker.new(assetType, convertIds, observeIdsBrio) self._convertIds = assert(convertIds, "No convertIds") self._observeIdsBrio = assert(observeIdsBrio, "No observeIdsBrio") - self._pendingPromises = {} -- { [number] = Promise } + self._pendingPurchasePromises = {} -- { [number] = Promise } + self._pendingPromptOpenPromises = {} -- { [number] = Promise } + self._purchasedThisSession = {} -- [number] = true self._receiptProcessingExpected = false @@ -49,9 +51,18 @@ function PlayerAssetMarketTracker.new(assetType, convertIds, observeIdsBrio) self.ShowPromptRequested = Signal.new() -- :Fire(id) self._maid:GiveTask(self.ShowPromptRequested) + self._maid:GiveTask(self.Purchased:Connect(function(id) + self._purchasedThisSession[id] = true + end)) + self._maid:GiveTask(function() - while #self._purchasedThisSession > 0 do - local pending = table.remove(self._purchasedThisSession, #self._purchasedThisSession) + while #self._pendingPurchasePromises > 0 do + local pending = table.remove(self._pendingPurchasePromises, #self._pendingPurchasePromises) + pending:Reject() + end + + while #self._pendingPromptOpenPromises > 0 do + local pending = table.remove(self._pendingPromptOpenPromises, #self._pendingPromptOpenPromises) pending:Reject() end end) @@ -112,41 +123,67 @@ function PlayerAssetMarketTracker:PromisePromptPurchase(idOrKey) local id = self._convertIds(idOrKey) if not id then - return Promise.rejected(("No %s with key %q"):format(self._assetType, tostring(idOrKey))) + return Promise.rejected(string.format("[PlayerAssetMarketTracker.PromisePromptPurchase] - No %s with key %q", + self._assetType, + tostring(idOrKey))) end - if self._pendingPromises[id] then - return self._maid:GivePromise(self._pendingPromises[id]) - end + return Promise.resolved() + :Then(function() + if self._ownershipTracker then + return self._maid:GivePromise(self._ownershipTracker:PromiseOwnsAsset(id)) + else + return false + end + end) + :Then(function(ownsAsset) + if ownsAsset then + return true + end - local ownershipPromise - if self._ownershipTracker then - ownershipPromise = self._ownershipTracker:PromiseOwnsAsset(id) - else - ownershipPromise = Promise.resolved(false) - end + -- We reject here because there's no safe way to queue this + if self._promptsOpen.Value > 0 then + return Promise.rejected(string.format("[PlayerAssetMarketTracker] - Either already prompting user, or prompting is on cooldown. Will not prompt for %s", idOrKey)) + end - return ownershipPromise:Then(function(ownsAsset) - if ownsAsset then - return true - end + -- We reject here because there's no safe way to queue this + if self._pendingPurchasePromises[id] then + return Promise.rejected(string.format("[PlayerAssetMarketTracker] - Already prompting user. Will not prompt for %s", idOrKey)) + end - local promise = Promise.new() - self._pendingPromises[id] = promise + if self._pendingPromptOpenPromises[id] then + warn("[PlayerAssetMarketTracker] - Failure. Prompts open should be tracking this.") - self._promptsOpen.Value = self._promptsOpen.Value + 1 + return Promise.rejected(string.format("[PlayerAssetMarketTracker] - Already prompting user. Will not prompt for %s", idOrKey)) + end - promise:Finally(function() - if self._pendingPromises[id] == promise then - self._pendingPromises[id] = nil + do + local promptOpenPromise = Promise.new() + self._pendingPromptOpenPromises[id] = promptOpenPromise + + self._promptsOpen.Value = self._promptsOpen.Value + 1 + promptOpenPromise:Finally(function() + if self._pendingPromptOpenPromises[id] == promptOpenPromise then + self._pendingPromptOpenPromises[id] = nil + end + self._promptsOpen.Value = self._promptsOpen.Value - 1 + end) end - self._promptsOpen.Value = self._promptsOpen.Value - 1 - end) - self.ShowPromptRequested:Fire(id) + -- Make sure to do promise here so we can't double-open prompts + local purchasePromise = Promise.new() + self._pendingPurchasePromises[id] = purchasePromise - return self._maid:GivePromise(promise) - end) + purchasePromise:Finally(function() + if self._pendingPurchasePromises[id] == purchasePromise then + self._pendingPurchasePromises[id] = nil + end + end) + + self.ShowPromptRequested:Fire(id) + + return self._maid:GivePromise(purchasePromise) + end) end --[=[ @@ -208,35 +245,33 @@ function PlayerAssetMarketTracker:_handlePurchaseEvent(id, isPurchased, isFromRe assert(type(id) == "number", "Bad id") assert(type(isPurchased) == "boolean", "Bad isPurchased") - local promise = self._pendingPromises[id] + local purchasePromise = self._pendingPurchasePromises[id] or Promise.new() + local promptOpenPromise = self._pendingPromptOpenPromises[id] or Promise.new() - -- Zero out promise resolution in receipt processing scenario (safety) if self._receiptProcessingExpected then - if isPurchased and not isFromReceipt then - promise = nil - end - end - - if isPurchased then - self._purchasedThisSession[id] = true - - if self._receiptProcessingExpected then + if isPurchased then + -- In this scenario we've got two possible purchase scenarios, resolving different promises. + -- We expect the event here to be fired twice. if isFromReceipt then self.Purchased:Fire(id) + purchasePromise:Resolve(true) + else + self.PromptFinished:Fire(id, true) + promptOpenPromise:Resolve(true) end else + self.PromptFinished:Fire(id, false) + purchasePromise:Resolve(false) + promptOpenPromise:Resolve(false) + end + else + if isPurchased then self.Purchased:Fire(id) end - end - if not isFromReceipt then self.PromptFinished:Fire(id, isPurchased) - end - - if promise then - task.spawn(function() - promise:Resolve(isPurchased) - end) + purchasePromise:Resolve(isPurchased) + promptOpenPromise:Resolve(isPurchased) end end diff --git a/src/geometryutils/src/Shared/CircleUtils.lua b/src/geometryutils/src/Shared/CircleUtils.lua index af756cdc96..a71176a32d 100644 --- a/src/geometryutils/src/Shared/CircleUtils.lua +++ b/src/geometryutils/src/Shared/CircleUtils.lua @@ -16,7 +16,7 @@ local CircleUtils = {} function CircleUtils.updatePositionToSmallestDistOnCircle(position, target, circumference) assert(target >= 0 and target <= circumference, "Target must be between 0 and circumference") - if math.abs(position - target) <= math.pi then + if math.abs(position - target) <= circumference/2 then -- No need to force spring update return position end diff --git a/src/humanoidanimatorutils/src/Shared/HumanoidAnimatorUtils.lua b/src/humanoidanimatorutils/src/Shared/HumanoidAnimatorUtils.lua index 3292225456..7fc24a8811 100644 --- a/src/humanoidanimatorutils/src/Shared/HumanoidAnimatorUtils.lua +++ b/src/humanoidanimatorutils/src/Shared/HumanoidAnimatorUtils.lua @@ -5,6 +5,8 @@ @class HumanoidAnimatorUtils ]=] +local RunService = game:GetService("RunService") + local HumanoidAnimatorUtils = {} --[=[ @@ -21,7 +23,10 @@ local HumanoidAnimatorUtils = {} function HumanoidAnimatorUtils.getOrCreateAnimator(humanoid) local animator = humanoid:FindFirstChildOfClass("Animator") if not animator then - warn("HumanoidAnimatorUtils] - Creating an animator") + if RunService:IsClient() then + warn(string.format("[HumanoidAnimatorUtils.getOrCreateAnimator] - Creating an animator on %s on the client", humanoid:GetFullName())) + end + animator = Instance.new("Animator") animator.Name = "Animator" animator.Parent = humanoid diff --git a/src/humanoidspeed/src/Client/HumanoidSpeedBindersClient.lua b/src/humanoidspeed/src/Client/HumanoidSpeedBindersClient.lua index f6d51e2ede..5a3267e33e 100644 --- a/src/humanoidspeed/src/Client/HumanoidSpeedBindersClient.lua +++ b/src/humanoidspeed/src/Client/HumanoidSpeedBindersClient.lua @@ -6,7 +6,6 @@ local require = require(script.Parent.loader).load(script) local BinderProvider = require("BinderProvider") -local Binder = require("Binder") return BinderProvider.new(script.Name, function(self, serviceBag) serviceBag:GetService(require("RogueHumanoidServiceClient")) @@ -15,5 +14,5 @@ return BinderProvider.new(script.Name, function(self, serviceBag) @prop HumanoidSpeed Binder @within HumanoidSpeedBindersClient ]=] - self:Add(Binder.new("HumanoidSpeed", require("HumanoidSpeedClient"), serviceBag)) + self:Add(serviceBag:GetService(require("HumanoidSpeedClient"))) end) \ No newline at end of file diff --git a/src/humanoidspeed/src/Client/HumanoidSpeedClient.lua b/src/humanoidspeed/src/Client/HumanoidSpeedClient.lua index dc972e2fed..bb3e80a50e 100644 --- a/src/humanoidspeed/src/Client/HumanoidSpeedClient.lua +++ b/src/humanoidspeed/src/Client/HumanoidSpeedClient.lua @@ -9,6 +9,7 @@ local require = require(script.Parent.loader).load(script) local BaseObject = require("BaseObject") local CharacterUtils = require("CharacterUtils") +local Binder = require("Binder") local HumanoidSpeedClient = setmetatable({}, BaseObject) HumanoidSpeedClient.ClassName = "HumanoidSpeedClient" @@ -28,4 +29,4 @@ function HumanoidSpeedClient:GetPlayer() return CharacterUtils.getPlayerFromCharacter(self._obj) end -return HumanoidSpeedClient \ No newline at end of file +return Binder.new("HumanoidSpeed", HumanoidSpeedClient) \ No newline at end of file diff --git a/src/humanoidspeed/src/Server/HumanoidSpeed.lua b/src/humanoidspeed/src/Server/HumanoidSpeed.lua index 1496963e91..a67ccdef11 100644 --- a/src/humanoidspeed/src/Server/HumanoidSpeed.lua +++ b/src/humanoidspeed/src/Server/HumanoidSpeed.lua @@ -9,6 +9,7 @@ local require = require(script.Parent.loader).load(script) local BaseObject = require("BaseObject") local RogueHumanoidProperties = require("RogueHumanoidProperties") +local Binder = require("Binder") local HumanoidSpeed = setmetatable({}, BaseObject) HumanoidSpeed.ClassName = "HumanoidSpeed" @@ -60,4 +61,4 @@ function HumanoidSpeed:ApplySpeedAdditive(amount) return self._properties.WalkSpeed:CreateAdditive(amount) end -return HumanoidSpeed \ No newline at end of file +return Binder.new("HumanoidSpeed", HumanoidSpeed) \ No newline at end of file diff --git a/src/humanoidspeed/src/Server/HumanoidSpeedBindersServer.lua b/src/humanoidspeed/src/Server/HumanoidSpeedBindersServer.lua index 677b087216..09fe6c8714 100644 --- a/src/humanoidspeed/src/Server/HumanoidSpeedBindersServer.lua +++ b/src/humanoidspeed/src/Server/HumanoidSpeedBindersServer.lua @@ -6,7 +6,6 @@ local require = require(script.Parent.loader).load(script) local BinderProvider = require("BinderProvider") -local Binder = require("Binder") return BinderProvider.new(script.Name, function(self, serviceBag) serviceBag:GetService(require("RogueHumanoidService")) @@ -15,5 +14,5 @@ return BinderProvider.new(script.Name, function(self, serviceBag) @prop HumanoidSpeed Binder @within HumanoidSpeedBindersServer ]=] - self:Add(Binder.new("HumanoidSpeed", require("HumanoidSpeed"), serviceBag)) + self:Add(serviceBag:GetService(require("HumanoidSpeed"))) end) \ No newline at end of file diff --git a/src/inputkeymaputils/src/Server/InputKeyMapService.lua b/src/inputkeymaputils/src/Server/InputKeyMapService.lua index 568c362a28..4ba532bb19 100644 --- a/src/inputkeymaputils/src/Server/InputKeyMapService.lua +++ b/src/inputkeymaputils/src/Server/InputKeyMapService.lua @@ -13,6 +13,7 @@ function InputKeyMapService:Init(serviceBag) -- Internal self._serviceBag:GetService(require("InputKeyMapRegistryServiceShared")) + self._serviceBag:GetService(require("InputKeyMapTranslator")) end return InputKeyMapService \ No newline at end of file diff --git a/src/inputkeymaputils/src/Client/InputKeyMapTranslator.lua b/src/inputkeymaputils/src/Shared/InputKeyMapTranslator.lua similarity index 100% rename from src/inputkeymaputils/src/Client/InputKeyMapTranslator.lua rename to src/inputkeymaputils/src/Shared/InputKeyMapTranslator.lua diff --git a/src/loader/src/ScriptInfoUtils.lua b/src/loader/src/ScriptInfoUtils.lua index b6380bdc91..2164e8deed 100644 --- a/src/loader/src/ScriptInfoUtils.lua +++ b/src/loader/src/ScriptInfoUtils.lua @@ -85,8 +85,6 @@ function ScriptInfoUtils.populateScriptInfoLookup(instance, scriptInfoLookup, la end local AVAILABLE_IN_SHARED = { - ["HoldingBindersServer"] = true; - ["HoldingBindersClient"] = true; ["IKService"] = true; ["IKServiceClient"] = true; } diff --git a/src/observablecollection/package.json b/src/observablecollection/package.json index 02284ac0ca..176ba070c7 100644 --- a/src/observablecollection/package.json +++ b/src/observablecollection/package.json @@ -38,6 +38,9 @@ "@quenty/symbol": "file:../symbol", "@quenty/valueobject": "file:../valueobject" }, + "devDependencies": { + "@quenty/blend": "file:../blend" + }, "publishConfig": { "access": "public" } diff --git a/src/observablecollection/src/Shared/FilteredObservableListView.lua b/src/observablecollection/src/Shared/FilteredObservableListView.lua index c9c30c077f..5370620d6a 100644 --- a/src/observablecollection/src/Shared/FilteredObservableListView.lua +++ b/src/observablecollection/src/Shared/FilteredObservableListView.lua @@ -25,10 +25,10 @@ function FilteredObservableListView.new(observableList, observeScoreCallback, co self._baseList = assert(observableList, "No observableList") self._observeScoreCallback = assert(observeScoreCallback, "No observeScoreCallback") - self._scoredList = ObservableSortedList.new(function(a, b) + self._scoredList = ObservableSortedList.new(false, function(a, b) -- Preserve index when scoring does not if a.score == b.score then - return a.index < b.index + return a.index - b.index else return self._compare(a.score, b.score) end diff --git a/src/observablecollection/src/Shared/ObservableCountingMap.lua b/src/observablecollection/src/Shared/ObservableCountingMap.lua index 96ba692b2b..f67031c151 100644 --- a/src/observablecollection/src/Shared/ObservableCountingMap.lua +++ b/src/observablecollection/src/Shared/ObservableCountingMap.lua @@ -25,8 +25,7 @@ function ObservableCountingMap.new() self._maid = Maid.new() self._map = {} - self._totalKeyCountValue = ValueObject.new(0, "number") - self._maid:GiveTask(self._totalKeyCountValue) + self._totalKeyCountValue = self._maid:Add(ValueObject.new(0, "number")) --[=[ Fires when an key is added @@ -34,8 +33,7 @@ function ObservableCountingMap.new() @prop KeyAdded Signal @within ObservableCountingMap ]=] - self.KeyAdded = Signal.new() - self._maid:GiveTask(self.KeyAdded) + self.KeyAdded = self._maid:Add(Signal.new()) --[=[ Fires when an key is removed. @@ -43,8 +41,7 @@ function ObservableCountingMap.new() @prop KeyRemoved Signal @within ObservableCountingMap ]=] - self.KeyRemoved = Signal.new() - self._maid:GiveTask(self.KeyRemoved) + self.KeyRemoved = self._maid:Add(Signal.new()) --[=[ Fires when an item count changes @@ -52,8 +49,7 @@ function ObservableCountingMap.new() @prop KeyChanged Signal @within ObservableCountingMap ]=] - self.KeyChanged = Signal.new() - self._maid:GiveTask(self.KeyChanged) + self.KeyChanged = self._maid:Add(Signal.new()) --[=[ Fires when the total count changes. diff --git a/src/observablecollection/src/Shared/ObservableList.lua b/src/observablecollection/src/Shared/ObservableList.lua index 16809cd201..24ab2071da 100644 --- a/src/observablecollection/src/Shared/ObservableList.lua +++ b/src/observablecollection/src/Shared/ObservableList.lua @@ -32,14 +32,9 @@ function ObservableList.new() self._contents = {} -- { [Symbol]: T } self._indexes = {} -- { [Symbol]: number } - self._indexObservers = ObservableSubscriptionTable.new() - self._maid:GiveTask(self._indexObservers) - - self._keyIndexObservables = ObservableSubscriptionTable.new() - self._maid:GiveTask(self._keyIndexObservables) - - self._countValue = ValueObject.new(0, "number") - self._maid:GiveTask(self._countValue) + self._indexObservers = self._maid:Add(ObservableSubscriptionTable.new()) + self._keyIndexObservables = self._maid:Add(ObservableSubscriptionTable.new()) + self._countValue = self._maid:Add(ValueObject.new(0, "number")) --[=[ Fires when an item is added @@ -47,8 +42,7 @@ function ObservableList.new() @prop ItemAdded Signal @within ObservableList ]=] - self.ItemAdded = Signal.new() - self._maid:GiveTask(self.ItemAdded) + self.ItemAdded = self._maid:Add(Signal.new()) --[=[ Fires when an item is removed. @@ -56,8 +50,7 @@ function ObservableList.new() @prop ItemRemoved Signal @within ObservableList ]=] - self.ItemRemoved = Signal.new() - self._maid:GiveTask(self.ItemRemoved) + self.ItemRemoved = self._maid:Add(Signal.new()) --[=[ Fires when the count changes. diff --git a/src/observablecollection/src/Shared/ObservableMapSet.lua b/src/observablecollection/src/Shared/ObservableMapSet.lua index 8f207373c6..ed3f841214 100644 --- a/src/observablecollection/src/Shared/ObservableMapSet.lua +++ b/src/observablecollection/src/Shared/ObservableMapSet.lua @@ -188,6 +188,22 @@ function ObservableMapSet:ObserveItemsForKeyBrio(key) end) end +--[=[ + Gets the first item for the given key + @param key TKey + @return TValue +]=] +function ObservableMapSet:GetFirstItemForKey(key) + assert(key ~= nil, "Bad key") + + local observableSet = self._observableSetMap[key] + if not observableSet then + return nil + end + + return observableSet:GetFirstItem() +end + --[=[ Gets a list for a given key @param key TKey diff --git a/src/observablecollection/src/Shared/ObservableSortedList.lua b/src/observablecollection/src/Shared/ObservableSortedList.lua index 5dbd2576fe..21a443ef23 100644 --- a/src/observablecollection/src/Shared/ObservableSortedList.lua +++ b/src/observablecollection/src/Shared/ObservableSortedList.lua @@ -9,6 +9,9 @@ For performance reasons this class defers firing events until the next defer() event frame. + This class always prefers to add equivalent elements to the end of the list if they're not in the list. + Otherwise it prefers minimal movement. + @class ObservableSortedList ]=] @@ -23,9 +26,16 @@ local Signal = require("Signal") local Symbol = require("Symbol") local ValueObject = require("ValueObject") --- Higher numbers last. Using <= ensures insertion at end on ties. +-- Higher numbers last local function defaultCompare(a, b) - return a <= b + -- equivalent of `return a - b` except it supports comparison of strings and stuff + if b > a then + return -1 + elseif b < a then + return 1 + else + return 0 + end end local ObservableSortedList = {} @@ -34,10 +44,13 @@ ObservableSortedList.__index = ObservableSortedList --[=[ Constructs a new ObservableSortedList - @param compare callback? + @param isReversed boolean + @param compare function @return ObservableSortedList ]=] -function ObservableSortedList.new(compare) +function ObservableSortedList.new(isReversed, compare) + assert(type(isReversed) == "boolean" or isReversed == nil, "Bad isReversed") + local self = setmetatable({}, ObservableSortedList) self._maid = Maid.new() @@ -56,10 +69,10 @@ function ObservableSortedList.new(compare) self._keyObservables = {} -- { [Symbol]: { Subscription } } + self._isReversed = isReversed or false self._compare = compare or defaultCompare - self._countValue = ValueObject.new(0, "number") - self._maid:GiveTask(self._countValue) + self._countValue = self._maid:Add(ValueObject.new(0, "number")) --[=[ Fires when an item is added @@ -67,20 +80,23 @@ function ObservableSortedList.new(compare) @prop ItemAdded Signal @within ObservableSortedList ]=] - self.ItemAdded = Signal.new() - self._maid:GiveTask(self.ItemAdded) + self.ItemAdded = self._maid:Add(Signal.new()) --[=[ Fires when an item is removed. @readonly - @prop ItemRemoved Signal + @prop ItemRemoved self._maid:Add(Signal) @within ObservableSortedList ]=] - self.ItemRemoved = Signal.new() - self._maid:GiveTask(self.ItemRemoved) + self.ItemRemoved = self._maid:Add(Signal.new()) - self.OrderChanged = Signal.new() - self._maid:GiveTask(self.OrderChanged) +--[=[ + Fires when an item's order changes. + @readonly + @prop OrderChanged self._maid:Add(Signal) + @within ObservableSortedList +]=] + self.OrderChanged = self._maid:Add(Signal.new()) --[=[ Fires when the count changes. @@ -303,12 +319,14 @@ function ObservableSortedList:Add(item, observeValue) self._contents[key] = item maid:GiveTask(observeValue:Subscribe(function(sortValue) - self._sortValue[key] = sortValue + self:_debugVerifyIntegrity() if sortValue ~= nil then local currentIndex = self._indexes[key] local targetIndex = self:_findCorrectIndex(sortValue, currentIndex) - self:_updateIndex(key, item, targetIndex) + + self._sortValue[key] = sortValue + self:_updateIndex(key, item, targetIndex, sortValue) else local observableSubs = self._keyObservables[key] @@ -320,6 +338,8 @@ function ObservableSortedList:Add(item, observeValue) self:_fireSubs(observableSubs, nil) end end + + self:_debugVerifyIntegrity() end)) maid:GiveTask(function() @@ -371,23 +391,23 @@ function ObservableSortedList:RemoveByKey(key) self._maid[key] = nil end -function ObservableSortedList:_updateIndex(key, item, index) +function ObservableSortedList:_updateIndex(key, item, newIndex) assert(item ~= nil, "Bad item") - assert(type(index) == "number", "Bad index") + assert(type(newIndex) == "number", "Bad newIndex") - local pastIndex = self._indexes[key] - if pastIndex == index then + local prevIndex = self._indexes[key] + if prevIndex == newIndex then return end - self._indexes[key] = index + self._indexes[key] = newIndex local changed = {} - if not pastIndex then + if not prevIndex then -- shift everything up to fit this space local n = #self._keyList - for i=n, index, -1 do + for i=n, newIndex, -1 do local nextKey = self._keyList[i] self._indexes[nextKey] = i + 1 self._keyList[i + 1] = nextKey @@ -397,9 +417,9 @@ function ObservableSortedList:_updateIndex(key, item, index) newIndex = i + 1; }) end - elseif index > pastIndex then - -- we're moving up (3 -> 5), so everything shifts down to fill up the pastIndex - for i=pastIndex + 1, index do + elseif newIndex > prevIndex then + -- we're shifting down + for i=prevIndex + 1, newIndex do local nextKey = self._keyList[i] self._indexes[nextKey] = i - 1 self._keyList[i - 1] = nextKey @@ -409,10 +429,10 @@ function ObservableSortedList:_updateIndex(key, item, index) newIndex = i - 1; }) end - else - -- if index < pastIndex then - -- we're moving down (5 -> 3) so everything shifts up to fit this space - for i=pastIndex-1, index, -1 do + elseif newIndex < prevIndex then + -- we're shifting up + + for i=prevIndex-1, newIndex, -1 do local belowKey = self._keyList[i] self._indexes[belowKey] = i + 1 self._keyList[i + 1] = belowKey @@ -421,23 +441,24 @@ function ObservableSortedList:_updateIndex(key, item, index) newIndex = i + 1; }) end + else + error("Bad state") end local itemAdded = { key = key; - newIndex = index; + newIndex = newIndex; item = item; } -- ensure ourself is considered changed table.insert(changed, itemAdded) - - self._keyList[index] = key + self._keyList[newIndex] = key -- Fire off our count value changed -- still O(n^2) but at least we prevent emitting O(n^2) events - if pastIndex == nil then + if prevIndex == nil then self:_deferChange(1, itemAdded, nil, changed) else self:_deferChange(0, nil, nil, changed) @@ -573,23 +594,119 @@ function ObservableSortedList:_queueDeferredChange() end function ObservableSortedList:_findCorrectIndex(sortValue, currentIndex) - -- todo: binary search - -- todo: stable + local highInsertionIndex = self:_highBinarySearch(sortValue) + + -- we're inserting, so always insert at end + if not currentIndex then + return highInsertionIndex + end + + local lowInsertionIndex = self:_lowBinarySearch(sortValue) + + -- remember we get insertion index so we need to subtract one + if highInsertionIndex > currentIndex then + highInsertionIndex = highInsertionIndex - 1 + end + if lowInsertionIndex > currentIndex then + lowInsertionIndex = lowInsertionIndex - 1 + end + + -- prioritize the smallest potential movement + if currentIndex < lowInsertionIndex then + return lowInsertionIndex + elseif currentIndex > highInsertionIndex then + return highInsertionIndex + else + return currentIndex + end +end + +function ObservableSortedList:_highBinarySearch(sortValue) + if #self._keyList == 0 then + return 1 + end - for i=#self._keyList, 1, -1 do - local currentKey = self._keyList[i] - if self._compare(self._sortValue[currentKey], sortValue) then + local minIndex = 1 + local maxIndex = #self._keyList + while true do + local mid = math.floor((minIndex + maxIndex) / 2) + local compareValue = self._compare(self._sortValue[self._keyList[mid]], sortValue) + assert(type(compareValue) == "number", "Expecting number") - -- include index in this - if currentIndex and currentIndex <= i then - return i + if self._isReversed then + compareValue = -compareValue + end + + if compareValue > 0 then + maxIndex = mid - 1 + if minIndex > maxIndex then + return mid + end + else + minIndex = mid + 1 + if minIndex > maxIndex then + return mid + 1 + end + end + end +end + +function ObservableSortedList:_lowBinarySearch(sortValue) + if #self._keyList == 0 then + return 1 + end + + local minIndex = 1 + local maxIndex = #self._keyList + while true do + local mid = math.floor((minIndex + maxIndex) / 2) + local compareValue = self._compare(self._sortValue[self._keyList[mid]], sortValue) + assert(type(compareValue) == "number", "Expecting number") + + if self._isReversed then + compareValue = -compareValue + end + + if compareValue < 0 then + minIndex = mid + 1 + if minIndex > maxIndex then + return mid + 1 end + else + maxIndex = mid - 1 + if minIndex > maxIndex then + return mid + end + end + end +end + +function ObservableSortedList:_debugSortValuesToString() + local values = {} + + for _, key in pairs(self._keyList) do + table.insert(values, string.format("%4d", self._sortValue[key])) + end + + return table.concat(values, ", ") +end - return i + 1 +function ObservableSortedList:_debugVerifyIntegrity() + for i=2, #self._keyList do + local compare = self._compare(self._sortValue[self._keyList[i-1]], self._sortValue[self._keyList[i]]) + if self._isReversed then + compare = -compare + end + if compare > 0 then + warn(string.format("Bad sorted list state %s at index %d", self:_debugSortValuesToString(), i)) end end - return 1 + for i=1, #self._keyList do + if self._indexes[self._keyList[i]] ~= i then + warn(string.format("Index is out of date for %d for %s", i, self:_debugSortValuesToString())) + end + end end function ObservableSortedList:_fireSubs(list, index) diff --git a/src/observablecollection/src/Shared/ObservableSortedList.story.lua b/src/observablecollection/src/Shared/ObservableSortedList.story.lua new file mode 100644 index 0000000000..c0f2933c8c --- /dev/null +++ b/src/observablecollection/src/Shared/ObservableSortedList.story.lua @@ -0,0 +1,152 @@ +--[[ + @class observableSortedList.story +]] + +local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script) + +local Maid = require("Maid") +local ValueObject = require("ValueObject") +local ObservableSortedList = require("ObservableSortedList") +local Blend = require("Blend") +local RxBrioUtils = require("RxBrioUtils") +local Rx = require("Rx") + +local ENTRIES = 10 +local CHANGE_TO_NEGATIVE_INDEX = false + +return function(target) + local maid = Maid.new() + + local observableSortedList = maid:Add(ObservableSortedList.new()) + + local random = Random.new() + + local values = {} + for i=1, ENTRIES do + local scoreValue = maid:Add(ValueObject.new(0, "number")) + + local data = { + originalIndex = i; + scoreValue = scoreValue; + } + + values[i] = data + + maid:GiveTask(task.delay(i*0.05, function() + maid:Add(observableSortedList:Add(data, scoreValue:Observe())) + end)) + + if CHANGE_TO_NEGATIVE_INDEX then + maid:GiveTask(task.delay(ENTRIES*0.05 + random:NextNumber()*3, function() + -- print("change", scoreValue.Value, " to", -1) + scoreValue.Value = -i + end)) + end + end + + maid:GiveTask(observableSortedList.OrderChanged:Connect(function() + local results = {} + local inOrder = true + local lastValue = nil + for _, item in pairs(observableSortedList:GetList()) do + if lastValue then + if item.scoreValue.Value < lastValue then + inOrder = false + end + end + lastValue = item.scoreValue.Value + table.insert(results, string.format("%3d", item.scoreValue.Value)) + end + + if not inOrder then + warn("BAD SORT ", table.concat(results, ", ")) + else + print("-->", table.concat(results, ", ")) + end + end)) + + -- maid:GiveTask(task.delay(0.1, function() + -- values[7].scoreValue.Value = -5 + -- end)) + + maid:GiveTask(Blend.mount(target, { + Blend.New "Frame" { + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + + Blend.New "UIListLayout" { + Padding = UDim.new(0, 5); + HorizontalAlignment = Enum.HorizontalAlignment.Center; + VerticalAlignment = Enum.VerticalAlignment.Top; + }; + + Blend.New "UIPadding" { + PaddingTop = UDim.new(0, 10); + PaddingBottom = UDim.new(0, 10); + }; + + observableSortedList:ObserveItemsBrio():Pipe({ + RxBrioUtils.flatMapBrio(function(data, itemKey) + local valid = ValueObject.new(false, "boolean") + + return Blend.New "Frame" { + Size = UDim2.fromOffset(100, 30); + BackgroundColor3 = Blend.Spring(Blend.Computed(valid, function(isValid) + if isValid then + return Color3.new(1, 1, 1) + else + return Color3.new(1, 0.5, 0.5) + end + end), 5); + LayoutOrder = observableSortedList:ObserveIndexByKey(itemKey); + + Blend.New "UICorner" { + CornerRadius = UDim.new(0, 5); + }; + + Blend.New "TextLabel" { + Text = data.scoreValue:Observe():Pipe({ + Rx.map(tostring) + }); + Size = UDim2.fromScale(1, 1); + BackgroundTransparency = 1; + Position = UDim2.new(1, 10, 0.5, 0); + AnchorPoint = Vector2.new(0, 0.5); + TextColor3 = Color3.new(1, 1, 1); + TextXAlignment = Enum.TextXAlignment.Left; + }; + + Blend.New "TextLabel" { + Text = data.originalIndex; + Size = UDim2.fromScale(1, 1); + BackgroundTransparency = 1; + Position = UDim2.new(0, -10, 0.5, 0); + AnchorPoint = Vector2.new(1, 0.5); + TextColor3 = Color3.new(1, 1, 1); + TextXAlignment = Enum.TextXAlignment.Right; + }; + + Blend.New "TextBox" { + Size = UDim2.fromScale(1, 1); + Text = tostring(data.scoreValue.Value); + BackgroundTransparency = 1; + [Blend.OnChange "Text"] = function(newValue) + if tonumber(newValue) then + data.scoreValue.Value = tonumber(newValue) + valid.Value = true + else + valid.Value = false + end + end; + }; + } + end) + }) + } + })) + + + return function() + maid:DoCleaning() + end +end \ No newline at end of file diff --git a/src/particles/package.json b/src/particles/package.json index 2a7405e20e..e93019b22d 100644 --- a/src/particles/package.json +++ b/src/particles/package.json @@ -26,6 +26,7 @@ ], "dependencies": { "@quenty/loader": "file:../loader", + "@quenty/maid": "file:../maid", "@quenty/numbersequenceutils": "file:../numbersequenceutils" }, "publishConfig": { diff --git a/src/particles/src/Shared/ParticleEmitterUtils.lua b/src/particles/src/Shared/ParticleEmitterUtils.lua index 3be8e7edb2..9272b0f50f 100644 --- a/src/particles/src/Shared/ParticleEmitterUtils.lua +++ b/src/particles/src/Shared/ParticleEmitterUtils.lua @@ -5,6 +5,7 @@ local require = require(script.Parent.loader).load(script) local NumberSequenceUtils = require("NumberSequenceUtils") +local Maid = require("Maid") local ParticleEmitterUtils = {} @@ -16,6 +17,30 @@ function ParticleEmitterUtils.scaleSize(adornee, scale) end end +function ParticleEmitterUtils.playFromTemplate(template, attachment) + local maid = Maid.new() + + for _, emitter in pairs(template:GetChildren()) do + local newEmitter = emitter:Clone() + newEmitter.Parent = attachment + maid:GiveTask(newEmitter) + + local emitDelay = newEmitter:GetAttribute("EmitDelay") + local emitCount = newEmitter:GetAttribute("EmitCount") + + if emitDelay then + maid:GiveTask(task.delay(emitDelay, function() + newEmitter:Emit(emitCount) + end)) + else + newEmitter:Emit(emitCount) + end + end + + return maid + +end + function ParticleEmitterUtils.getParticleEmitters(adornee) assert(typeof(adornee) == "Instance", "Bad adornee") diff --git a/src/preferredparentutils/src/Shared/PreferredParentUtils.lua b/src/preferredparentutils/src/Shared/PreferredParentUtils.lua index ad1751caf2..6fa3e341c6 100644 --- a/src/preferredparentutils/src/Shared/PreferredParentUtils.lua +++ b/src/preferredparentutils/src/Shared/PreferredParentUtils.lua @@ -11,9 +11,10 @@ local PreferredParentUtils = {} --[=[ @param parent Instance @param name string + @param forceCreate boolean @return () -> Instance ]=] -function PreferredParentUtils.createPreferredParentRetriever(parent, name) +function PreferredParentUtils.createPreferredParentRetriever(parent, name, forceCreate) assert(typeof(parent) == "Instance", "Bad parent") assert(type(name) == "string", "Bad name") @@ -24,7 +25,7 @@ function PreferredParentUtils.createPreferredParentRetriever(parent, name) return cache end - cache = PreferredParentUtils.getPreferredParent(parent, name) + cache = PreferredParentUtils.getPreferredParent(parent, name, forceCreate) return cache end end @@ -32,9 +33,10 @@ end --[=[ @param parent Instance @param name string + @param forceCreate boolean @return Instance ]=] -function PreferredParentUtils.getPreferredParent(parent, name) +function PreferredParentUtils.getPreferredParent(parent, name, forceCreate) assert(typeof(parent) == "Instance", "Bad parent") assert(type(name) == "string", "Bad name") @@ -54,7 +56,7 @@ function PreferredParentUtils.getPreferredParent(parent, name) return found end - if RunService:IsServer() then + if RunService:IsServer() or forceCreate then local newParent = Instance.new("Folder") newParent.Name = name newParent.Parent = parent diff --git a/src/r15utils/src/Shared/R15Utils.lua b/src/r15utils/src/Shared/R15Utils.lua index de2b2019fe..d9697dae76 100644 --- a/src/r15utils/src/Shared/R15Utils.lua +++ b/src/r15utils/src/Shared/R15Utils.lua @@ -297,6 +297,17 @@ function R15Utils.getWristToGripLength(character, side) end end +function R15Utils.getHumanoidScaleProperty(humanoid, scaleValueName) + assert(typeof(humanoid) == "Instance" and humanoid:IsA("Humanoid"), "Bad humanoid") + + local scaleValue = humanoid:FindFirstChild(scaleValueName) + if scaleValue then + return scaleValue.Value + else + return nil + end +end + --[=[ Computes the length of an arm for a given character @param character Model diff --git a/src/r15utils/src/Shared/RxR15Utils.lua b/src/r15utils/src/Shared/RxR15Utils.lua index bd1cc8381f..5dc72956f9 100644 --- a/src/r15utils/src/Shared/RxR15Utils.lua +++ b/src/r15utils/src/Shared/RxR15Utils.lua @@ -64,4 +64,32 @@ function RxR15Utils.observeCharacterPartBrio(character, partName) return RxInstanceUtils.observeLastNamedChildBrio(character, "BasePart", partName) end +--[=[ + Observes a rig motor as a brio + @param character Model + @return Observable> +]=] +function RxR15Utils.observeHumanoidBrio(character) + assert(typeof(character) == "Instance", "Bad character") + + return RxInstanceUtils.observeLastNamedChildBrio(character, "Humanoid", "Humanoid") +end + +function RxR15Utils.observeHumanoidScaleValueObject(humanoid, scaleValueName) + assert(typeof(humanoid) == "Instance" and humanoid:IsA("Humanoid"), "Bad humanoid") + + return RxInstanceUtils.observeLastNamedChildBrio(humanoid, "NumberValue", scaleValueName) +end + +function RxR15Utils.observeHumanoidScaleProperty(humanoid, scaleValueName) + assert(typeof(humanoid) == "Instance" and humanoid:IsA("Humanoid"), "Bad humanoid") + + return RxR15Utils.observeHumanoidScaleValueObject(humanoid, scaleValueName):Pipe({ + RxBrioUtils.switchMapBrio(function(scaleValue) + return RxInstanceUtils.observeProperty(scaleValue, "Value") + end) + }) +end + + return RxR15Utils \ No newline at end of file diff --git a/src/racketingropeconstraint/package.json b/src/racketingropeconstraint/package.json index 3f40597eed..ddd6118086 100644 --- a/src/racketingropeconstraint/package.json +++ b/src/racketingropeconstraint/package.json @@ -28,9 +28,12 @@ ], "dependencies": { "@quenty/baseobject": "file:../baseobject", + "@quenty/binder": "file:../binder", "@quenty/loader": "file:../loader", "@quenty/overriddenproperty": "file:../overriddenproperty", - "@quenty/promise": "file:../promise" + "@quenty/promise": "file:../promise", + "@quenty/tie": "file:../tie", + "@quenty/valueobject": "file:../valueobject" }, "publishConfig": { "access": "public" diff --git a/src/racketingropeconstraint/src/Shared/RacketingRopeConstraint.lua b/src/racketingropeconstraint/src/Shared/RacketingRopeConstraint.lua index 055fa846cf..1ac2a92726 100644 --- a/src/racketingropeconstraint/src/Shared/RacketingRopeConstraint.lua +++ b/src/racketingropeconstraint/src/Shared/RacketingRopeConstraint.lua @@ -8,8 +8,11 @@ local require = require(script.Parent.loader).load(script) local RunService = game:GetService("RunService") local BaseObject = require("BaseObject") +local Binder = require("Binder") local OverriddenProperty = require("OverriddenProperty") local Promise = require("Promise") +local RacketingRopeConstraintInterface = require("RacketingRopeConstraintInterface") +local ValueObject = require("ValueObject") local START_DISTANCE = 1000 @@ -23,6 +26,9 @@ function RacketingRopeConstraint.new(ropeConstraint) self._smallestDistance = START_DISTANCE self._targetDistance = 0.5 + self._isConstrained = ValueObject.new(false, "boolean") + self._maid:GiveTask(self._isConstrained) + self._maid:GiveTask(self._obj:GetPropertyChangedSignal("Enabled"):Connect(function() self:_handleActiveChanged() end)) @@ -40,11 +46,13 @@ function RacketingRopeConstraint.new(ropeConstraint) self._maid:GiveTask(self._overriddenLength) end + self._maid:GiveTask(RacketingRopeConstraintInterface:Implement(self._obj, self)) + return self end function RacketingRopeConstraint:PromiseConstrained() - if self:_isValid() and self:_isConstrained() then + if self:_isValid() and self:_queryIsConstrained() then return Promise.resolved() end @@ -57,7 +65,11 @@ function RacketingRopeConstraint:PromiseConstrained() return promise end -function RacketingRopeConstraint:_isConstrained() +function RacketingRopeConstraint:ObserveIsConstrained() + return self._isConstrained:Observe() +end + +function RacketingRopeConstraint:_queryIsConstrained() return self._obj.Length <= self._targetDistance end @@ -94,11 +106,12 @@ function RacketingRopeConstraint:_update() self:_setLength(self._smallestDistance) - if self:_isConstrained() then + if self:_queryIsConstrained() then self._maid._updateHeartbeat = nil if self._maid._pendingConstrainedPromise then - if self:_isValid() and self:_isConstrained() then + if self:_isValid() and self:_queryIsConstrained() then + self._isConstrained.Value = true self._maid._pendingConstrainedPromise:Resolve() end @@ -115,4 +128,4 @@ function RacketingRopeConstraint:_setLength(length) end end -return RacketingRopeConstraint \ No newline at end of file +return Binder.new("RacketingRopeConstraint", RacketingRopeConstraint) \ No newline at end of file diff --git a/src/racketingropeconstraint/src/Shared/RacketingRopeConstraintInterface.lua b/src/racketingropeconstraint/src/Shared/RacketingRopeConstraintInterface.lua new file mode 100644 index 0000000000..19bf9bc449 --- /dev/null +++ b/src/racketingropeconstraint/src/Shared/RacketingRopeConstraintInterface.lua @@ -0,0 +1,12 @@ +--[=[ + @class RacketingRopeConstraintInterface +]=] + +local require = require(script.Parent.loader).load(script) + +local TieDefinition = require("TieDefinition") + +return TieDefinition.new("RacketingRopeConstraint", { + PromiseConstrained = TieDefinition.Types.METHOD; + ObserveIsConstrained = TieDefinition.Types.METHOD; +}) \ No newline at end of file diff --git a/src/ragdoll/package.json b/src/ragdoll/package.json index 7f5de964de..7fac5e4e41 100644 --- a/src/ragdoll/package.json +++ b/src/ragdoll/package.json @@ -46,9 +46,11 @@ "@quenty/remoting": "file:../remoting", "@quenty/rx": "file:../rx", "@quenty/rxbinderutils": "file:../rxbinderutils", + "@quenty/rxsignal": "file:../rxsignal", "@quenty/spring": "file:../spring", "@quenty/steputils": "file:../steputils", "@quenty/table": "file:../table", + "@quenty/tie": "file:../tie", "@quenty/valuebaseutils": "file:../valuebaseutils", "@quenty/valueobject": "file:../valueobject" }, diff --git a/src/ragdoll/src/Client/Classes/RagdollableClient.lua b/src/ragdoll/src/Client/Classes/RagdollableClient.lua index 7633086fd5..499c8dc660 100644 --- a/src/ragdoll/src/Client/Classes/RagdollableClient.lua +++ b/src/ragdoll/src/Client/Classes/RagdollableClient.lua @@ -7,12 +7,14 @@ local require = require(script.Parent.loader).load(script) -local BaseObject = require("BaseObject") +local RagdollableBase = require("RagdollableBase") local RagdollClient = require("RagdollClient") local RxRagdollUtils = require("RxRagdollUtils") local Binder = require("Binder") +local RagdollableInterface = require("RagdollableInterface") +local Rx = require("Rx") -local RagdollableClient = setmetatable({}, BaseObject) +local RagdollableClient = setmetatable({}, RagdollableBase) RagdollableClient.ClassName = "RagdollableClient" RagdollableClient.__index = RagdollableClient @@ -23,7 +25,7 @@ RagdollableClient.__index = RagdollableClient @return RagdollableClient ]=] function RagdollableClient.new(humanoid, serviceBag) - local self = setmetatable(BaseObject.new(humanoid), RagdollableClient) + local self = setmetatable(RagdollableBase.new(humanoid), RagdollableClient) self._serviceBag = assert(serviceBag, "No serviceBag") self._ragdollBinder = self._serviceBag:GetService(RagdollClient) @@ -32,9 +34,19 @@ function RagdollableClient.new(humanoid, serviceBag) self:_onRagdollChanged(ragdoll) end)) + self._maid:GiveTask(RagdollableInterface:Implement(self._obj, self)) + return self end +function RagdollableClient:ObserveIsRagdolled() + return self._ragdollBinder:Observe(self._obj):Pipe({ + Rx.map(function(value) + return value and true or false + end) + }) +end + function RagdollableClient:_onRagdollChanged(ragdoll) if ragdoll then self._maid._ragdoll = RxRagdollUtils.runLocal(self._obj) diff --git a/src/ragdoll/src/Server/Classes/Ragdollable.lua b/src/ragdoll/src/Server/Classes/Ragdollable.lua index c75af2a800..5add863cf2 100644 --- a/src/ragdoll/src/Server/Classes/Ragdollable.lua +++ b/src/ragdoll/src/Server/Classes/Ragdollable.lua @@ -6,7 +6,7 @@ local require = require(script.Parent.loader).load(script) -local BaseObject = require("BaseObject") +local RagdollableBase = require("RagdollableBase") local Maid = require("Maid") local Motor6DStackHumanoid = require("Motor6DStackHumanoid") local PlayerHumanoidBinder = require("PlayerHumanoidBinder") @@ -17,8 +17,10 @@ local RagdollCollisionUtils = require("RagdollCollisionUtils") local RagdollMotorUtils = require("RagdollMotorUtils") local RxBrioUtils = require("RxBrioUtils") local RxRagdollUtils = require("RxRagdollUtils") +local RagdollableInterface = require("RagdollableInterface") +local Rx = require("Rx") -local Ragdollable = setmetatable({}, BaseObject) +local Ragdollable = setmetatable({}, RagdollableBase) Ragdollable.ClassName = "Ragdollable" Ragdollable.__index = Ragdollable @@ -29,7 +31,7 @@ Ragdollable.__index = Ragdollable @return Ragdollable ]=] function Ragdollable.new(humanoid, serviceBag) - local self = setmetatable(BaseObject.new(humanoid), Ragdollable) + local self = setmetatable(RagdollableBase.new(humanoid), Ragdollable) self._serviceBag = assert(serviceBag, "No serviceBag") self._ragdollBinder = self._serviceBag:GetService(Ragdoll) @@ -63,9 +65,19 @@ function Ragdollable.new(humanoid, serviceBag) end)) self:_onRagdollChanged() + self._maid:GiveTask(RagdollableInterface:Implement(self._obj, self)) + return self end +function Ragdollable:ObserveIsRagdolled() + return self._ragdollBinder:Observe(self._obj):Pipe({ + Rx.map(function(value) + return value and true or false + end) + }) +end + function Ragdollable:_onRagdollChanged() if self._ragdollBinder:Get(self._obj) then self:_setRagdollEnabled(true) diff --git a/src/ragdoll/src/Shared/Classes/RagdollableBase.lua b/src/ragdoll/src/Shared/Classes/RagdollableBase.lua new file mode 100644 index 0000000000..e6cf0664b2 --- /dev/null +++ b/src/ragdoll/src/Shared/Classes/RagdollableBase.lua @@ -0,0 +1,56 @@ +--[=[ + @class RagdollableBase +]=] + +local require = require(script.Parent.loader).load(script) + +local BaseObject = require("BaseObject") +local RxSignal = require("RxSignal") +local Rx = require("Rx") + +local RagdollableBase = setmetatable({}, BaseObject) +RagdollableBase.ClassName = "RagdollableBase" +RagdollableBase.__index = RagdollableBase + +function RagdollableBase.new(humanoid) + local self = setmetatable(BaseObject.new(humanoid), RagdollableBase) + + self.Ragdolled = RxSignal.new(function() + return self:ObserveIsRagdolled():Pipe({ + Rx.skip(1); + Rx.where(function(value) + return value == true + end); + }) + end) + + self.Unragdolled = RxSignal.new(function() + return self:ObserveIsRagdolled():Pipe({ + Rx.skip(1); + Rx.where(function(value) + return value == true + end); + }) + end) + + return self +end + +function RagdollableBase:Ragdoll() + self._obj:AddTag("Ragdoll") +end + +function RagdollableBase:Unragdoll() + self._obj:RemoveTag("Ragdoll") +end + +function RagdollableBase:ObserveIsRagdolled() + error("Not implemented") +end + +function RagdollableBase:IsRagdolled() + return self._obj:HasTag("Ragdoll") +end + + +return RagdollableBase \ No newline at end of file diff --git a/src/ragdoll/src/Shared/Interfaces/RagdollableInterface.lua b/src/ragdoll/src/Shared/Interfaces/RagdollableInterface.lua new file mode 100644 index 0000000000..21ac2a43b4 --- /dev/null +++ b/src/ragdoll/src/Shared/Interfaces/RagdollableInterface.lua @@ -0,0 +1,18 @@ + +--[=[ + @class RagdollableInterface +]=] + +local require = require(script.Parent.loader).load(script) + +local TieDefinition = require("TieDefinition") + +return TieDefinition.new("Ragdollable", { + Ragdolled = TieDefinition.Types.SIGNAL; + Unragdolled = TieDefinition.Types.SIGNAL; + + Ragdoll = TieDefinition.Types.METHOD; + Unragdoll = TieDefinition.Types.METHOD; + ObserveIsRagdolled = TieDefinition.Types.METHOD; + IsRagdolled = TieDefinition.Types.METHOD; +}) \ No newline at end of file diff --git a/src/randomutils/package.json b/src/randomutils/package.json index f10633d2cb..cb71b238c5 100644 --- a/src/randomutils/package.json +++ b/src/randomutils/package.json @@ -29,6 +29,7 @@ "access": "public" }, "dependencies": { - "@quenty/loader": "file:../loader" + "@quenty/loader": "file:../loader", + "@quenty/table": "file:../table" } } diff --git a/src/randomutils/src/Shared/WeightedRandomChooser.lua b/src/randomutils/src/Shared/WeightedRandomChooser.lua new file mode 100644 index 0000000000..4023a47e59 --- /dev/null +++ b/src/randomutils/src/Shared/WeightedRandomChooser.lua @@ -0,0 +1,96 @@ +--[=[ + @class WeightedRandomChooser +]=] + +local require = require(script.Parent.loader).load(script) + +local RandomUtils = require("RandomUtils") +local Table = require("Table") + +local WeightedRandomChooser = {} +WeightedRandomChooser.ClassName = "WeightedRandomChooser" +WeightedRandomChooser.__index = WeightedRandomChooser + +--[=[ + Creates a new weighted random chooser + + @return WeightedRandomChooser +]=] +function WeightedRandomChooser.new() + local self = setmetatable({}, WeightedRandomChooser) + + self._optionToWeight = {} + + return self +end + +--[=[ + Sets the weight for a given option. Setting the weight to nil + removes the option. + + @param option T + @param weight number | nil +]=] +function WeightedRandomChooser:SetWeight(option, weight) + assert(option ~= nil, "Bad option") + assert(type(weight) == "number" or weight == nil, "Bad weight") + + self._optionToWeight[option] = weight +end + +--[=[ + Gets the weight for the option + + @param option T + @return number | nil +]=] +function WeightedRandomChooser:GetWeight(option) + return self._optionToWeight[option] +end + +--[=[ + Gets the percent probability from 0 to 1 + + @param option T + @return number | nil +]=] +function WeightedRandomChooser:GetProbability(option) + local weight = self._optionToWeight[option] + if weight then + return nil + end + + -- TODO: Cache if we call like a million times + local total = 0 + for _, item in pairs(self._optionToWeight) do + total = total + item + end + + return weight/total +end + +--[=[ + Removes the option from the chooser. Equivalent of setting the weight to nil + + @param option T +]=] +function WeightedRandomChooser:Remove(option) + self:SetWeight(option, nil) +end + +--[=[ + Picks a weighted choise + + @return T +]=] +function WeightedRandomChooser:Choose() + local options = Table.keys(self._optionToWeight) + local weights = {} + for index, key in pairs(options) do + weights[index] = self._optionToWeight[key] + end + + return RandomUtils.weightedChoice(options, weights) +end + +return WeightedRandomChooser \ No newline at end of file diff --git a/src/rogue-properties/src/Shared/Implementation/RogueProperty.lua b/src/rogue-properties/src/Shared/Implementation/RogueProperty.lua index 73a89ffd15..52b954d9cc 100644 --- a/src/rogue-properties/src/Shared/Implementation/RogueProperty.lua +++ b/src/rogue-properties/src/Shared/Implementation/RogueProperty.lua @@ -292,8 +292,9 @@ function RogueProperty:_encodeValue(current) end function RogueProperty:GetChangedEvent() - return RxSignal.new(self:Observe()) + return RxSignal.new(self:Observe():Pipe({ + Rx.skip(1) + })) end - return RogueProperty \ No newline at end of file diff --git a/src/rxsignal/src/Shared/RxSignal.lua b/src/rxsignal/src/Shared/RxSignal.lua index 2cbbf9e968..f22977ff6d 100644 --- a/src/rxsignal/src/Shared/RxSignal.lua +++ b/src/rxsignal/src/Shared/RxSignal.lua @@ -5,6 +5,7 @@ local require = require(script.Parent.loader).load(script) local Rx = require("Rx") +local Observable = require("Observable") local RxSignal = {} RxSignal.ClassName = "RxSignal" @@ -13,7 +14,7 @@ RxSignal.__index = RxSignal --[=[ Converts an observable to the Signal interface - @param observable Observable + @param observable Observable | () -> Observable @return RxSignal ]=] function RxSignal.new(observable) @@ -21,19 +22,33 @@ function RxSignal.new(observable) local self = setmetatable({}, RxSignal) - self._observable = observable:Pipe({ - Rx.skip(1); - }) + self._observable = observable return self end function RxSignal:Connect(callback) - return self._observable:Subscribe(callback) + return self:_getObservable():Subscribe(callback) +end + +function RxSignal:_getObservable() + if Observable.isObservable(self._observable) then + return self._observable + end + + if type(self._observable) == "function" then + local result = self._observable() + + assert(Observable.isObservable(result), "Result should be observable") + + return result + else + error("Could not convert self._observable to observable") + end end function RxSignal:Once(callback) - return self._observable:Pipe({ + return self:_getObservable():Pipe({ Rx.take(1); }):Subscribe(callback) end diff --git a/src/signal/package.json b/src/signal/package.json index 84b8929eba..c62cbd57e1 100644 --- a/src/signal/package.json +++ b/src/signal/package.json @@ -28,5 +28,8 @@ ], "publishConfig": { "access": "public" + }, + "dependencies": { + "@quenty/loader": "file:../loader" } } diff --git a/src/signal/src/Shared/GoodSignal.lua b/src/signal/src/Shared/GoodSignal.lua index 127549106e..df96ba5d1b 100644 --- a/src/signal/src/Shared/GoodSignal.lua +++ b/src/signal/src/Shared/GoodSignal.lua @@ -1,27 +1,47 @@ --------------------------------------------------------------------------------- --- Batched Yield-Safe Signal Implementation -- --- This is a Signal class which has effectively identical behavior to a -- --- normal RBXScriptSignal, with the only difference being a couple extra -- --- stack frames at the bottom of the stack trace when an error is thrown. -- --- This implementation caches runner coroutines, so the ability to yield in -- --- the signal handlers comes at minimal extra cost over a naive signal -- --- implementation that either always or never spawns a thread. -- --- -- --- API: -- --- local Signal = require(THIS MODULE) -- --- local sig = Signal.new() -- --- local connection = sig:Connect(function(arg1, arg2, ...) ... end) -- --- sig:Fire(arg1, arg2, ...) -- --- connection:Disconnect() -- --- sig:DisconnectAll() -- --- local arg1, arg2, ... = sig:Wait() -- --- -- --- Licence: -- --- Licenced under the MIT licence. -- --- -- --- Authors: -- --- stravant - July 31st, 2021 - Created the file. -- --------------------------------------------------------------------------------- +--[=[ + Batched Yield-Safe Signal Implementation + + Lua-side duplication of the [API of events on Roblox objects](https://create.roblox.com/docs/reference/engine/datatypes/RBXScriptSignal). + + Signals are needed for to ensure that for local events objects are passed by + reference rather than by value where possible, as the BindableEvent objects + always pass signal arguments by value, meaning tables will be deep copied. + Roblox's deep copy method parses to a non-lua table compatable format. + + This class is designed to work both in deferred mode and in regular mode. + It follows whatever mode is set. + + ```lua + local signal = Signal.new() + + local arg = {} + + signal:Connect(function(value) + assert(arg == value, "Tables are preserved when firing a Signal") + end) + + signal:Fire(arg) + ``` + + :::info + Why this over a direct [BindableEvent]? Well, in this case, the signal + prevents Roblox from trying to serialize and desialize each table reference + fired through the BindableEvent. + ::: + + This is a Signal class which has effectively identical behavior to a + normal RBXScriptSignal, with the only difference being a couple extra + stack frames at the bottom of the stack trace when an error is thrown + This implementation caches runner coroutines, so the ability to yield in + the signal handlers comes at minimal extra cost over a naive signal + implementation that either always or never spawns a thread. + + Author notes: + stravant - July 31st, 2021 - Created the file. + Quenty - Auguest 21st, 2023 - Modified to fit Nevermore contract, with Moonwave docs + + @class Signal +]=] -- The currently idle thread to run the next handler on local freeRunnerThread = nil @@ -100,14 +120,36 @@ setmetatable(Connection, { -- Signal class local Signal = {} +Signal.ClassName = "Signal" Signal.__index = Signal +--[=[ + Constructs a new signal. + @return Signal +]=] function Signal.new() return setmetatable({ _handlerListHead = false, }, Signal) end +--[=[ + Returns whether a class is a signal + + @param value any + @return boolean +]=] +function Signal.isSignal(value) + return type(value) == "table" + and getmetatable(value) == Signal +end + +--[=[ + Connect a new handler to the event. Returns a connection object that can be disconnected. + + @param fn (... T) -> () -- Function handler called when `:Fire(...)` is called + @return RBXScriptConnection +]=] function Signal:Connect(fn) local connection = Connection.new(self, fn) if self._handlerListHead then @@ -119,16 +161,30 @@ function Signal:Connect(fn) return connection end --- Disconnect all handlers. Since we use a linked list it suffices to clear the --- reference to the head handler. +--[=[ + Disconnects all connected events to the signal. + + :::info + Disconnect all handlers. Since we use a linked list it suffices to clear the + reference to the head handler. + ::: +]=] function Signal:DisconnectAll() self._handlerListHead = false end --- Signal:Fire(...) implemented by running the handler functions on the --- coRunnerThread, and any time the resulting thread yielded without returning --- to us, that means that it yielded to the Roblox scheduler and has been taken --- over by Roblox scheduling, meaning we have to make a new coroutine runner. +--[=[ + Fire the event with the given arguments. All handlers will be invoked. Handlers follow + + ::: info + Signal:Fire(...) is implemented by running the handler functions on the + coRunnerThread, and any time the resulting thread yielded without returning + to us, that means that it yielded to the Roblox scheduler and has been taken + over by Roblox scheduling, meaning we have to make a new coroutine runner. + ::: + + @param ... T -- Variable arguments to pass to handler +]=] function Signal:Fire(...) local item = self._handlerListHead while item do @@ -144,8 +200,17 @@ function Signal:Fire(...) end end --- Implement Signal:Wait() in terms of a temporary connection using --- a Signal:Connect() which disconnects itself. +--[=[ + Wait for fire to be called, and return the arguments it was given. + + ::: info + Signal:Wait() is implemented in terms of a temporary connection using + a Signal:Connect() which disconnects itself. + ::: + + @yields + @return T +]=] function Signal:Wait() local waitingCoroutine = coroutine.running() local cn; @@ -156,8 +221,17 @@ function Signal:Wait() return coroutine.yield() end --- Implement Signal:Once() in terms of a connection which disconnects --- itself before running the handler. +--[=[ + Connect a new, one-time handler to the event. Returns a connection object that can be disconnected. + + ::: info + -- Implement Signal:Once() in terms of a connection which disconnects + -- itself before running the handler. + ::: + + @param fn (... T) -> () -- One-time function handler called when `:Fire(...)` is called + @return RBXScriptConnection +]=] function Signal:Once(fn) local cn; cn = self:Connect(function(...) @@ -169,6 +243,12 @@ function Signal:Once(fn) return cn end +--[=[ + Alias for [DisconnectAll] + + @function Destroy + @within Signal +]=] Signal.Destroy = Signal.DisconnectAll -- Make signal strict diff --git a/src/signal/src/Shared/Signal.lua b/src/signal/src/Shared/Signal.lua index 66456e788b..41a9864880 100644 --- a/src/signal/src/Shared/Signal.lua +++ b/src/signal/src/Shared/Signal.lua @@ -30,6 +30,14 @@ @class Signal ]=] +local USE_GOOD_SIGNAL_ONLY = true + +if USE_GOOD_SIGNAL_ONLY then + local require = require(script.Parent.loader).load(script) + + return require("GoodSignal") +end + local HttpService = game:GetService("HttpService") local ENABLE_TRACEBACK = false diff --git a/src/signal/src/node_modules.project.json b/src/signal/src/node_modules.project.json new file mode 100644 index 0000000000..46233dac4f --- /dev/null +++ b/src/signal/src/node_modules.project.json @@ -0,0 +1,7 @@ +{ + "name": "node_modules", + "globIgnorePaths": [ "**/.package-lock.json" ], + "tree": { + "$path": { "optional": "../node_modules" } + } +} \ No newline at end of file diff --git a/src/softshutdown/src/Server/SoftShutdownService.lua b/src/softshutdown/src/Server/SoftShutdownService.lua index d7a9429b15..81cef704f5 100644 --- a/src/softshutdown/src/Server/SoftShutdownService.lua +++ b/src/softshutdown/src/Server/SoftShutdownService.lua @@ -26,7 +26,6 @@ local Maid = require("Maid") local Promise = require("Promise") local SoftShutdownConstants = require("SoftShutdownConstants") local TeleportServiceUtils = require("TeleportServiceUtils") -local BindToCloseService = require("BindToCloseService") local SoftShutdownService = {} SoftShutdownService.ServiceName = "SoftShutdownService" @@ -44,7 +43,8 @@ SoftShutdownService.ServiceName = "SoftShutdownService" function SoftShutdownService:Init(serviceBag) self._serviceBag = assert(serviceBag, "No serviceBag") - self._bindToCloseService = self._serviceBag:GetService(BindToCloseService) + self._bindToCloseService = self._serviceBag:GetService(require("BindToCloseService")) + self._serviceBag:GetService(require("SoftShutdownTranslator")) self._dataStore = DataStoreService:GetDataStore("IsSoftShutdownServer") self._maid = Maid.new() diff --git a/src/softshutdown/src/Client/SoftShutdownTranslator.lua b/src/softshutdown/src/Shared/SoftShutdownTranslator.lua similarity index 100% rename from src/softshutdown/src/Client/SoftShutdownTranslator.lua rename to src/softshutdown/src/Shared/SoftShutdownTranslator.lua diff --git a/src/tie/package.json b/src/tie/package.json index e1c3ae0d18..f8373f1e56 100644 --- a/src/tie/package.json +++ b/src/tie/package.json @@ -31,6 +31,7 @@ "@quenty/attributeutils": "file:../attributeutils", "@quenty/baseobject": "file:../baseobject", "@quenty/brio": "file:../brio", + "@quenty/collectionserviceutils": "file:../collectionserviceutils", "@quenty/instanceutils": "file:../instanceutils", "@quenty/loader": "file:../loader", "@quenty/maid": "file:../maid", diff --git a/src/tie/src/Shared/Definition/TieDefinition.lua b/src/tie/src/Shared/Definition/TieDefinition.lua index 2a89d27bdb..617411a817 100644 --- a/src/tie/src/Shared/Definition/TieDefinition.lua +++ b/src/tie/src/Shared/Definition/TieDefinition.lua @@ -11,6 +11,7 @@ local Maid = require("Maid") local Observable = require("Observable") local Rx = require("Rx") local RxBrioUtils = require("RxBrioUtils") +local RxCollectionServiceUtils = require("RxCollectionServiceUtils") local RxInstanceUtils = require("RxInstanceUtils") local RxStateStackUtils = require("RxStateStackUtils") local String = require("String") @@ -82,6 +83,85 @@ function TieDefinition:GetImplementations(adornee: Instance) return implementations end +--[=[ + Observes all the children implementations for this adornee + + @param adornee Instance + @return Observable> +]=] +function TieDefinition:ObserveChildrenBrio(adornee: Instance) + return RxInstanceUtils.observeChildrenBrio(adornee):Pipe({ + RxBrioUtils.flatMapBrio(function(child) + return self:ObserveBrio(child) + end) + }) +end + +--[=[ + Promises the implementation + + @param adornee Adornee + @return Promise +]=] +function TieDefinition:Promise(adornee) + assert(typeof(adornee) == "Instance", "Bad adornee") + + -- TODO: Support cancellation cleanup here. + + return Rx.toPromise(self:Observe(adornee):Pipe({ + Rx.where(function(value) + return value ~= nil + end) + })) +end + +--[=[ + Gets all valid interfaces for this adornee's children + + @param adornee Instance + @return { TieInterface } +]=] +function TieDefinition:GetChildren(adornee: Instance) + assert(typeof(adornee) == "Instance", "Bad adornee") + + local implementations = {} + + -- TODO: Make this faster + for _, item in pairs(adornee:GetChildren()) do + for _, option in pairs(self:GetImplementations(item)) do + table.insert(implementations, option) + end + end + + return implementations +end + +--[=[ + Finds the implementation on the adornee. Alais for [FindFirstImplementation] + + @param adornee Adornee + @return TieInterface | nil +]=] +function TieDefinition:Find(adornee: Instance) + return self:FindFirstImplementation(adornee) +end + +--[=[ + Observes all implementations that are tagged with the given tag name + + @param tagName string + @return TieInterface | nil +]=] +function TieDefinition:ObserveAllTaggedBrio(tagName) + assert(type(tagName) == "string", "Bad tagName") + + return RxCollectionServiceUtils.observeTaggedBrio(tagName):Pipe({ + RxBrioUtils.flatMapBrio(function(instance) + return self:ObserveBrio(instance) + end) + }) +end + --[=[ Finds the first valid interfaces for this adornee @param adornee Instance diff --git a/src/tie/src/Shared/Implementation/TieImplementation.lua b/src/tie/src/Shared/Implementation/TieImplementation.lua index b9e9391f7b..76a8be68bd 100644 --- a/src/tie/src/Shared/Implementation/TieImplementation.lua +++ b/src/tie/src/Shared/Implementation/TieImplementation.lua @@ -59,7 +59,7 @@ function TieImplementation:__index(index) if memberMap[index] then return memberMap[index]:GetInterface(self._folder, self) else - error(("Bad index %q for TieImplementation"):format(tostring(index))) + error(string.format("Bad index %q for TieImplementation", tostring(index))) end end diff --git a/src/tie/src/Shared/Implementation/TiePropertyImplementation.lua b/src/tie/src/Shared/Implementation/TiePropertyImplementation.lua index 7056e28dc3..1fcb2edfc2 100644 --- a/src/tie/src/Shared/Implementation/TiePropertyImplementation.lua +++ b/src/tie/src/Shared/Implementation/TiePropertyImplementation.lua @@ -100,7 +100,7 @@ function TiePropertyImplementation:_updateImplementation(maid, implementation) local className = ValueBaseUtils.getClassNameFromType(typeof(implementation)) if not className then - error(("[TiePropertyImplementation] - Bad implementation value type %q, cannot set"):format(typeof(implementation))) + error(string.format("[TiePropertyImplementation] - Bad implementation value type %q, cannot set %s", typeof(implementation), self._memberDefinition:GetMemberName())) end local copy = self:_changeToClassIfNeeded(className, implementation) diff --git a/src/tie/src/Shared/Interface/TiePropertyInterface.lua b/src/tie/src/Shared/Interface/TiePropertyInterface.lua index 0695783f27..663cf0fa3f 100644 --- a/src/tie/src/Shared/Interface/TiePropertyInterface.lua +++ b/src/tie/src/Shared/Interface/TiePropertyInterface.lua @@ -151,6 +151,10 @@ function TiePropertyInterface:_observeFromFolder(folder) local lastImplementationType = UNSET_VALUE local function update() + if not sub:IsPending() then + return + end + -- Prioritize attributes first local currentAttribute = folder:GetAttribute(memberName) if currentAttribute ~= nil then diff --git a/src/userserviceutils/src/Client/UserInfoServiceClient.lua b/src/userserviceutils/src/Client/UserInfoServiceClient.lua index f45f529319..e2534dd758 100644 --- a/src/userserviceutils/src/Client/UserInfoServiceClient.lua +++ b/src/userserviceutils/src/Client/UserInfoServiceClient.lua @@ -46,6 +46,18 @@ function UserInfoServiceClient:PromiseDisplayName(userId) return self._aggregator:PromiseDisplayName(userId) end +--[=[ + Observes the user info for the user + + @param userId number + @return Observable +]=] +function UserInfoServiceClient:ObserveUserInfo(userId) + assert(type(userId) == "number", "Bad userId") + + return self._aggregator:ObserveDisplayName(userId) +end + --[=[ Observes the user display name for the userId diff --git a/src/userserviceutils/src/Server/UserInfoService.lua b/src/userserviceutils/src/Server/UserInfoService.lua index 06d5bd86d1..61913f1579 100644 --- a/src/userserviceutils/src/Server/UserInfoService.lua +++ b/src/userserviceutils/src/Server/UserInfoService.lua @@ -34,6 +34,18 @@ function UserInfoService:PromiseUserInfo(userId) return self._aggregator:PromiseUserInfo(userId) end +--[=[ + Observes the user info for the user + + @param userId number + @return Observable +]=] +function UserInfoService:ObserveUserInfo(userId) + assert(type(userId) == "number", "Bad userId") + + return self._aggregator:ObserveDisplayName(userId) +end + --[=[ Promises the user display name for the userId diff --git a/src/userserviceutils/src/Shared/UserInfoAggregator.lua b/src/userserviceutils/src/Shared/UserInfoAggregator.lua index 4f03e846eb..9ea0118f94 100644 --- a/src/userserviceutils/src/Shared/UserInfoAggregator.lua +++ b/src/userserviceutils/src/Shared/UserInfoAggregator.lua @@ -1,5 +1,5 @@ --[=[ - Aggregates all requests into one big send request + Aggregates all requests into one big send request to deduplicate the request @class UserInfoAggregator ]=] @@ -11,6 +11,8 @@ local Promise = require("Promise") local UserServiceUtils = require("UserServiceUtils") local Rx = require("Rx") +local MAX_USER_IDS_PER_REQUEST = 200 + local UserInfoAggregator = setmetatable({}, BaseObject) UserInfoAggregator.ClassName = "UserInfoAggregator" UserInfoAggregator.__index = UserInfoAggregator @@ -20,6 +22,8 @@ function UserInfoAggregator.new() -- TODO: LRU cache this? Limit to 1k or something? self._promises = {} + + self._unsentCount = 0 self._unsentPromises = {} return self @@ -42,6 +46,7 @@ function UserInfoAggregator:PromiseUserInfo(userId) local promise = Promise.new() self._unsentPromises[userId] = promise + self._unsentCount = self._unsentCount + 1 self._promises[userId] = promise self:_queueAggregatedPromises() @@ -64,6 +69,48 @@ function UserInfoAggregator:PromiseDisplayName(userId) end) end +--[=[ + Promises the user display name for the userId + + @param userId number + @return Promise +]=] +function UserInfoAggregator:PromiseDisplayName(userId) + assert(type(userId) == "number", "Bad userId") + + return self:PromiseUserInfo(userId) + :Then(function(userInfo) + return userInfo.DisplayName + end) +end + +--[=[ + Promises the user display name for the userId + + @param userId number + @return Promise +]=] +function UserInfoAggregator:PromiseHasVerifiedBadge(userId) + assert(type(userId) == "number", "Bad userId") + + return self:PromiseUserInfo(userId) + :Then(function(userInfo) + return userInfo.HasVerifiedBadge + end) +end + +--[=[ + Observes the user display name for the userId + + @param userId number + @return Observable +]=] +function UserInfoAggregator:ObserveUserInfo(userId) + assert(type(userId) == "number", "Bad userId") + + return Rx.fromPromise(self:PromiseUserInfo(userId)) +end + --[=[ Observes the user display name for the userId @@ -73,12 +120,15 @@ end function UserInfoAggregator:ObserveDisplayName(userId) assert(type(userId) == "number", "Bad userId") - return Rx.fromPromise(self:PromiseUserInfo(userId)) + return self:ObserveUserInfo():Pipe({ + Rx.map(function(userInfo) + return userInfo.DisplayName + end) + }) end -function UserInfoAggregator:_sendAggregatedPromises() - local promiseMap = self._unsentPromises - self._unsentPromises = {} +function UserInfoAggregator:_sendAggregatedPromises(promiseMap) + assert(promiseMap, "No promiseMap") local userIds = {} local unresolvedMap = {} @@ -91,6 +141,8 @@ function UserInfoAggregator:_sendAggregatedPromises() return end + assert(#userIds <= MAX_USER_IDS_PER_REQUEST, "Too many userIds sent") + self._maid:GivePromise(UserServiceUtils.promiseUserInfosByUserIds(userIds)) :Then(function(result) assert(type(result) == "table", "Bad result") @@ -109,24 +161,37 @@ function UserInfoAggregator:_sendAggregatedPromises() promise:Reject(string.format("Failed to get result for userId %d", userId)) end end, function(...) - for _, item in pairs(promiseMap) do + for _, item in pairs(unresolvedMap) do item:Reject(...) end end) end +function UserInfoAggregator:_resetQueue() + local promiseMap = self._unsentPromises + + self._maid._queue = nil + self._unsentCount = 0 + self._unsentPromises = {} + + return promiseMap +end + function UserInfoAggregator:_queueAggregatedPromises() - if self._queued then + if self._unsentCount >= MAX_USER_IDS_PER_REQUEST then + self:_sendAggregatedPromises(self:_resetQueue()) + return + end + + if self._maid._queue then return end - self._queued = true - self._maid._queue = task.delay(0.05, function() - self._queued = false - self:_sendAggregatedPromises() + self._maid._queue = task.delay(0.1, function() + task.spawn(function() + self:_sendAggregatedPromises(self:_resetQueue()) + end) end) end - - return UserInfoAggregator \ No newline at end of file diff --git a/src/userserviceutils/src/Shared/UserServiceUtils.lua b/src/userserviceutils/src/Shared/UserServiceUtils.lua index a85f5f2b5f..fd915e5b72 100644 --- a/src/userserviceutils/src/Shared/UserServiceUtils.lua +++ b/src/userserviceutils/src/Shared/UserServiceUtils.lua @@ -1,4 +1,6 @@ --[=[ + Wraps [UserService] API calls with [Promise]. + @class UserServiceUtils ]=] @@ -15,11 +17,17 @@ local UserServiceUtils = {} .Id number -- The Id associated with the UserInfoResponse object .Username string -- The username associated with the UserInfoResponse object .DisplayName string -- The display name associated with the UserInfoResponse object + .HasVerifiedBadge boolean -- The HasVerifiedBadge value associated with the user. @within UserServiceUtils ]=] --[=[ Wraps UserService:GetUserInfosByUserIdsAsync(userIds) + + ::: tip + User [UserInfoAggregator] via [UserInfoService] to get this deduplicated. + ::: + @param userIds { number } @return Promise<{ UserInfo }> ]=] @@ -46,6 +54,10 @@ end --[=[ Wraps UserService:GetUserInfosByUserIdsAsync({ userId })[1] + ::: tip + User [UserInfoAggregator] via [UserInfoService] to get this deduplicated. + ::: + @param userId number @return Promise ]=] @@ -67,6 +79,10 @@ end --[=[ Wraps UserService:GetUserInfosByUserIdsAsync({ userId })[1].DisplayName + ::: tip + User [UserInfoAggregator] via [UserInfoService] to get this deduplicated. + ::: + @param userId number @return Promise ]=] @@ -82,6 +98,10 @@ end --[=[ Wraps UserService:GetUserInfosByUserIdsAsync({ userId })[1].Username + ::: tip + User [UserInfoAggregator] via [UserInfoService] to get this deduplicated. + ::: + @param userId number @return Promise ]=] diff --git a/src/valuebaseutils/src/Shared/ValueBaseValue.lua b/src/valuebaseutils/src/Shared/ValueBaseValue.lua index 16d79ccec6..3cb9ee2e8a 100644 --- a/src/valuebaseutils/src/Shared/ValueBaseValue.lua +++ b/src/valuebaseutils/src/Shared/ValueBaseValue.lua @@ -11,6 +11,7 @@ local RunService = game:GetService("RunService") local ValueBaseUtils = require("ValueBaseUtils") local RxValueBaseUtils = require("RxValueBaseUtils") local RxSignal = require("RxSignal") +local Rx = require("Rx") local ValueBaseValue = {} ValueBaseValue.ClassName = "ValueBaseValue" @@ -48,7 +49,9 @@ function ValueBaseValue:__index(index) if index == "Value" then return ValueBaseUtils.getValue(self._parent, self._className, self._name, self._defaultValue) elseif index == "Changed" then - return RxSignal.new(self:Observe()) + return RxSignal.new(self:Observe():Pipe({ + Rx.skip(1) + })) elseif ValueBaseValue[index] or index == "_defaultValue" then return ValueBaseValue[index] else diff --git a/src/viewport/src/Client/Viewport.lua b/src/viewport/src/Client/Viewport.lua index 0feea3d040..82e6ef4dbc 100644 --- a/src/viewport/src/Client/Viewport.lua +++ b/src/viewport/src/Client/Viewport.lua @@ -48,28 +48,18 @@ Viewport.__index = Viewport function Viewport.new() local self = setmetatable(BasicPane.new(), Viewport) - self._current = ValueObject.new(nil) - self._maid:GiveTask(self._current) + self._current = self._maid:Add(ValueObject.new(nil)) + self._transparency = self._maid:Add(ValueObject.new(0, "number")) + self._absoluteSize = self._maid:Add(ValueObject.new(Vector2.zero, "Vector2")) + self._fieldOfView = self._maid:Add(ValueObject.new(20, "number")) - self._transparency = ValueObject.new(0, "number") - self._maid:GiveTask(self._transparency) - - self._absoluteSize = ValueObject.new(Vector2.zero, "Vector2") - self._maid:GiveTask(self._absoluteSize) - - self._fieldOfView = ValueObject.new(20, "number") - self._maid:GiveTask(self._fieldOfView) - - self._rotationYawSpring = SpringObject.new(math.pi/4) + self._rotationYawSpring = self._maid:Add(SpringObject.new(math.rad(90 + 90 - 30))) self._rotationYawSpring.Speed = 30 - self._maid:GiveTask(self._rotationYawSpring) - self._rotationPitchSpring = SpringObject.new(-math.pi/6) + self._rotationPitchSpring = self._maid:Add(SpringObject.new(math.rad(-15))) self._rotationPitchSpring.Speed = 30 - self._maid:GiveTask(self._rotationPitchSpring) - self._notifyInstanceSizeChanged = Signal.new() - self._maid:GiveTask(self._notifyInstanceSizeChanged) + self._notifyInstanceSizeChanged = self._maid:Add(Signal.new()) return self end @@ -132,6 +122,9 @@ function Viewport.blend(props) end) end +function Viewport:ObserveTransparency() + return self._transparency:Observe() +end --[=[ Sets the field of view on the viewport. @@ -171,6 +164,12 @@ function Viewport:SetInstance(instance) assert(typeof(instance) == "Instance" or instance == nil, "Bad instance") self._current.Value = instance + + return function() + if self._current.Value == instance then + self._current.Value = nil + end + end end --[=[ @@ -181,26 +180,34 @@ function Viewport:NotifyInstanceSizeChanged() self._notifyInstanceSizeChanged:Fire() end -function Viewport:RotateBy(deltaV2, doNotAnimate) - local target = (self._rotationYawSpring.Value + deltaV2.x) % TAU - self._rotationYawSpring.Position = CircleUtils.updatePositionToSmallestDistOnCircle(self._rotationYawSpring.Position, target, TAU) +function Viewport:SetYaw(yaw, doNotAnimate) + yaw = yaw % TAU - self._rotationYawSpring.Target = target + self._rotationYawSpring.Position = CircleUtils.updatePositionToSmallestDistOnCircle(self._rotationYawSpring.Position, yaw, TAU) + self._rotationYawSpring.Target = yaw if doNotAnimate then self._rotationYawSpring.Position = self._rotationYawSpring.Target end +end - self._rotationPitchSpring.Target = math.clamp(self._rotationPitchSpring.Value + deltaV2.y, MIN_PITCH, MAX_PITCH) +function Viewport:SetPitch(pitch, doNotAnimate) + self._rotationPitchSpring.Target = math.clamp(pitch, MIN_PITCH, MAX_PITCH) if doNotAnimate then self._rotationPitchSpring.Position = self._rotationPitchSpring.Target end end +function Viewport:RotateBy(deltaV2, doNotAnimate) + self:SetYaw(self._rotationYawSpring.Value + deltaV2.x, doNotAnimate) + self:SetPitch(self._rotationPitchSpring.Value + deltaV2.y, doNotAnimate) +end + --[=[ Renders the viewport. Allows the following properties. * Ambient - Color3 + * ImageColor3 - Color3 * AnchorPoint - Vector2 * LayoutOrder - number * LightColor - Color3 @@ -221,16 +228,25 @@ function Viewport:Render(props) local currentCamera = ValueObject.new() self._maid:GiveTask(currentCamera) + local lightDirectionCFrame = (CFrame.Angles(0, math.rad(180), 0) + * CFrame.Angles(math.rad(-45), 0, 0)) + local brightness = 1.25 + local ambientBrightness = 0.75 + return Blend.New "ViewportFrame" { Parent = props.Parent; Size = props.Size or UDim2.new(1, 0, 1, 0); AnchorPoint = props.AnchorPoint; Position = props.Position; + ImageColor3 = props.ImageColor3; LayoutOrder = props.LayoutOrder; BackgroundTransparency = 1; + BackgroundColor3 = props.BackgroundColor3; CurrentCamera = currentCamera; - LightColor = props.LightColor or Color3.fromRGB(200, 200, 200); - Ambient = props.Ambient or Color3.fromRGB(140, 140, 140); + -- selene:allow(roblox_incorrect_color3_new_bounds) + LightColor = props.LightColor or Color3.new(brightness, brightness, brightness + 0.15); + LightDirection = props.LightDirection or lightDirectionCFrame:vectorToWorldSpace(Vector3.new(0, 0, -1)); + Ambient = props.Ambient or Color3.new(ambientBrightness, ambientBrightness, ambientBrightness + 0.15); ImageTransparency = Blend.Computed(props.Transparency or 0, self._transparency, function(propTransparency, selfTransparency) return Math.map(propTransparency, 0, 1, selfTransparency, 1) @@ -265,7 +281,10 @@ function Viewport:Render(props) return maid end)] = true; [Blend.Children] = { + props[Blend.Children]; + self._current; + Blend.New "Camera" { [Blend.Instance] = currentCamera; Name = "CurrentCamera"; diff --git a/src/viewport/src/Client/ViewportControls.lua b/src/viewport/src/Client/ViewportControls.lua index 0df25464e0..97feb77fa1 100644 --- a/src/viewport/src/Client/ViewportControls.lua +++ b/src/viewport/src/Client/ViewportControls.lua @@ -68,6 +68,10 @@ function ViewportControls:_startDrag(startInputObject) end)) maid:GiveTask(function() + if not self._viewportModel.Destroy then + return + end + -- Compute rotation if lastDelta then self._viewportModel:RotateBy(lastDelta) diff --git a/tools/nevermore-cli/templates/game-template/src/modules/Server/{{gameNameProper}}Service.lua b/tools/nevermore-cli/templates/game-template/src/modules/Server/{{gameNameProper}}Service.lua index e062dbcf88..ed3d569d7f 100644 --- a/tools/nevermore-cli/templates/game-template/src/modules/Server/{{gameNameProper}}Service.lua +++ b/tools/nevermore-cli/templates/game-template/src/modules/Server/{{gameNameProper}}Service.lua @@ -15,6 +15,7 @@ function {{gameNameProper}}Service:Init(serviceBag) self._serviceBag:GetService(require("CmdrService")) -- Internal + self._serviceBag:GetService(require("{{gameNameProper}}Translator")) end return {{gameNameProper}}Service \ No newline at end of file diff --git a/tools/nevermore-cli/templates/game-template/src/modules/Client/{{gameNameProper}}Translator.lua b/tools/nevermore-cli/templates/game-template/src/modules/Shared/{{gameNameProper}}Translator.lua similarity index 100% rename from tools/nevermore-cli/templates/game-template/src/modules/Client/{{gameNameProper}}Translator.lua rename to tools/nevermore-cli/templates/game-template/src/modules/Shared/{{gameNameProper}}Translator.lua