From a5f3434817d2237572e9466568fbbc2862b97c7d Mon Sep 17 00:00:00 2001 From: Azur Date: Sun, 2 Nov 2025 21:42:44 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5Finish=20Flamework=20Migration.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/components/lobby_door.ts | 45 +++++ src/server/components/random_decoration.ts | 31 +++ src/server/runtime.server.ts | 2 +- src/server/services/command.ts | 10 +- src/server/services/decoration.ts | 21 -- src/server/services/gameplay/ragdoll.ts | 179 +++++++++++++++++- src/server/services/gameplay/sprint.ts | 36 +++- ...{playerjoinservice.ts => player_events.ts} | 5 +- src/shared/build.ts | 16 -- src/shared/modding/player_events.ts | 3 + src/shared/ownership.ts | 178 ----------------- src/shared/utils/ownership.ts | 27 +++ 12 files changed, 315 insertions(+), 238 deletions(-) create mode 100644 src/server/components/lobby_door.ts create mode 100644 src/server/components/random_decoration.ts delete mode 100644 src/server/services/decoration.ts rename src/server/services/modding/{playerjoinservice.ts => player_events.ts} (90%) delete mode 100644 src/shared/build.ts create mode 100644 src/shared/modding/player_events.ts delete mode 100644 src/shared/ownership.ts create mode 100644 src/shared/utils/ownership.ts diff --git a/src/server/components/lobby_door.ts b/src/server/components/lobby_door.ts new file mode 100644 index 0000000..7a74399 --- /dev/null +++ b/src/server/components/lobby_door.ts @@ -0,0 +1,45 @@ +import { BaseComponent, Component } from "@flamework/components"; +import { OnStart } from "@flamework/core"; +import { CollectionService, Players } from "@rbxts/services"; + +interface DoorInstance extends Model { + Right_Door: Model & { + Velocity: LinearVelocity; + }; + Left_Door: Model & { + Velocity: LinearVelocity; + }; +} + +@Component({ + tag: "Lobby_Door", +}) +export class Doors extends BaseComponent<{}, DoorInstance> implements OnStart { + constructor() { + super(); + } + onStart(): void { + const doororigin = this.instance.WorldPivot.Position; + while (task.wait(0.5)) { + let actived = false; + Players.GetPlayers().forEach((player) => { + const character = player.Character; + if (character) { + const hrp = character.FindFirstChild("HumanoidRootPart") as Part; + if (hrp) { + const vectorToDoor = doororigin.sub(hrp.Position); + const distanceToDoor = vectorToDoor.Magnitude; + if (distanceToDoor <= 25) actived = true; + } + } + }); + if (actived) { + this.instance.Right_Door.Velocity.VectorVelocity = new Vector3(-5, 0, 0); + this.instance.Left_Door.Velocity.VectorVelocity = new Vector3(5, 0, 0); + } else { + this.instance.Right_Door.Velocity.VectorVelocity = new Vector3(5, 0, 0); + this.instance.Left_Door.Velocity.VectorVelocity = new Vector3(-5, 0, 0); + } + } + } +} diff --git a/src/server/components/random_decoration.ts b/src/server/components/random_decoration.ts new file mode 100644 index 0000000..bb8d84d --- /dev/null +++ b/src/server/components/random_decoration.ts @@ -0,0 +1,31 @@ +import { BaseComponent, Component } from "@flamework/components"; +import { OnStart } from "@flamework/core"; +import { ReplicatedStorage } from "@rbxts/services"; + +interface Attribute { + Type: string; +} + +@Component({ + tag: "Random_Decoration", +}) +class RandomDecoration extends BaseComponent implements OnStart { + onStart(): void { + const decoration = this.getRandomDecoration(this.attributes.Type); + decoration.Parent = this.instance.Parent; + decoration.MoveTo(this.instance.Position); + this.instance.Destroy(); + this.destroy(); + } + private getRandomDecoration(deco_type: string) { + function getDecoration(decorations: Model[]) { + return decorations[math.random(0, decorations.size() - 1)]; + } + const decorations = ReplicatedStorage.FindFirstChild("Decoration") + ?.FindFirstChild(`${deco_type}Types`) + ?.GetChildren() + .filter((decoration) => decoration.IsA("Model")); + if (!decorations) throw "Unknow Decoration Type"; + return getDecoration(decorations).Clone(); + } +} diff --git a/src/server/runtime.server.ts b/src/server/runtime.server.ts index 5728924..0a23899 100644 --- a/src/server/runtime.server.ts +++ b/src/server/runtime.server.ts @@ -1,6 +1,6 @@ import { Flamework } from "@flamework/core"; -// Flamework.addPaths("src/server/components"); +Flamework.addPaths("src/server/components"); Flamework.addPaths("src/server/services"); // Flamework.addPaths("src/shared/components"); diff --git a/src/server/services/command.ts b/src/server/services/command.ts index 753f59c..0d47ef3 100644 --- a/src/server/services/command.ts +++ b/src/server/services/command.ts @@ -1,20 +1,21 @@ import { OnStart, Service } from "@flamework/core"; import { Players, TextChatService } from "@rbxts/services"; -import { RagdollHandler, takeNetworkOwner } from "shared/ownership"; +import { takeNetworkOwner } from "shared/utils/ownership"; +import { RagdollService } from "./gameplay/ragdoll"; /** * Setup Chat Command */ @Service() class CommandsService implements OnStart { + constructor(private readonly ragdollService: RagdollService) {} onStart(): void { const ragdollCommand = new TextChatCommand(); ragdollCommand.Parent = TextChatService.WaitForChild("TextChatCommands"); ragdollCommand.PrimaryAlias = "/ragdoll"; ragdollCommand.Triggered.Connect((TextSource) => { const player = Players.GetPlayerByUserId(TextSource.UserId); - const handler = RagdollHandler.getHandlerByCharacter(player!.Character!, true); - handler.ragdoll(); + this.ragdollService.ragdoll(player?.Character!); }); const unragdollCommand = new TextChatCommand(); @@ -22,8 +23,7 @@ class CommandsService implements OnStart { unragdollCommand.PrimaryAlias = "/unragdoll"; unragdollCommand.Triggered.Connect((TextSource) => { const player = Players.GetPlayerByUserId(TextSource.UserId); - const handler = RagdollHandler.getHandlerByCharacter(player!.Character!, true); - handler.stopRagdoll(); + this.ragdollService.ragdoll(player?.Character!); }); const takeownerCommand = new TextChatCommand(); diff --git a/src/server/services/decoration.ts b/src/server/services/decoration.ts deleted file mode 100644 index 29fce38..0000000 --- a/src/server/services/decoration.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { OnStart, Service } from "@flamework/core"; -import { ReplicatedStorage } from "@rbxts/services"; -import { randomDecorationPlace } from "shared/build"; - -/** Place Decorator on map. */ -@Service() -class DecoratorService implements OnStart { - onStart(): void { - //#region Castle - const flowers = ReplicatedStorage.FindFirstChild("Decoration")?.FindFirstChild("FlowerTypes")?.GetChildren(); - if (flowers) { - randomDecorationPlace("Build_Flower", flowers as Model[]); - } - - const rocks = ReplicatedStorage.FindFirstChild("Decoration")?.FindFirstChild("RockTypes")?.GetChildren(); - if (rocks) { - randomDecorationPlace("Build_Gravel", rocks as Model[]); - } - //#endregion - } -} diff --git a/src/server/services/gameplay/ragdoll.ts b/src/server/services/gameplay/ragdoll.ts index 4a8723b..36da9a1 100644 --- a/src/server/services/gameplay/ragdoll.ts +++ b/src/server/services/gameplay/ragdoll.ts @@ -1,20 +1,181 @@ -import { OnStart, Service } from "@flamework/core"; -import { RagdollHandler } from "shared/ownership"; -import { OnPlayerJoined } from "../modding/playerjoinservice"; +import { Modding, OnStart, Service } from "@flamework/core"; +import { RunService } from "@rbxts/services"; +import Signal from "@rbxts/signal"; +import { OnPlayerJoined } from "shared/modding/player_events"; +import { releaseNetworkOwnership, takeNetworkOwner } from "shared/utils/ownership"; +import { SprintService } from "./sprint"; + +export interface OnRagdoll { + onPlayerRagdoll: (character: Model) => void; +} /** * Service related to Ragdoll. */ @Service() -class RagdollService implements OnPlayerJoined { +export class RagdollService implements OnPlayerJoined, OnStart { + private listeners = new Set(); + + constructor(private readonly sprintService: SprintService) {} + + //#region Flamework events. + onStart(): void { + Modding.onListenerAdded((object) => this.listeners.add(object)); + Modding.onListenerRemoved((object) => this.listeners.delete(object)); + } + onPlayerJoined(player: Player): void { player.CharacterAdded.Connect((character) => { - const ragdollhandler = new RagdollHandler(character); - ragdollhandler.getRagdollFallingEvent().Connect(() => { - ragdollhandler.ragdoll(); - ragdollhandler.waitMotionLess().Wait(); - ragdollhandler.stopRagdoll(); + this.prepareRagdoll(character); + this.getRagdollFallingEvent(character).Connect(() => { + this.ragdoll(character); + this.sprintService.setRunningInterdiction(player, true); + this.waitMotionLess(character).Once(() => { + this.stopRagdoll(character); + this.sprintService.setRunningInterdiction(player, false); + }); }); }); } + //#endregion + + //#region Private functions. + private getRagdollPhysic(character: Model) { + const motors = new Set(); + const constraints = new Set(); + character.GetDescendants().forEach((descendant) => { + if (descendant.IsA("Motor6D")) { + motors.add(descendant); + } + if (descendant.IsA("BallSocketConstraint") && descendant.HasTag("Ragdoll")) { + constraints.add(descendant); + } + }); + return $tuple(motors, constraints); + } + + private prepareRagdoll(character: Model) { + const children = character.GetDescendants(); + children.forEach((motor) => { + if (motor.IsA("Motor6D")) { + const ballsocketconstraint = new BallSocketConstraint(), + attachment0 = new Attachment(), + attachment1 = new Attachment(); + + attachment0.CFrame = motor.C0; + attachment0.Parent = motor.Part0; + attachment0.AddTag("Ragdoll"); + attachment0.Name = motor.Name; + + attachment1.CFrame = motor.C1; + attachment1.Parent = motor.Part1; + attachment1.AddTag("Ragdoll"); + attachment1.Name = motor.Name; + + ballsocketconstraint.Attachment0 = attachment0; + ballsocketconstraint.Attachment1 = attachment1; + ballsocketconstraint.Parent = motor.Parent; + ballsocketconstraint.AddTag("Ragdoll"); + ballsocketconstraint.Enabled = false; + + if (motor.Name === "Left Hip") { + attachment0.CFrame = attachment0.CFrame.add(new Vector3(0.5, 0, 0)); + attachment1.CFrame = attachment1.CFrame.add(new Vector3(0.5, 0, 0)); + } else if (motor.Name === "Right Hip") { + attachment0.CFrame = attachment0.CFrame.sub(new Vector3(0.5, 0, 0)); + attachment1.CFrame = attachment1.CFrame.sub(new Vector3(0.5, 0, 0)); + } + } + }); + } + + private fireOnRagdoll(character: Model) { + for (const listener of this.listeners) { + task.spawn(() => listener.onPlayerRagdoll(character)); + } + } + //#endregion + + //#region Main functions. + /** + * Ragdoll a character. + * @param character The player character to ragdoll. + */ + ragdoll(character: Model) { + this.fireOnRagdoll(character); + takeNetworkOwner(character); + const humanoid = character.WaitForChild("Humanoid", 10) as Humanoid; + const [motors, constraint] = this.getRagdollPhysic(character); + humanoid.ChangeState(Enum.HumanoidStateType.Physics); + motors.forEach((motor) => (motor.Enabled = false)); + constraint.forEach((constraint) => (constraint.Enabled = true)); + takeNetworkOwner(character); + } + + /** + * Stop a Ragdoll. + * @param character The player character to unragdoll. + */ + stopRagdoll(character: Model) { + const humanoid = character.WaitForChild("Humanoid", 10) as Humanoid; + const [motors, constraint] = this.getRagdollPhysic(character); + humanoid.ChangeState(Enum.HumanoidStateType.Running); + motors.forEach((motor) => (motor.Enabled = true)); + constraint.forEach((constraint) => (constraint.Enabled = false)); + releaseNetworkOwnership(character); + } + //#endregion + + //#region Events. + /** + * Create an Event which is fired when the player fall at an defined speed. + * @param character The player character to check. + * @param maximumSpeed The limit speed before the event fired. + * @returns A signal which is fired when the player fall at the defined speed. + */ + getRagdollFallingEvent(character: Model, maximumSpeed: number = 38) { + const humanoid = character.WaitForChild("Humanoid", 10) as Humanoid; + const root = character.FindFirstChild("HumanoidRootPart") as Part; + const event = new Signal(); + let fired = false; + let beatEvent: RBXScriptConnection; + humanoid.FreeFalling.Connect((active) => { + if (active) { + beatEvent = RunService.Heartbeat.Connect(() => { + if (root.AssemblyLinearVelocity.Y < -maximumSpeed && !fired) { + event.Fire(); + fired = true; + } + }); + } else { + beatEvent.Disconnect(); + fired = false; + } + }); + + return event; + } + + /** + * This function return a signal which fire when the player stop moving. + * @param character The player character to check. + * @param threshold The maximum distance (in studs) the player must travel in 0.3 seconds for the signal to be fired. + * @returns The signal. + */ + waitMotionLess(character: Model, threshold: number = 0.1) { + const root = character.FindFirstChild("HumanoidRootPart") as Part; + + const signal = new Signal(); + let delta: number = -1; + task.spawn(() => { + while (0 > delta || delta > threshold) { + const lastPos = root.Position; + task.wait(0.3); + delta = root.Position.sub(lastPos).Magnitude; + } + signal.Fire(); + }); + return signal; + } + //#endregion } diff --git a/src/server/services/gameplay/sprint.ts b/src/server/services/gameplay/sprint.ts index 849d861..e7295dc 100644 --- a/src/server/services/gameplay/sprint.ts +++ b/src/server/services/gameplay/sprint.ts @@ -1,22 +1,23 @@ import { OnStart, Service } from "@flamework/core"; import { Players } from "@rbxts/services"; import { GameplayServerEvents } from "server/networking"; -import { OnPlayerJoined } from "../modding/playerjoinservice"; +import { OnPlayerJoined } from "shared/modding/player_events"; /** * Service related to Sprinting. */ @Service() -class SprintService implements OnStart, OnPlayerJoined { +export class SprintService implements OnStart, OnPlayerJoined { // Sprint config. private default_stamina = 100; private base_sprint_speed = 16; private sprint_speed = 1.5; private animation_id = "rbxassetid://88297455683117"; - // Cache + // Data private is_running = new Set(); private is_sprinting = new Set(); + private cant_run = new Set(); private stamina = new Map(); private timeout = new Map(); private runanim = new Animation(); @@ -60,7 +61,7 @@ class SprintService implements OnStart, OnPlayerJoined { // Change the sprint state with networking GameplayServerEvents.sprint.connect((player, enabled) => { - if (this.timeout.has(player.UserId)) return; + if (this.timeout.has(player.UserId) && !this.cant_run.has(player.UserId)) return; const character = player.Character; enabled ? this.is_running.add(player.UserId) : this.is_running.delete(player.UserId); if (character) { @@ -75,6 +76,7 @@ class SprintService implements OnStart, OnPlayerJoined { } }); } + onPlayerJoined(player: Player): void { let running_connection: RBXScriptConnection; @@ -119,4 +121,30 @@ class SprintService implements OnStart, OnPlayerJoined { // Clean up the connection when the character died. player.CharacterRemoving.Once(() => running_connection.Disconnect()); } + + setRunning(player: Player, enabled: boolean): void { + const character = player.Character; + if (!character) throw character; + if (enabled) { + this.is_running.add(player.UserId); + } else { + this.is_running.delete(player.UserId); + this.is_sprinting.delete(player.UserId); + } + if (character) { + const humanoid = character.FindFirstChildOfClass("Humanoid"); + if (humanoid) { + if (enabled) { + humanoid.WalkSpeed = this.base_sprint_speed * this.sprint_speed; + } else { + humanoid.WalkSpeed = this.base_sprint_speed; + } + } + } + } + + setRunningInterdiction(player: Player, enabled: boolean) { + this.setRunning(player, !enabled); + enabled ? this.cant_run.add(player.UserId) : this.cant_run.delete(player.UserId); + } } diff --git a/src/server/services/modding/playerjoinservice.ts b/src/server/services/modding/player_events.ts similarity index 90% rename from src/server/services/modding/playerjoinservice.ts rename to src/server/services/modding/player_events.ts index 0ba131c..d10d91f 100644 --- a/src/server/services/modding/playerjoinservice.ts +++ b/src/server/services/modding/player_events.ts @@ -1,9 +1,6 @@ import { Modding, OnStart, Service } from "@flamework/core"; import { Players } from "@rbxts/services"; - -export interface OnPlayerJoined { - onPlayerJoined(player: Player): void; -} +import { OnPlayerJoined } from "shared/modding/player_events"; @Service() class PlayerJoinService implements OnStart { diff --git a/src/shared/build.ts b/src/shared/build.ts deleted file mode 100644 index e1c7a3b..0000000 --- a/src/shared/build.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CollectionService } from "@rbxts/services"; - -export function randomDecorationPlace(tag: string, decorations: Model[]) { - function getDecoration() { - return decorations[math.random(0, decorations.size() - 1)]; - } - - CollectionService.GetTagged(tag).forEach((part) => { - if (part.IsA("Part")) { - const deco = getDecoration().Clone(); - deco.Parent = part.Parent; - deco.MoveTo(part.Position); - part.Destroy(); - } - }); -} diff --git a/src/shared/modding/player_events.ts b/src/shared/modding/player_events.ts new file mode 100644 index 0000000..2faf0f5 --- /dev/null +++ b/src/shared/modding/player_events.ts @@ -0,0 +1,3 @@ +export interface OnPlayerJoined { + onPlayerJoined(player: Player): void; +} diff --git a/src/shared/ownership.ts b/src/shared/ownership.ts deleted file mode 100644 index c83186e..0000000 --- a/src/shared/ownership.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { RunService } from "@rbxts/services"; -import Signal from "@rbxts/signal"; - -/** - * Takes the Network Ownership of a player's character. - * @param character The character of a player. - */ -export function takeNetworkOwner(character: Model) { - const part = character.GetDescendants(); - - part.forEach((part) => { - if (part.IsA("Part")) { - part.SetNetworkOwner(undefined); - } - }); -} -/** - * Releases the Network Ownership of a player's character. - * @param character The character of a player - */ -export function releaseNetworkOwnership(character: Model) { - const part = character.GetDescendants(); - - part.forEach((part) => { - if (part.IsA("Part")) { - part.SetNetworkOwnershipAuto(); - } - }); -} - -/** - * A class which help to ragdoll the player. - */ -export class RagdollHandler { - private character: Model; - private humanoid: Humanoid; - private ragdollattachment: Attachment[] = []; - private ragdollconstraint: BallSocketConstraint[] = []; - private motors: Motor6D[] = []; - private root: Part; - private static ragdollHandlers: Map = new Map(); - - private getRagdollElements() { - return [...this.ragdollattachment, ...this.ragdollconstraint]; - } - - constructor(character: Model) { - this.character = character; - const humanoid = character.WaitForChild("Humanoid"); - const root = character.FindFirstChild("HumanoidRootPart"); - if (!humanoid?.IsA("Humanoid") || !root?.IsA("Part")) - throw '"character" isn\'t a player Character. Failed to handle ragdoll.'; - this.humanoid = humanoid; - this.root = root; - this.prepareRagdoll(); - RagdollHandler.ragdollHandlers.set(character, this); - } - - private prepareRagdoll() { - const children = this.character.GetDescendants(); - children.forEach((motor) => { - if (motor.IsA("Motor6D")) { - this.motors.push(motor); - const ballsocketconstraint = new BallSocketConstraint(), - attachment0 = new Attachment(), - attachment1 = new Attachment(); - - attachment0.CFrame = motor.C0; - attachment0.Parent = motor.Part0; - attachment0.AddTag("Ragdoll"); - attachment0.Name = motor.Name; - this.ragdollattachment.push(attachment0); - - attachment1.CFrame = motor.C1; - attachment1.Parent = motor.Part1; - attachment1.AddTag("Ragdoll"); - attachment1.Name = motor.Name; - this.ragdollattachment.push(attachment1); - - ballsocketconstraint.Attachment0 = attachment0; - ballsocketconstraint.Attachment1 = attachment1; - ballsocketconstraint.Parent = motor.Parent; - ballsocketconstraint.AddTag("Ragdoll"); - ballsocketconstraint.Enabled = false; - this.ragdollconstraint.push(ballsocketconstraint); - - if (motor.Name === "Left Hip") { - attachment0.CFrame = attachment0.CFrame.add(new Vector3(0.5, 0, 0)); - attachment1.CFrame = attachment1.CFrame.add(new Vector3(0.5, 0, 0)); - } else if (motor.Name === "Right Hip") { - attachment0.CFrame = attachment0.CFrame.sub(new Vector3(0.5, 0, 0)); - attachment1.CFrame = attachment1.CFrame.sub(new Vector3(0.5, 0, 0)); - } - } - }); - } - - /** - * Ragdoll the charactere linked to this class. - */ - ragdoll() { - takeNetworkOwner(this.character); - this.humanoid.ChangeState(Enum.HumanoidStateType.Physics); - this.motors.forEach((motor) => (motor.Enabled = false)); - this.ragdollconstraint.forEach((constraint) => (constraint.Enabled = true)); - takeNetworkOwner(this.character); - } - - /** - * Stop the Ragdoll. - */ - stopRagdoll() { - this.humanoid.ChangeState(Enum.HumanoidStateType.Running); - this.motors.forEach((motor) => (motor.Enabled = true)); - this.ragdollconstraint.forEach((constraint) => (constraint.Enabled = false)); - releaseNetworkOwnership(this.character); - } - - /** - * This function return a signal which fire when the player stop moving. - * @param threshold The maximum distance (in studs) the player must travel in 0.3 seconds for the signal to be fired. - * @returns The signal. - */ - waitMotionLess(threshold: number = 0.1) { - const signal = new Signal(); - let delta: number = -1; - task.spawn(() => { - while (0 > delta || delta > threshold) { - const lastPos = this.root.Position; - task.wait(0.3); - delta = this.root.Position.sub(lastPos).Magnitude; - } - signal.Fire(); - }); - return signal; - } - - /** - * Create an Event which is fired when the player fall at an defined speed. - * @param maximumSpeed The limit speed before the event fired. - * @returns A signal which is fired when the player fall at the defined speed. - */ - getRagdollFallingEvent(maximumSpeed: number = 38) { - const event = new Signal(); - let fired = false; - let beatEvent: RBXScriptConnection; - this.humanoid.FreeFalling.Connect((active) => { - if (active) { - beatEvent = RunService.Heartbeat.Connect(() => { - if (this.root.AssemblyLinearVelocity.Y < -maximumSpeed && !fired) { - event.Fire(); - fired = true; - } - }); - } else { - beatEvent.Disconnect(); - fired = false; - } - }); - - return event; - } - - /** - * Destroy all parts related to ragdoll. - */ - destroy() { - this.getRagdollElements().forEach((element) => element.Destroy()); - } - - static getHandlerByCharacter(character: Model, createifmissing?: false): RagdollHandler | undefined; - static getHandlerByCharacter(character: Model, createifmissing: true): RagdollHandler; - static getHandlerByCharacter(character: Model, createifmissing: boolean = false) { - return createifmissing - ? (this.ragdollHandlers.get(character) ?? new this(character)) - : this.ragdollHandlers.get(character); - } -} diff --git a/src/shared/utils/ownership.ts b/src/shared/utils/ownership.ts new file mode 100644 index 0000000..44d24ae --- /dev/null +++ b/src/shared/utils/ownership.ts @@ -0,0 +1,27 @@ +/** + * Takes the Network Ownership of a player's character. + * @param character The character of a player. + */ +export function takeNetworkOwner(character: Model) { + const part = character.GetDescendants(); + + part.forEach((part) => { + if (part.IsA("Part")) { + part.SetNetworkOwner(undefined); + } + }); +} + +/** + * Releases the Network Ownership of a player's character. + * @param character The character of a player + */ +export function releaseNetworkOwnership(character: Model) { + const part = character.GetDescendants(); + + part.forEach((part) => { + if (part.IsA("Part")) { + part.SetNetworkOwnershipAuto(); + } + }); +}