Moved some interfaces out of services to decouple

+ More OOP changes on the server side
This commit is contained in:
loplkc loplkc 2022-01-29 23:04:01 -05:00
parent 47bde9f3ce
commit 79202b9034
11 changed files with 260 additions and 134 deletions

View file

@ -1,5 +1,5 @@
import { Input, Output } from "shared/Remotes"; import { Input, Output } from "shared/Remotes";
export function bindToOutput(functionToBind: Callback) { export function bindToServerMessage(functionToBind: Callback) {
assert(Output?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil'); assert(Output?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil');
Output.OnClientEvent.Connect(functionToBind); Output.OnClientEvent.Connect(functionToBind);
} }

View file

@ -24,7 +24,7 @@ interface effectHandler extends effectMaker, effectRunner {
} }
export function makeEffectHandler(effectFolder: Folder) { export function makeEffectHandler(effectFolder: Folder) {
const effectHandler: effectHandler = { return {
meshPartEffect: function(meshPart: MeshPart, material: Enum.Material, effectKeypoints: effectKeypoint[], priority?: number) { meshPartEffect: function(meshPart: MeshPart, material: Enum.Material, effectKeypoints: effectKeypoint[], priority?: number) {
const effectMeshPart = meshPart.Clone(); const effectMeshPart = meshPart.Clone();
effectMeshPart.Material = material; effectMeshPart.Material = material;
@ -102,8 +102,7 @@ export function makeEffectHandler(effectFolder: Folder) {
EFFECT_FOLDER: effectFolder, EFFECT_FOLDER: effectFolder,
effectsToRun: [], effectsToRun: [],
} } as effectHandler;
return effectHandler;
} }
/* /*
local function horseEffModule(o,typ,a1,a2,a3,a4,a5,a6,a7) local function horseEffModule(o,typ,a1,a2,a3,a4,a5,a6,a7)

View file

@ -0,0 +1,96 @@
const Players = game.GetService("Players");
const UserInputService = game.GetService("UserInputService");
const ContextActionService = game.GetService("ContextActionService");
const Workspace = game.GetService("Workspace");
const CAMERA = Workspace.CurrentCamera as Camera;
assert(CAMERA, 'Camera of "' + Players.LocalPlayer.DisplayName + '"does not exist! (HOW???)');
export type inputBindings = {
[input in keyof controlBindings]?: [((argument1?: unknown, argument2?: unknown) => void), unknown, unknown];
};
export interface inputBinder {
assignControlBindings: (controlBindings: controlBindings) => void;
assignInputBindings: (inputBindings: inputBindings) => void;
removeInputBindings: (inputBindings: inputBindings) => void;
}
interface inputHandler extends inputBinder {
controlHandler: (actionName: string, state: Enum.UserInputState, inputObject: InputObject) => void;
controlBindings?: controlBindings
boundInputs: inputBindings
storedInput?: string // + compound inputs
}
const hitParams = new RaycastParams();
//hitParams.FilterDescendantsInstances = {efFolder,Plr.Character}
hitParams.FilterType = Enum.RaycastFilterType.Blacklist;
function getMouseLocation(): [Vector3, Vector3, Instance | undefined] {
//hitParams.FilterDescendantsInstances = {efFolder,Plr.Character}
const mouseLocation = UserInputService.GetMouseLocation();
const unitRay = CAMERA.ViewportPointToRay(mouseLocation.X, mouseLocation.Y);
const cast = Workspace.Raycast(unitRay.Origin, unitRay.Direction.mul(1000), hitParams);
if (cast) {
return [cast.Position, cast.Normal, cast.Instance];
} else {
return [unitRay.Origin.add(unitRay.Direction.mul(1000)), new Vector3(0, 0, 0), undefined];
}
}
function isValidInput(controlBindings: controlBindings, value: string): value is keyof controlBindings {
return value in controlBindings; // uh oh
}
//function(actionName: string, state: Enum.UserInputState, inputObject: InputObject) {
// inputParameters[0](inputParameters[1])
//}
export function makeInputHandler() {
const t: inputHandler = {//return {
assignControlBindings: function(controlBindings: controlBindings) {
this.controlBindings = controlBindings // WOW!!!!!
},
assignInputBindings: function(inputBindings: inputBindings) {
const controlBindings = this.controlBindings;
if (controlBindings) {
for (let input in inputBindings) {
if (isValidInput(controlBindings, input)) {
// + Check for this.boundInputs[input]
const inputParameters = inputBindings[input]
if (inputParameters) {
this.boundInputs[input] = inputParameters;
const controlArray = controlBindings[input];
for (const control of controlArray) {
ContextActionService.BindAction(input, this.controlHandler, false, control)
}
}
}
}
}
},
removeInputBindings: function(inputBindings: inputBindings) {},
controlHandler: function(actionName: string, state: Enum.UserInputState, inputObject: InputObject) {
const controlBindings = this.controlBindings;
if (controlBindings) {
if (isValidInput(controlBindings, actionName)) {
const inputParameters = this.boundInputs[actionName]
if (inputParameters) {
inputParameters[0](inputParameters[1], inputParameters[2])
}
}
}
},
boundInputs: {}
} //as inputHandler;
}
/*
function handleInput(input: InputObject, otherInteraction: boolean) {
let mousePosition: Vector3, mouseNormal: Vector3, mouseInstance: Instance | undefined;
[mousePosition, mouseNormal, mouseInstance] = getMouseLocation(); // eslint-disable-line prefer-const
if (input.UserInputType === Enum.UserInputType.MouseButton1) {
messageServer("move", mousePosition);
}
}
*/
// UserInputService.InputBegan.Connect(handleInput);

View file

@ -1,9 +1,7 @@
// "init": The local script. This script doesn't have to account for any other players. // "init": The main client-side thread.
const Players = game.GetService("Players"); const Players = game.GetService("Players");
const UserInputService = game.GetService("UserInputService");
const RunService = game.GetService("RunService"); const RunService = game.GetService("RunService");
const Workspace = game.GetService("Workspace"); import { bindToServerMessage, messageServer } from "./ClientMessenger";
import { bindToOutput, messageServer } from "./ClientMessenger";
import { handleGuiInput, drawGui, closeGui } from "./GuiHandler"; import { handleGuiInput, drawGui, closeGui } from "./GuiHandler";
import { makeEffectHandler, effectRunner } from "./EffectMaker"; import { makeEffectHandler, effectRunner } from "./EffectMaker";
const LOCALPLAYER = Players.LocalPlayer; const LOCALPLAYER = Players.LocalPlayer;
@ -12,8 +10,6 @@ assert(
PLAYERGUI && classIs(PLAYERGUI, "PlayerGui"), PLAYERGUI && classIs(PLAYERGUI, "PlayerGui"),
'PlayerGui of "' + LOCALPLAYER.DisplayName + '"does not exist! (HOW???)', 'PlayerGui of "' + LOCALPLAYER.DisplayName + '"does not exist! (HOW???)',
); );
const CAMERA = Workspace.CurrentCamera as Camera;
assert(CAMERA, 'Camera of "' + LOCALPLAYER.DisplayName + '"does not exist! (HOW???)');
let inMainMenu = true; let inMainMenu = true;
function openMainMenu(playerGui: PlayerGui) { function openMainMenu(playerGui: PlayerGui) {
@ -26,29 +22,7 @@ function openMainMenu(playerGui: PlayerGui) {
} }
} }
const hitParams = new RaycastParams(); function handleServerMessage(messageType: unknown, messageContent: unknown) {
//hitParams.FilterDescendantsInstances = {efFolder,Plr.Character}
//hitParams.FilterType = Enum.RaycastFilterType.Blacklist;
function getMouseLocation(): [Vector3, Vector3, Instance | undefined] {
//hitParams.FilterDescendantsInstances = {efFolder,Plr.Character}
const mouseLocation = UserInputService.GetMouseLocation();
const unitRay = CAMERA.ViewportPointToRay(mouseLocation.X, mouseLocation.Y);
const cast = Workspace.Raycast(unitRay.Origin, unitRay.Direction.mul(1000), hitParams);
if (cast) {
return [cast.Position, cast.Normal, cast.Instance];
} else {
return [unitRay.Origin.add(unitRay.Direction.mul(1000)), new Vector3(0, 0, 0), undefined];
}
}
function handleInput(input: InputObject, otherInteraction: boolean) {
let mousePosition: Vector3, mouseNormal: Vector3, mouseInstance: Instance | undefined;
[mousePosition, mouseNormal, mouseInstance] = getMouseLocation(); // eslint-disable-line prefer-const
if (input.UserInputType === Enum.UserInputType.MouseButton1) {
messageServer("move", mousePosition);
}
}
function handleOutput(messageType: unknown, messageContent: unknown) {
if (messageType === "init") { if (messageType === "init") {
openMainMenu(PLAYERGUI); openMainMenu(PLAYERGUI);
inMainMenu = true; inMainMenu = true;
@ -57,12 +31,12 @@ function handleOutput(messageType: unknown, messageContent: unknown) {
inMainMenu = false; inMainMenu = false;
} }
} }
// Action phase // Bind functions
UserInputService.InputBegan.Connect(handleInput);
bindToOutput(handleOutput); bindToServerMessage(handleServerMessage);
const effectRunners: effectRunner[] = []; const effectRunners: effectRunner[] = [];
// Put stuff in the effectRunners table // + Put stuff in the effectRunners table
RunService.RenderStepped.Connect(function(deltaTime) { RunService.RenderStepped.Connect(function(deltaTime) {
effectRunners.forEach(effectRunner => { effectRunners.forEach(effectRunner => {
effectRunner.runEffects(deltaTime) effectRunner.runEffects(deltaTime)

View file

@ -1,13 +1,13 @@
import { Input, Output } from "shared/Remotes"; import { Input, Output } from "shared/Remotes";
export function bindToInput(functionToBind: Callback) { export function bindToClientMessage(functionToBind: Callback) {
assert(Input?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil'); assert(Input?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil');
Input.OnServerEvent.Connect(functionToBind); Input.OnServerEvent.Connect(functionToBind);
} }
export function messageClient(client: Player, messageType: serverMessageType, messageContent?: string) { export function messageClient(client: Player, messageType: string, messageContent?: string) {
assert(Output?.IsA("RemoteEvent"), 'Remote event "Output" is of incorrect class or nil'); assert(Output?.IsA("RemoteEvent"), 'Remote event "Output" is of incorrect class or nil');
Output.FireClient(client, messageType, messageContent); Output.FireClient(client, messageType, messageContent);
} }
export function messageAllClients(messageType: serverMessageType, messageContent?: string) { export function messageAllClients(messageType: string, messageContent?: string) {
assert(Output?.IsA("RemoteEvent"), 'Remote event "Output" is of incorrect class or nil'); assert(Output?.IsA("RemoteEvent"), 'Remote event "Output" is of incorrect class or nil');
Output.FireAllClients(messageType, messageContent); Output.FireAllClients(messageType, messageContent);
} }

View file

@ -1,24 +1,23 @@
// "main": This is the core of reality. It serves as the highest-level abstraction. // "main": This is the core of reality. It serves as the highest-level abstraction.
// + Prevent this from coupling with the entity manager, if possible // + Prevent this from coupling with the entity manager, if possible
const Players = game.GetService("Players"); const Players = game.GetService("Players");
import { makeEntity, moveEntity } from "shared/EntityManager"; import { makePlayerStorage, playerStorage, storedPlayer } from "shared/PlayerManager";
import { initPlayer, deinitPlayer, loadInPlayer, teleportPlayer } from "shared/PlayerManager"; import { bindToClientMessage, messageClient, messageAllClients } from "./ServerMessenger";
import { bindToInput, messageClient, messageAllClients } from "./ServerMessenger"; const playerStorage: playerStorage = makePlayerStorage();
const playerStorage: (playerStorageEntry | undefined)[] = [];
const entityStorage: entity[] = [];
function addPlayer(player: Player) { function addPlayer(player: Player) {
playerStorage[player.UserId] = initPlayer(player); playerStorage.initPlayer(player);
messageClient(player, "init", "idk"); messageClient(player, "init", "idk");
} }
function removePlayer(player: Player) { function removePlayer(player: Player) {
playerStorage[player.UserId] = deinitPlayer(playerStorage[player.UserId], player); playerStorage.deinitPlayer(player);
} }
// Handling input is a complicated process that requires passing a large variety of data to a large variety of places, so it's staying here for now // Handling input is a complicated process that requires passing a large variety of data to a large variety of places, so it's staying here for now
function handleInput(player: Player, messageType: unknown, messageContent: unknown) { function handleClientMessage(player: Player, messageType: unknown, messageContent: unknown) {
const storedPlayer = playerStorage.fetchPlayer(player);
if (messageType === "EnterGame") { if (messageType === "EnterGame") {
try { try {
entityStorage[player.UserId] = loadInPlayer(player); storedPlayer.loadIn();
messageClient(player, "enterGame"); messageClient(player, "enterGame");
} catch (thrownError) { } catch (thrownError) {
if (typeIs(thrownError, "string")) { if (typeIs(thrownError, "string")) {
@ -30,11 +29,11 @@ function handleInput(player: Player, messageType: unknown, messageContent: unkno
} }
} else if (messageType === "move") { } else if (messageType === "move") {
if (typeIs(messageContent, "Vector3")) { if (typeIs(messageContent, "Vector3")) {
moveEntity(entityStorage[player.UserId], messageContent); storedPlayer.setPosition(messageContent)
} }
} }
} }
// Action phase // Action phase
Players.PlayerAdded.Connect(addPlayer); Players.PlayerAdded.Connect(addPlayer);
Players.PlayerRemoving.Connect(removePlayer); Players.PlayerRemoving.Connect(removePlayer);
bindToInput(handleInput); bindToClientMessage(handleClientMessage);

41
src/services.d.ts vendored
View file

@ -1,33 +1,8 @@
type puppetEntry = ["Character", Player] | ["Placeholder", "Placeholder"]; /*
type bodyPart = "root" | "torso" | "head" | "leftArm" | "rightArm" | "leftLeg" | "rightLeg"; type bodyPart = "root" | "torso" | "head" | "leftArm" | "rightArm" | "leftLeg" | "rightLeg";
type serverMessageType = "init" | "promptError" | "enterGame"; type serverMessageType = "init" | "promptError" | "enterGame";
type clientMessageType = "move" | "placeholder"; type clientMessageType = "move" | "placeholder";
// + Why can the client see all of these?
interface saveDataEntry {
placeholder: string;
}
interface playerStorageEntry {
inMainMenu: boolean;
// + Other data that is unique to players but does not persist between sessions
saveData: saveDataEntry;
entity?: entity;
}
interface puppet {
entry: puppetEntry;
model: Model;
rootPart: Part;
//placeholder: (x: string) => string; // + "Puppet string" functions will (not?) go here
}
interface entity {
baseStats: [number, number, number, number]; // MaxHealth, Attack, Speed, Defense (things used for calculation, only modified by buffs and debuffs)
baseAmounts: [number, number]; // Health, Barrier (things of indescribable importance)
puppet: puppet;
}
interface event {
winEvents?: event[]; // A list of events that need to return true (in sequence) to complete this event
winEntities?: entity[]; // A list of entities that need to die to complete the event
timeout?: number; // A timeout for the event; passes a lose condition if there are other completion requirements that have not been satisfied
}
type meshType = "Ball"; type meshType = "Ball";
type effectState = [CFrame, Vector3, Color3, number]; // The number is transparency type effectState = [CFrame, Vector3, Color3, number]; // The number is transparency
@ -36,6 +11,18 @@ type effectEntry = [meshType, EnumItem, effectState[]]; // The enumitem is mater
interface modeLocal { interface modeLocal {
aura?: [effectEntry, bodyPart?, number?][]; // effect, part it is attached to (default root), how many times it should be called per frame (default 1) aura?: [effectEntry, bodyPart?, number?][]; // effect, part it is attached to (default root), how many times it should be called per frame (default 1)
} }
*/
// Genuinely require being on both sides - but in the services file? No shot!
type acceptedControls = Enum.KeyCode[] // + Include controller "keys"
interface controlBindings {
clicker1: acceptedControls; // What is used to click on things (enemies in game, UI elements)
diamond1: acceptedControls; // Diamond controls
diamond2: acceptedControls;
diamond3: acceptedControls;
diamond4: acceptedControls;
special1: acceptedControls; // Special controls
}
/*interface hookInEntry { /*interface hookInEntry {
name: string; name: string;

View file

@ -1,18 +1,27 @@
// "EntityManager": Create entities objects and deck them out with functions to use. // "EntityManager": Create entities objects and deck them out with functions to use.
// + Functions are here, as to avoid storing unecessary data in the server store. // + Functions are here, as to avoid storing unecessary data in the server store.
import { makePuppet, movePuppet } from "./Puppetmaster"; import { makePuppet, puppet, puppetEntry } from "./Puppetmaster";
export function makeEntity(puppetEntry: puppetEntry) { type stats = [maxHealth: number, attack: number, speed: number, defense: number] // values used for calculation, only modified by buffs and debuffs
const newEntity: entity = { type amounts = [health: number, barrier: number] // values used to store an entity's current status (more existential than stats)
baseStats: [0, 0, 0, 0],
baseAmounts: [0, 0], export interface entity {
puppet: makePuppet(puppetEntry), setPosition: (location: Vector3) => void;
};
return newEntity;
} }
// This exists because the main server should never see the puppets, but it is a bit weird class entityHandler implements entity {
export function moveEntity(entity: entity | undefined, location: Vector3) { constructor(baseStats: stats, baseAmounts: amounts, puppetEntry: puppetEntry) {
if (entity) { this.baseStats = baseStats;
entity.puppet = movePuppet(entity.puppet, location); this.baseAmounts = baseAmounts;
this.puppet = makePuppet(puppetEntry);
};
setPosition(location: Vector3) {
this.puppet.movePuppet(location);
} }
baseStats: stats;
baseAmounts: amounts; // Health, Barrier (things of indescribable importance)
puppet: puppet;
}
export function makeEntity(puppetEntry: puppetEntry, baseStats = [100, 1, 16, 0] as stats, baseAmounts = [100, 0] as amounts) {
return new entityHandler(baseStats, baseAmounts, puppetEntry);
} }

View file

@ -1,6 +1,11 @@
// "The": Handle events. // "The": Handle events.
// WORST CONDITION RigHT NOW // WORST CONDITION RigHT NOW
import { makeEntity } from "./EntityManager"; import { makeEntity, entity } from "./EntityManager";
interface event {
winEvents?: event[]; // A list of events that need to return true (in sequence) to complete this event
winEntities?: entity[]; // A list of entities that need to die to complete the event
timeout?: number; // A timeout for the event; passes a lose condition if there are other completion requirements that have not been satisfied
}
export function runEvent(event: event) { export function runEvent(event: event) {
let complete = false; let complete = false;
const startTime = os.clock(); const startTime = os.clock();

View file

@ -1,28 +1,66 @@
// "PlayerManager": Handle the data of players. This involves receiving them when they arrive, cleaning up after they exit, teleporting them, etc. // "PlayerManager": Handle the data of players. This involves receiving them when they arrive, cleaning up after they exit, teleporting them, etc.
import { makeEntity } from "./EntityManager"; import { makeEntity, entity } from "./EntityManager";
export function initPlayer(player: Player) { interface saveDataEntry { // + May need to move this to archiver
const newEntry: playerStorageEntry = { placeholder: string;
inMainMenu: true, }
saveData: { export interface storedPlayer {
placeholder: "placeholder", teleportToServer: () => void;
}, setPosition: (location: Vector3) => void;
loadIn: () => void;
}
class storedPlayerHandler implements storedPlayer {
constructor(player: Player) {
this.player = player;
this.inMainMenu = true;
this.saveData = {placeholder: "fortnite"};
}
teleportToServer() {
// + Do checking related to where the player is allowed to go
// + Teleport player to other server, sending a message to have them load in automatically
}; };
// + Load player's datastore into server store setPosition(location: Vector3) {
return newEntry; // Return the entry to be put into the server store if (this.entity) {
this.entity.setPosition(location);
}
};
loadIn() {
this.entity = makeEntity(["Character", this.player]);
// + Give the entity the stats it's supposed to have, load from save data maybe?
};
player: Player;
inMainMenu: boolean;
// + Other data that is unique to players but does not persist between sessions
saveData: saveDataEntry;
entity?: entity;
} }
export function deinitPlayer(entry: playerStorageEntry | undefined, player: Player) {
assert(entry, "Trying to remove entry of player " + player.DisplayName + ", but entry does not exist!"); export interface playerStorage {
// ? Tell the entity to unload, if it still exists (the entity will tell the other clients to remove the player) initPlayer: (player: Player) => void;
// + Unload player's server store to datastores deinitPlayer: (player: Player) => void;
return undefined; // A nil entry to replace the entry to be wiped and maybe a success value in a wrapper fetchPlayer: (player: Player) => storedPlayer;
} }
export function loadInPlayer(player: Player) {
const entity = makeEntity(["Character", player]); class playerStorageHandler implements playerStorage {
// + Give the entity the stats it's supposed to have, load from save data maybe? constructor() {};
return entity; initPlayer(player: Player) {
} this.playerStorageArray[player.UserId] = new storedPlayerHandler(player);
export function teleportPlayer() { // + Load player's datastore into server store
// + Do checking related to where the player is allowed to go };
// + Teleport player to other server, sending a message to have them load in automatically deinitPlayer(player: Player) {
const entry = this.playerStorageArray[player.UserId];
assert(entry, "Trying to remove entry of player " + player.DisplayName + ", but entry does not exist!");
// ? Tell the entity to unload, if it still exists (the entity will tell the other clients to remove the player)
// + Unload player's server store to datastores
return undefined; // A nil entry to replace the entry to be wiped and maybe a success value in a wrapper
};
fetchPlayer(player: Player) {
return this.playerStorageArray[player.UserId];
}
playerStorageArray: storedPlayerHandler[] = [];
} }
export function makePlayerStorage() {
return new playerStorageHandler();
}

View file

@ -5,34 +5,53 @@ const puppetLibraries = {
["Placeholder"]: m1, ["Placeholder"]: m1,
}; };
export function makePuppet(puppetEntry: puppetEntry) { export type puppetEntry = ["Character", Player] | ["Placeholder", "Placeholder"];
export interface puppet {
movePuppet: (location: Vector3) => void; // + Success value maybe?
}
/* interface puppetHandler extends puppet {
entry: puppetEntry;
} */
/* interface completePuppetHandler extends puppetHandler {
model: Model;
rootPart: Part;
} */
function makePuppetModel(puppetEntry: puppetEntry) {
if (puppetEntry[0] === "Character") { if (puppetEntry[0] === "Character") {
const model: [Model, Part] = puppetLibraries[puppetEntry[0]].makeModel(puppetEntry[1]); return puppetLibraries[puppetEntry[0]].makeModel(puppetEntry[1]) as [Model, Part];
return {
entry: puppetEntry,
model: model[0],
rootPart: model[1],
};
} else { } else {
throw 'Invalid puppet type "' + puppetEntry[0] + '"!'; throw 'Invalid puppet type "' + puppetEntry[0] + '"!';
} }
} }
function verifyPuppetExistence(puppet: puppet) { function verifyPuppetExistence(puppetHandler: puppetHandler)/*: puppetHandler is completePuppetHandler */{ // + Making this do type checking is currently beyond me
if (puppet.rootPart.Parent) { if (!puppetHandler.rootPart || !puppetHandler.rootPart.Parent) {
// Placeholder; puppet integrity will include other body parts print("Regenerating puppet!");
return puppet; [puppetHandler.model, puppetHandler.rootPart] = makePuppetModel(puppetHandler.entry);
} else {
print("No puppet!");
return makePuppet(puppet.entry);
} }
// return true;
} }
class puppetHandler implements puppet {
constructor(puppetEntry: puppetEntry) {
this.entry = puppetEntry;
};
export function movePuppet(puppet: puppet, location: Vector3) { movePuppet(location: Vector3) {
print("executing puppet move"); print("executing puppet move");
puppet = verifyPuppetExistence(puppet); //const newPuppet = verifyPuppetExistence(puppet); verifyPuppetExistence(this);
//puppet.rootPart = newPuppet.rootPart; if (!this.rootPart) { // + Remove this once you get better at typescript
//puppet.model = newPuppet.model; throw "Fornite";
puppet.rootPart.CFrame = new CFrame(location); }
return puppet; this.rootPart.CFrame = new CFrame(location);
}
entry: puppetEntry;
model?: Model;
rootPart?: Part;
};
export function makePuppet(puppetEntry: puppetEntry) {
return new puppetHandler(puppetEntry);// return newPuppet as puppet;
} }