-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathcharacter-service.ts
192 lines (158 loc) · 5.78 KB
/
character-service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import type { OnStart } from "@flamework/core";
import { Service } from "@flamework/core";
import type { Logger } from "@rbxts/log";
import { PhysicsService } from "@rbxts/services";
import { promiseTree } from "@rbxts/validate-tree";
import type PlayerEntity from "server/player/player-entity";
import type { ListenerData } from "shared/util/flamework-util";
import { setupLifecycle } from "shared/util/flamework-util";
import { addToCollisionGroup } from "shared/util/physics-util";
import {
CHARACTER_LOAD_TIMEOUT,
type CharacterRig,
characterSchema,
loadCharacter,
onCharacterAdded,
} from "shared/util/player-util";
import CollisionGroup from "types/enum/collision-group";
import Tag from "types/enum/tag";
import type { OnPlayerJoin } from "../player-service";
PhysicsService.RegisterCollisionGroup(CollisionGroup.Character);
export interface OnCharacterAdded {
/** Fires when a character is added to the game. */
onCharacterAdded(character: CharacterRig, playerEntity: PlayerEntity): void;
}
/**
* A service for managing character rigs in the game. This service listens for
* when a player's character is added to the game and ensures that the character
* rig is loaded and fully exists according to the schema before allowing any
* other systems to interact with it. We also handle retries for loading the
* character rig in case it fails to load.
*/
@Service({})
export default class CharacterService implements OnStart, OnPlayerJoin {
private readonly characterAddedEvents = new Array<ListenerData<OnCharacterAdded>>();
private readonly characterRigs = new Map<Player, CharacterRig>();
constructor(private readonly logger: Logger) {}
/** @ignore */
public onStart(): void {
setupLifecycle<OnCharacterAdded>(this.characterAddedEvents);
}
/** @ignore */
public onPlayerJoin(playerEntity: PlayerEntity): void {
const { janitor, player } = playerEntity;
janitor.Add(
onCharacterAdded(player, character => {
janitor.AddPromise(this.characterAdded(playerEntity, character)).catch(err => {
this.logger.Fatal(`Could not get character rig because:\n${err}`);
});
}),
);
}
/**
* Returns the character rig associated with the given player, if it exists.
*
* @param player - The player whose character rig to retrieve.
* @returns The character rig associated with the player, or undefined if it
* does not exist.
*/
public getCharacterRig(player: Player): CharacterRig | undefined {
return this.characterRigs.get(player);
}
/**
* This method wraps a callback and replaces the first argument (that must
* be of type `Player`) with that players `character rig`.
*
* @param func - The callback to wrap.
* @returns A new callback that replaces the first argument with the
* player's character rig.
*/
public withPlayerRig<T extends Array<unknown>, R = void>(
func: (playerRig: CharacterRig, ...args: T) => R,
) {
return (player: Player, ...args: T): R | undefined => {
const playerRig = this.getCharacterRig(player);
if (!playerRig) {
this.logger.Info(`Could not get character rig for ${player.UserId}`);
return;
}
return func(playerRig, ...args);
};
}
private async characterAdded(playerEntity: PlayerEntity, model: Model): Promise<void> {
const promise = promiseTree(model, characterSchema);
const { player } = playerEntity;
// If our character fails to load, we want to cancel the promise and
// attempt to load it again.
const timeout = Promise.delay(CHARACTER_LOAD_TIMEOUT).then(async () => {
promise.cancel();
return this.retryCharacterLoad(player);
});
// If our character is removed before it loads, we want to cancel.
const connection = model.AncestryChanged.Connect(() => {
if (model.IsDescendantOf(game)) {
return;
}
promise.cancel();
});
const [success, rig] = promise.await();
timeout.cancel();
connection.Disconnect();
if (!success) {
throw `Could not get character rig for ${player.UserId}`;
}
this.listenForCharacterRemoving(player, model);
this.onRigLoaded(playerEntity, rig);
}
private listenForCharacterRemoving(player: Player, character: Model): void {
const connection = character.AncestryChanged.Connect(() => {
if (character.IsDescendantOf(game)) {
return;
}
this.logger.Verbose(`Character ${character.GetFullName()} has been removed.`);
connection.Disconnect();
this.characterRemoving(player);
});
}
private onRigLoaded(playerEntity: PlayerEntity, rig: CharacterRig): void {
const { name, janitor, player, userId } = playerEntity;
janitor.Add(addToCollisionGroup(rig, CollisionGroup.Character, true), true);
rig.AddTag(Tag.PlayerCharacter);
this.characterRigs.set(player, rig);
this.logger.Debug(`Loaded character rig for ${name}`);
debug.profilebegin("Lifecycle_Character_Added");
for (const { id, event } of this.characterAddedEvents) {
janitor
.Add(
Promise.defer(() => {
debug.profilebegin(id);
event.onCharacterAdded(rig, playerEntity);
}),
)
.catch(err => {
this.logger.Error(`Error in character lifecycle ${id}: ${err}`);
});
}
debug.profileend();
janitor.AddPromise(this.characterAppearanceLoaded(player, rig)).catch(err => {
this.logger.Info(
`Character appearance did not load for ${userId}, with reason: ${err}`,
);
});
}
private async characterAppearanceLoaded(player: Player, rig: CharacterRig): Promise<void> {
if (!player.HasAppearanceLoaded()) {
await Promise.fromEvent(player.CharacterAppearanceLoaded).timeout(
CHARACTER_LOAD_TIMEOUT,
);
}
rig.Head.AddTag(Tag.PlayerHead);
}
private characterRemoving(player: Player): void {
this.characterRigs.delete(player);
}
private async retryCharacterLoad(player: Player): Promise<void> {
this.logger.Warn(`Getting full rig for ${player.UserId} timed out. Retrying...`);
return loadCharacter(player);
}
}