@elizaos/plugin-roblox 2.0.3-beta.6 → 2.0.3-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/suite.d.ts +7 -0
- package/dist/__tests__/suite.d.ts.map +1 -0
- package/dist/actions/index.d.ts +5 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/robloxAction.d.ts +4 -0
- package/dist/actions/robloxAction.d.ts.map +1 -0
- package/dist/client/RobloxClient.d.ts +29 -0
- package/dist/client/RobloxClient.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1062 -0
- package/dist/index.js.map +19 -0
- package/dist/providers/gameStateProvider.d.ts +3 -0
- package/dist/providers/gameStateProvider.d.ts.map +1 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/services/RobloxService.d.ts +40 -0
- package/dist/services/RobloxService.d.ts.map +1 -0
- package/dist/types/index.d.ts +68 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/package.json +3 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
// types/index.ts
|
|
2
|
+
var ROBLOX_SERVICE_NAME = "roblox";
|
|
3
|
+
|
|
4
|
+
// __tests__/suite.ts
|
|
5
|
+
class RobloxTestSuite {
|
|
6
|
+
name = "roblox";
|
|
7
|
+
description = "Test suite for Roblox plugin";
|
|
8
|
+
tests = [
|
|
9
|
+
{
|
|
10
|
+
name: "Service initialization",
|
|
11
|
+
fn: async (runtime) => {
|
|
12
|
+
const service = runtime.getService(ROBLOX_SERVICE_NAME);
|
|
13
|
+
const apiKey = runtime.getSetting("ROBLOX_API_KEY");
|
|
14
|
+
const universeId = runtime.getSetting("ROBLOX_UNIVERSE_ID");
|
|
15
|
+
if (apiKey && universeId) {
|
|
16
|
+
if (!service) {
|
|
17
|
+
throw new Error("Roblox service should be initialized when API key and Universe ID are provided");
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
runtime.logger.info("Roblox service not initialized - missing configuration (expected)");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "Configuration validation",
|
|
26
|
+
fn: async (runtime) => {
|
|
27
|
+
const apiKey = runtime.getSetting("ROBLOX_API_KEY");
|
|
28
|
+
const universeId = runtime.getSetting("ROBLOX_UNIVERSE_ID");
|
|
29
|
+
if (apiKey && universeId) {
|
|
30
|
+
const service = runtime.getService(ROBLOX_SERVICE_NAME);
|
|
31
|
+
if (!service) {
|
|
32
|
+
throw new Error("Service should exist when properly configured");
|
|
33
|
+
}
|
|
34
|
+
const client = service.getClient(runtime.agentId);
|
|
35
|
+
if (!client) {
|
|
36
|
+
throw new Error("Client should exist for agent");
|
|
37
|
+
}
|
|
38
|
+
const config = client.getConfig();
|
|
39
|
+
if (config.universeId !== universeId) {
|
|
40
|
+
throw new Error("Universe ID mismatch in config");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "Actions registered",
|
|
47
|
+
fn: async (runtime) => {
|
|
48
|
+
runtime.logger.info("Roblox actions registration check passed");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// actions/robloxAction.ts
|
|
55
|
+
import {
|
|
56
|
+
logger
|
|
57
|
+
} from "@elizaos/core";
|
|
58
|
+
var actionName = "ROBLOX";
|
|
59
|
+
var ROBLOX_ACTION_TIMEOUT_MS = 15000;
|
|
60
|
+
var MAX_ROBLOX_TARGET_IDS = 25;
|
|
61
|
+
var MAX_ROBLOX_MESSAGE_LENGTH = 1000;
|
|
62
|
+
var KNOWN_GAME_ACTIONS = [
|
|
63
|
+
{
|
|
64
|
+
name: "move_npc",
|
|
65
|
+
patterns: [
|
|
66
|
+
/(?:move|walk)\s+(?:the\s+)?(?:npc|bot|agent)?\s*(?:to|towards)\s+(?:the\s+)?(\w+)/i,
|
|
67
|
+
/(?:move|walk)\s+to\s+\(?(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\)?/i
|
|
68
|
+
],
|
|
69
|
+
extractParams: (match) => {
|
|
70
|
+
if (match.length >= 4 && match[1] && match[2] && match[3]) {
|
|
71
|
+
return {
|
|
72
|
+
x: Number.parseFloat(match[1]),
|
|
73
|
+
y: Number.parseFloat(match[2]),
|
|
74
|
+
z: Number.parseFloat(match[3])
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { waypoint: match[1] ?? "" };
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "give_coins",
|
|
82
|
+
patterns: [/give\s+(?:player\s*)?(\d+)\s+(\d+)\s+coins?/i],
|
|
83
|
+
extractParams: (match) => ({
|
|
84
|
+
playerId: Number.parseInt(match[1], 10),
|
|
85
|
+
amount: Number.parseInt(match[2], 10)
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "teleport",
|
|
90
|
+
patterns: [/teleport\s+(?:everyone|all)\s+to\s+(?:the\s+)?(\w+)/i],
|
|
91
|
+
extractParams: (match) => ({ destination: match[1] })
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "spawn_entity",
|
|
95
|
+
patterns: [/spawn\s+(?:a\s+)?(\w+)\s+at\s+(\w+)/i],
|
|
96
|
+
extractParams: (match) => ({
|
|
97
|
+
entityType: match[1],
|
|
98
|
+
location: match[2]
|
|
99
|
+
})
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "start_event",
|
|
103
|
+
patterns: [/start\s+(?:a\s+)?(\w+)\s+(?:show|event|celebration)/i],
|
|
104
|
+
extractParams: (match) => ({ eventType: match[1] })
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
function isRecord(value) {
|
|
108
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
109
|
+
}
|
|
110
|
+
function parseJsonObject(text) {
|
|
111
|
+
const trimmed = text.trim();
|
|
112
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
|
|
113
|
+
return {};
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(trimmed);
|
|
116
|
+
return isRecord(parsed) ? parsed : {};
|
|
117
|
+
} catch {
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function readParams(options) {
|
|
122
|
+
const maybeParams = isRecord(options) && isRecord(options.parameters) ? options.parameters : {};
|
|
123
|
+
return maybeParams;
|
|
124
|
+
}
|
|
125
|
+
function mergedInput(message, options) {
|
|
126
|
+
return {
|
|
127
|
+
...parseJsonObject(message.content.text ?? ""),
|
|
128
|
+
...readParams(options)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function readString(params, ...keys) {
|
|
132
|
+
for (const key of keys) {
|
|
133
|
+
const value = params[key];
|
|
134
|
+
if (typeof value === "string" && value.trim())
|
|
135
|
+
return value.trim();
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function readNumber(params, ...keys) {
|
|
140
|
+
for (const key of keys) {
|
|
141
|
+
const value = params[key];
|
|
142
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
143
|
+
return value;
|
|
144
|
+
if (typeof value === "string" && value.trim()) {
|
|
145
|
+
const parsed = Number(value);
|
|
146
|
+
if (Number.isFinite(parsed))
|
|
147
|
+
return parsed;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function normalizeSubaction(value) {
|
|
153
|
+
const normalized = value?.trim().toLowerCase().replace(/[_\s-]+/g, "_");
|
|
154
|
+
if (!normalized)
|
|
155
|
+
return null;
|
|
156
|
+
if (["message", "send_message", "send", "chat", "announce"].includes(normalized)) {
|
|
157
|
+
return "message";
|
|
158
|
+
}
|
|
159
|
+
if (["execute", "action", "game_action", "run", "trigger"].includes(normalized)) {
|
|
160
|
+
return "execute";
|
|
161
|
+
}
|
|
162
|
+
if (["get_player", "player", "player_info", "user", "lookup"].includes(normalized)) {
|
|
163
|
+
return "get_player";
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function inferSubaction(text, params) {
|
|
168
|
+
const explicit = normalizeSubaction(readString(params, "subaction", "action", "type"));
|
|
169
|
+
if (explicit)
|
|
170
|
+
return explicit;
|
|
171
|
+
const lower = text.toLowerCase();
|
|
172
|
+
if (/\b(who is|look up|lookup|find|get).*\b(player|user|roblox)\b/.test(lower)) {
|
|
173
|
+
return "get_player";
|
|
174
|
+
}
|
|
175
|
+
if (/\b(execute|run|trigger|start|give|teleport|spawn|move|walk)\b/.test(lower)) {
|
|
176
|
+
return "execute";
|
|
177
|
+
}
|
|
178
|
+
if (/\b(send|message|tell|announce|chat|say)\b/.test(lower)) {
|
|
179
|
+
return "message";
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
function readTargetPlayerIds(params, text) {
|
|
184
|
+
const explicit = params.targetPlayerIds;
|
|
185
|
+
if (Array.isArray(explicit)) {
|
|
186
|
+
const ids = explicit.map((value) => typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN).filter((value) => Number.isInteger(value) && value > 0);
|
|
187
|
+
if (ids.length)
|
|
188
|
+
return ids.slice(0, MAX_ROBLOX_TARGET_IDS);
|
|
189
|
+
}
|
|
190
|
+
const single = readNumber(params, "targetPlayerId", "playerId", "userId");
|
|
191
|
+
if (single !== null && Number.isInteger(single) && single > 0)
|
|
192
|
+
return [single];
|
|
193
|
+
const matches = [...text.matchAll(/\bplayer\s*(\d+)\b/gi)];
|
|
194
|
+
return matches.length ? matches.map((match) => Number.parseInt(match[1], 10)).slice(0, MAX_ROBLOX_TARGET_IDS) : undefined;
|
|
195
|
+
}
|
|
196
|
+
function withRobloxTimeout(promise, label) {
|
|
197
|
+
return Promise.race([
|
|
198
|
+
promise,
|
|
199
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out`)), ROBLOX_ACTION_TIMEOUT_MS))
|
|
200
|
+
]);
|
|
201
|
+
}
|
|
202
|
+
function parseGameAction(text, params) {
|
|
203
|
+
const explicitActionName = readString(params, "actionName", "gameAction", "command");
|
|
204
|
+
const explicitParameters = params.parameters;
|
|
205
|
+
if (explicitActionName) {
|
|
206
|
+
return {
|
|
207
|
+
name: explicitActionName,
|
|
208
|
+
parameters: sanitizeParameters(explicitParameters),
|
|
209
|
+
targetPlayerIds: readTargetPlayerIds(params, text)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
for (const gameAction of KNOWN_GAME_ACTIONS) {
|
|
213
|
+
for (const pattern of gameAction.patterns) {
|
|
214
|
+
const match = text.match(pattern);
|
|
215
|
+
if (match) {
|
|
216
|
+
return {
|
|
217
|
+
name: gameAction.name,
|
|
218
|
+
parameters: gameAction.extractParams(match),
|
|
219
|
+
targetPlayerIds: readTargetPlayerIds(params, text)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const genericMatch = text.match(/(?:execute|run|do|trigger)\s+(\w+)/i);
|
|
225
|
+
if (genericMatch) {
|
|
226
|
+
return {
|
|
227
|
+
name: genericMatch[1].toLowerCase(),
|
|
228
|
+
parameters: {},
|
|
229
|
+
targetPlayerIds: readTargetPlayerIds(params, text)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
function sanitizeParameters(value) {
|
|
235
|
+
if (!isRecord(value))
|
|
236
|
+
return {};
|
|
237
|
+
const out = {};
|
|
238
|
+
for (const [key, item] of Object.entries(value)) {
|
|
239
|
+
if (item === null) {
|
|
240
|
+
out[key] = null;
|
|
241
|
+
} else if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
|
|
242
|
+
out[key] = item;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
function extractUserIdentifier(text, params) {
|
|
248
|
+
const userId = readNumber(params, "playerId", "userId", "id");
|
|
249
|
+
if (userId !== null && Number.isInteger(userId) && userId > 0) {
|
|
250
|
+
return { type: "id", value: userId };
|
|
251
|
+
}
|
|
252
|
+
const username = readString(params, "username", "playerName", "user");
|
|
253
|
+
if (username && !/^\d+$/.test(username)) {
|
|
254
|
+
return { type: "username", value: username };
|
|
255
|
+
}
|
|
256
|
+
const idMatch = text.match(/\b(?:player|user|id)\s*[:#]?\s*(\d{5,})\b/i);
|
|
257
|
+
if (idMatch) {
|
|
258
|
+
return { type: "id", value: Number.parseInt(idMatch[1], 10) };
|
|
259
|
+
}
|
|
260
|
+
const usernameMatch = text.match(/\b(?:user(?:name)?|player)\s*[:#]?\s*([A-Za-z0-9_]{3,20})\b/i);
|
|
261
|
+
if (usernameMatch && !/^\d+$/.test(usernameMatch[1])) {
|
|
262
|
+
return { type: "username", value: usernameMatch[1] };
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
async function handleMessage(runtime, service, message, state, params, callback) {
|
|
267
|
+
const content = readString(params, "message", "text", "content") ?? (typeof state?.message === "string" ? state.message : undefined) ?? (message.content.text ?? "").trim();
|
|
268
|
+
if (!content) {
|
|
269
|
+
await callback?.({ text: "I need a message to send to the Roblox game.", action: actionName });
|
|
270
|
+
return { success: false, error: "No message content to send" };
|
|
271
|
+
}
|
|
272
|
+
const targetPlayerIds = readTargetPlayerIds(params, content);
|
|
273
|
+
const cappedContent = content.slice(0, MAX_ROBLOX_MESSAGE_LENGTH);
|
|
274
|
+
await withRobloxTimeout(service.sendMessage(runtime.agentId, cappedContent, targetPlayerIds), "roblox message");
|
|
275
|
+
const targetText = targetPlayerIds && targetPlayerIds.length > 0 ? `to ${targetPlayerIds.length} player(s)` : "to all players";
|
|
276
|
+
await callback?.({ text: `Sent Roblox message ${targetText}.`, action: actionName });
|
|
277
|
+
return {
|
|
278
|
+
success: true,
|
|
279
|
+
text: `Sent Roblox message ${targetText}`,
|
|
280
|
+
data: { subaction: "message", targetPlayerIds, messageLength: cappedContent.length }
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function handleExecute(runtime, service, message, params, callback) {
|
|
284
|
+
const parsedAction = parseGameAction(message.content.text ?? "", params);
|
|
285
|
+
if (!parsedAction) {
|
|
286
|
+
await callback?.({ text: "Could not parse Roblox game action.", action: actionName });
|
|
287
|
+
return { success: false, error: "Could not parse Roblox game action" };
|
|
288
|
+
}
|
|
289
|
+
await withRobloxTimeout(service.executeAction(runtime.agentId, parsedAction.name, parsedAction.parameters, parsedAction.targetPlayerIds), "roblox execute");
|
|
290
|
+
await callback?.({ text: `Triggered Roblox action "${parsedAction.name}".`, action: actionName });
|
|
291
|
+
return {
|
|
292
|
+
success: true,
|
|
293
|
+
text: `Executed Roblox action "${parsedAction.name}"`,
|
|
294
|
+
data: {
|
|
295
|
+
subaction: "execute",
|
|
296
|
+
actionName: parsedAction.name,
|
|
297
|
+
parameters: parsedAction.parameters,
|
|
298
|
+
targetPlayerIds: parsedAction.targetPlayerIds
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async function handleGetPlayer(runtime, service, message, params, callback) {
|
|
303
|
+
const client = service.getClient(runtime.agentId);
|
|
304
|
+
if (!client) {
|
|
305
|
+
await callback?.({ text: "The Roblox client is not available.", action: actionName });
|
|
306
|
+
return { success: false, error: "Roblox client not found for agent" };
|
|
307
|
+
}
|
|
308
|
+
const identifier = extractUserIdentifier(message.content.text ?? "", params);
|
|
309
|
+
if (!identifier) {
|
|
310
|
+
await callback?.({
|
|
311
|
+
text: "I need a Roblox player ID or username to look up.",
|
|
312
|
+
action: actionName
|
|
313
|
+
});
|
|
314
|
+
return { success: false, error: "Could not extract Roblox player identifier" };
|
|
315
|
+
}
|
|
316
|
+
let user;
|
|
317
|
+
if (identifier.type === "id") {
|
|
318
|
+
user = await withRobloxTimeout(client.getUserById(identifier.value), "roblox get user");
|
|
319
|
+
} else {
|
|
320
|
+
user = await withRobloxTimeout(client.getUserByUsername(identifier.value), "roblox get user");
|
|
321
|
+
}
|
|
322
|
+
if (!user) {
|
|
323
|
+
await callback?.({
|
|
324
|
+
text: `No Roblox user found for ${identifier.type}: ${identifier.value}.`,
|
|
325
|
+
action: actionName
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
success: true,
|
|
329
|
+
text: `Roblox user not found for ${identifier.type}: ${identifier.value}`,
|
|
330
|
+
data: { subaction: "get_player", found: false, identifier }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const avatarUrl = await withRobloxTimeout(client.getAvatarUrl(user.id), "roblox avatar");
|
|
334
|
+
user.avatarUrl = avatarUrl;
|
|
335
|
+
await callback?.({
|
|
336
|
+
text: `${user.displayName} (@${user.username}) - Roblox user ID ${user.id}`,
|
|
337
|
+
action: actionName
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
success: true,
|
|
341
|
+
text: `Found Roblox user: ${user.displayName} (@${user.username})`,
|
|
342
|
+
data: {
|
|
343
|
+
subaction: "get_player",
|
|
344
|
+
found: true,
|
|
345
|
+
userId: user.id,
|
|
346
|
+
username: user.username,
|
|
347
|
+
displayName: user.displayName,
|
|
348
|
+
avatarUrl: avatarUrl || undefined,
|
|
349
|
+
isBanned: user.isBanned,
|
|
350
|
+
createdAt: user.createdAt ? user.createdAt.toISOString() : undefined
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
var robloxAction = {
|
|
355
|
+
name: actionName,
|
|
356
|
+
contexts: ["media", "automation"],
|
|
357
|
+
contextGate: { anyOf: ["media", "automation"] },
|
|
358
|
+
roleGate: { minRole: "USER" },
|
|
359
|
+
similes: ["ROBLOX", "ROBLOX_ROUTER", "ROBLOX_GAME_ACTION"],
|
|
360
|
+
description: "Route Roblox game integration with action message, execute, or get_player.",
|
|
361
|
+
descriptionCompressed: "Route Roblox action: message, execute, or get_player.",
|
|
362
|
+
parameters: [
|
|
363
|
+
{
|
|
364
|
+
name: "action",
|
|
365
|
+
description: "Roblox operation: message, execute, or get_player.",
|
|
366
|
+
descriptionCompressed: "Roblox action.",
|
|
367
|
+
required: true,
|
|
368
|
+
schema: { type: "string", enum: ["message", "execute", "get_player"] }
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "message",
|
|
372
|
+
description: "Message content for the message subaction.",
|
|
373
|
+
descriptionCompressed: "message text",
|
|
374
|
+
required: false,
|
|
375
|
+
schema: { type: "string" }
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: "actionName",
|
|
379
|
+
description: "Game-side action name for execute.",
|
|
380
|
+
descriptionCompressed: "game action name",
|
|
381
|
+
required: false,
|
|
382
|
+
schema: { type: "string" }
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "parameters",
|
|
386
|
+
description: "Game-side action parameters for execute.",
|
|
387
|
+
descriptionCompressed: "game action params",
|
|
388
|
+
required: false,
|
|
389
|
+
schema: { type: "object" }
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: "targetPlayerIds",
|
|
393
|
+
description: "Roblox player IDs to target for message or execute.",
|
|
394
|
+
descriptionCompressed: "target player ids",
|
|
395
|
+
required: false,
|
|
396
|
+
schema: { type: "array", items: { type: "number" } }
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "playerId",
|
|
400
|
+
description: "Roblox player/user ID for lookup or targeting.",
|
|
401
|
+
descriptionCompressed: "Roblox user id",
|
|
402
|
+
required: false,
|
|
403
|
+
schema: { type: "number" }
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: "username",
|
|
407
|
+
description: "Roblox username for lookup.",
|
|
408
|
+
descriptionCompressed: "Roblox username",
|
|
409
|
+
required: false,
|
|
410
|
+
schema: { type: "string" }
|
|
411
|
+
}
|
|
412
|
+
],
|
|
413
|
+
validate: async (runtime, message, _state) => {
|
|
414
|
+
const params = parseJsonObject(message.content.text ?? "");
|
|
415
|
+
const hasIntent = Boolean(normalizeSubaction(readString(params, "subaction", "action", "type"))) || /\b(roblox|player|user|send|message|announce|execute|trigger|teleport|spawn|coins|lookup|look up)\b/i.test(message.content.text ?? "");
|
|
416
|
+
if (!hasIntent)
|
|
417
|
+
return false;
|
|
418
|
+
const apiKey = runtime.getSetting("ROBLOX_API_KEY");
|
|
419
|
+
const universeId = runtime.getSetting("ROBLOX_UNIVERSE_ID");
|
|
420
|
+
return Boolean(apiKey && universeId);
|
|
421
|
+
},
|
|
422
|
+
handler: async (runtime, message, state, options, callback) => {
|
|
423
|
+
const service = runtime.getService(ROBLOX_SERVICE_NAME);
|
|
424
|
+
if (!service) {
|
|
425
|
+
logger.error("Roblox service not found");
|
|
426
|
+
await callback?.({ text: "Roblox service not available.", action: actionName });
|
|
427
|
+
return { success: false, error: "Roblox service not found" };
|
|
428
|
+
}
|
|
429
|
+
const params = mergedInput(message, options);
|
|
430
|
+
const maxRobloxTargetIds = MAX_ROBLOX_TARGET_IDS;
|
|
431
|
+
if (Array.isArray(params.targetPlayerIds)) {
|
|
432
|
+
params.targetPlayerIds = params.targetPlayerIds.slice(0, maxRobloxTargetIds);
|
|
433
|
+
}
|
|
434
|
+
const subaction = inferSubaction(message.content.text ?? "", params);
|
|
435
|
+
try {
|
|
436
|
+
if (subaction === "message") {
|
|
437
|
+
return await handleMessage(runtime, service, message, state, params, callback);
|
|
438
|
+
}
|
|
439
|
+
if (subaction === "execute") {
|
|
440
|
+
return await handleExecute(runtime, service, message, params, callback);
|
|
441
|
+
}
|
|
442
|
+
if (subaction === "get_player") {
|
|
443
|
+
return await handleGetPlayer(runtime, service, message, params, callback);
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
success: false,
|
|
447
|
+
error: "Missing Roblox subaction",
|
|
448
|
+
text: "Missing Roblox subaction"
|
|
449
|
+
};
|
|
450
|
+
} catch (error) {
|
|
451
|
+
logger.error({ error }, "Roblox action failed");
|
|
452
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
453
|
+
await callback?.({ text: `Roblox action failed: ${errorMessage}`, action: actionName });
|
|
454
|
+
return { success: false, error: errorMessage };
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
examples: [
|
|
458
|
+
[
|
|
459
|
+
{
|
|
460
|
+
name: "{{user1}}",
|
|
461
|
+
content: { text: "Tell everyone in Roblox that the event starts now" }
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
name: "{{agentName}}",
|
|
465
|
+
content: {
|
|
466
|
+
text: "I'll send that message to the Roblox game.",
|
|
467
|
+
action: actionName
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
]
|
|
471
|
+
]
|
|
472
|
+
};
|
|
473
|
+
var robloxAction_default = robloxAction;
|
|
474
|
+
|
|
475
|
+
// actions/index.ts
|
|
476
|
+
var robloxActions = [robloxAction_default];
|
|
477
|
+
|
|
478
|
+
// providers/gameStateProvider.ts
|
|
479
|
+
var providerName = "roblox-game-state";
|
|
480
|
+
var EXPERIENCE_NAME_LIMIT = 160;
|
|
481
|
+
var gameStateProvider = {
|
|
482
|
+
name: providerName,
|
|
483
|
+
description: "Provides information about the connected Roblox game/experience",
|
|
484
|
+
descriptionCompressed: "Read Roblox connection state, experience metadata, and messaging topic.",
|
|
485
|
+
contexts: ["automation", "agent_internal"],
|
|
486
|
+
contextGate: { anyOf: ["automation", "agent_internal"] },
|
|
487
|
+
cacheStable: false,
|
|
488
|
+
cacheScope: "turn",
|
|
489
|
+
get: async (runtime, _message, _state) => {
|
|
490
|
+
const apiKeyConfigured = Boolean(runtime.getSetting("ROBLOX_API_KEY"));
|
|
491
|
+
const universeIdSetting = runtime.getSetting("ROBLOX_UNIVERSE_ID");
|
|
492
|
+
const universeId = typeof universeIdSetting === "string" ? universeIdSetting : undefined;
|
|
493
|
+
const configured = apiKeyConfigured && Boolean(universeId);
|
|
494
|
+
try {
|
|
495
|
+
const service = runtime.getService(ROBLOX_SERVICE_NAME);
|
|
496
|
+
if (!service) {
|
|
497
|
+
return {
|
|
498
|
+
text: [
|
|
499
|
+
"Roblox:",
|
|
500
|
+
`configured: ${configured}`,
|
|
501
|
+
"service: unavailable",
|
|
502
|
+
`apiKey: ${apiKeyConfigured ? "configured" : "missing"}`,
|
|
503
|
+
`universeId: ${universeId ?? "missing"}`
|
|
504
|
+
].join(`
|
|
505
|
+
`),
|
|
506
|
+
data: {
|
|
507
|
+
configured,
|
|
508
|
+
apiKeyConfigured,
|
|
509
|
+
universeId: universeId ?? null,
|
|
510
|
+
serviceAvailable: false,
|
|
511
|
+
clientAvailable: false
|
|
512
|
+
},
|
|
513
|
+
values: {
|
|
514
|
+
configured,
|
|
515
|
+
serviceAvailable: false,
|
|
516
|
+
clientAvailable: false
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const client = service.getClient(runtime.agentId);
|
|
521
|
+
if (!client) {
|
|
522
|
+
return {
|
|
523
|
+
text: [
|
|
524
|
+
"Roblox:",
|
|
525
|
+
`configured: ${configured}`,
|
|
526
|
+
"service: available",
|
|
527
|
+
"client: unavailable",
|
|
528
|
+
`apiKey: ${apiKeyConfigured ? "configured" : "missing"}`,
|
|
529
|
+
`universeId: ${universeId ?? "missing"}`
|
|
530
|
+
].join(`
|
|
531
|
+
`),
|
|
532
|
+
data: {
|
|
533
|
+
configured,
|
|
534
|
+
apiKeyConfigured,
|
|
535
|
+
universeId: universeId ?? null,
|
|
536
|
+
serviceAvailable: true,
|
|
537
|
+
clientAvailable: false
|
|
538
|
+
},
|
|
539
|
+
values: {
|
|
540
|
+
configured,
|
|
541
|
+
serviceAvailable: true,
|
|
542
|
+
clientAvailable: false
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const config = client.getConfig();
|
|
547
|
+
let experienceInfo = null;
|
|
548
|
+
try {
|
|
549
|
+
experienceInfo = await client.getExperienceInfo();
|
|
550
|
+
} catch {
|
|
551
|
+
experienceInfo = null;
|
|
552
|
+
}
|
|
553
|
+
const parts = [
|
|
554
|
+
"Roblox:",
|
|
555
|
+
"configured: true",
|
|
556
|
+
"service: available",
|
|
557
|
+
"client: available",
|
|
558
|
+
`universeId: ${config.universeId}`
|
|
559
|
+
];
|
|
560
|
+
if (config.placeId) {
|
|
561
|
+
parts.push(`placeId: ${config.placeId}`);
|
|
562
|
+
}
|
|
563
|
+
if (experienceInfo) {
|
|
564
|
+
parts.push(`experienceName: ${experienceInfo.name.slice(0, EXPERIENCE_NAME_LIMIT)}`);
|
|
565
|
+
if (experienceInfo.playing !== undefined) {
|
|
566
|
+
parts.push(`activePlayers: ${experienceInfo.playing}`);
|
|
567
|
+
}
|
|
568
|
+
if (experienceInfo.visits !== undefined) {
|
|
569
|
+
parts.push(`totalVisits: ${experienceInfo.visits}`);
|
|
570
|
+
}
|
|
571
|
+
parts.push(`creator: ${experienceInfo.creator.name} (${experienceInfo.creator.type})`);
|
|
572
|
+
} else {
|
|
573
|
+
parts.push("experience: unavailable");
|
|
574
|
+
}
|
|
575
|
+
parts.push(`messagingTopic: ${config.messagingTopic}`);
|
|
576
|
+
if (config.dryRun) {
|
|
577
|
+
parts.push("dryRun: true");
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
text: parts.join(`
|
|
581
|
+
`),
|
|
582
|
+
data: {
|
|
583
|
+
configured: true,
|
|
584
|
+
serviceAvailable: true,
|
|
585
|
+
clientAvailable: true,
|
|
586
|
+
universeId: config.universeId,
|
|
587
|
+
placeId: config.placeId ?? null,
|
|
588
|
+
messagingTopic: config.messagingTopic,
|
|
589
|
+
dryRun: config.dryRun,
|
|
590
|
+
experienceInfo
|
|
591
|
+
},
|
|
592
|
+
values: {
|
|
593
|
+
configured: true,
|
|
594
|
+
serviceAvailable: true,
|
|
595
|
+
clientAvailable: true,
|
|
596
|
+
universeId: config.universeId,
|
|
597
|
+
placeId: config.placeId ?? null,
|
|
598
|
+
messagingTopic: config.messagingTopic,
|
|
599
|
+
dryRun: config.dryRun,
|
|
600
|
+
experienceName: experienceInfo?.name ?? null,
|
|
601
|
+
activePlayers: experienceInfo?.playing ?? null
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
} catch (error) {
|
|
605
|
+
runtime.logger.error({ error }, "Error in gameStateProvider");
|
|
606
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
607
|
+
return {
|
|
608
|
+
text: `Roblox provider error: ${message}`,
|
|
609
|
+
data: { configured, error: message },
|
|
610
|
+
values: { configured, error: true }
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// providers/index.ts
|
|
617
|
+
var robloxProviders = [gameStateProvider];
|
|
618
|
+
|
|
619
|
+
// services/RobloxService.ts
|
|
620
|
+
import { Service } from "@elizaos/core";
|
|
621
|
+
|
|
622
|
+
// client/RobloxClient.ts
|
|
623
|
+
import { logger as logger2 } from "@elizaos/core";
|
|
624
|
+
var ROBLOX_API_BASE = "https://apis.roblox.com";
|
|
625
|
+
var USERS_API_BASE = "https://users.roblox.com";
|
|
626
|
+
|
|
627
|
+
class RobloxApiError extends Error {
|
|
628
|
+
statusCode;
|
|
629
|
+
endpoint;
|
|
630
|
+
details;
|
|
631
|
+
constructor(message, statusCode, endpoint, details) {
|
|
632
|
+
super(message);
|
|
633
|
+
this.statusCode = statusCode;
|
|
634
|
+
this.endpoint = endpoint;
|
|
635
|
+
this.details = details;
|
|
636
|
+
this.name = "RobloxApiError";
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
class RobloxClient {
|
|
641
|
+
config;
|
|
642
|
+
constructor(config) {
|
|
643
|
+
this.config = config;
|
|
644
|
+
}
|
|
645
|
+
async request(endpoint, options = {}, baseUrl = ROBLOX_API_BASE) {
|
|
646
|
+
const url = `${baseUrl}${endpoint}`;
|
|
647
|
+
const headers = {
|
|
648
|
+
"x-api-key": this.config.apiKey,
|
|
649
|
+
"Content-Type": "application/json",
|
|
650
|
+
...options.headers || {}
|
|
651
|
+
};
|
|
652
|
+
const response = await fetch(url, {
|
|
653
|
+
...options,
|
|
654
|
+
headers
|
|
655
|
+
});
|
|
656
|
+
if (!response.ok) {
|
|
657
|
+
let details;
|
|
658
|
+
try {
|
|
659
|
+
details = await response.json();
|
|
660
|
+
} catch {
|
|
661
|
+
details = await response.text();
|
|
662
|
+
}
|
|
663
|
+
throw new RobloxApiError(`Roblox API error: ${response.statusText}`, response.status, endpoint, details);
|
|
664
|
+
}
|
|
665
|
+
const text = await response.text();
|
|
666
|
+
if (!text) {
|
|
667
|
+
return {};
|
|
668
|
+
}
|
|
669
|
+
return JSON.parse(text);
|
|
670
|
+
}
|
|
671
|
+
async publishMessage(topic, data, universeId) {
|
|
672
|
+
if (this.config.dryRun) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const targetUniverseId = universeId || this.config.universeId;
|
|
676
|
+
await this.request(`/messaging-service/v1/universes/${targetUniverseId}/topics/${encodeURIComponent(topic)}`, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
body: JSON.stringify({ message: JSON.stringify(data) })
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
async sendAgentMessage(message) {
|
|
682
|
+
const payload = {
|
|
683
|
+
...message.data
|
|
684
|
+
};
|
|
685
|
+
if (message.sender) {
|
|
686
|
+
payload.sender = message.sender;
|
|
687
|
+
}
|
|
688
|
+
await this.publishMessage(message.topic, payload);
|
|
689
|
+
}
|
|
690
|
+
async getDataStoreEntry(datastoreName, key, scope = "global") {
|
|
691
|
+
try {
|
|
692
|
+
const response = await this.request(`/datastores/v1/universes/${this.config.universeId}/standard-datastores/datastore/entries/entry?datastoreName=${encodeURIComponent(datastoreName)}&scope=${encodeURIComponent(scope)}&entryKey=${encodeURIComponent(key)}`);
|
|
693
|
+
return {
|
|
694
|
+
key,
|
|
695
|
+
value: JSON.parse(response.value),
|
|
696
|
+
version: response.version,
|
|
697
|
+
createdAt: new Date(response.createdTime),
|
|
698
|
+
updatedAt: new Date(response.updatedTime)
|
|
699
|
+
};
|
|
700
|
+
} catch (error) {
|
|
701
|
+
if (error instanceof RobloxApiError && error.statusCode === 404) {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
throw error;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async setDataStoreEntry(datastoreName, key, value, scope = "global") {
|
|
708
|
+
if (this.config.dryRun) {
|
|
709
|
+
logger2.info({ datastoreName, key, scope, value }, "[RobloxClient] Dry run: would set DataStore entry");
|
|
710
|
+
return {
|
|
711
|
+
key,
|
|
712
|
+
value,
|
|
713
|
+
version: "dry-run",
|
|
714
|
+
createdAt: new Date,
|
|
715
|
+
updatedAt: new Date
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
const response = await this.request(`/datastores/v1/universes/${this.config.universeId}/standard-datastores/datastore/entries/entry?datastoreName=${encodeURIComponent(datastoreName)}&scope=${encodeURIComponent(scope)}&entryKey=${encodeURIComponent(key)}`, {
|
|
719
|
+
method: "POST",
|
|
720
|
+
body: JSON.stringify(value)
|
|
721
|
+
});
|
|
722
|
+
return {
|
|
723
|
+
key,
|
|
724
|
+
value,
|
|
725
|
+
version: response.version,
|
|
726
|
+
createdAt: new Date(response.createdTime),
|
|
727
|
+
updatedAt: new Date(response.updatedTime)
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
async deleteDataStoreEntry(datastoreName, key, scope = "global") {
|
|
731
|
+
if (this.config.dryRun) {
|
|
732
|
+
logger2.info({ datastoreName, key, scope }, "[RobloxClient] Dry run: would delete DataStore entry");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
await this.request(`/datastores/v1/universes/${this.config.universeId}/standard-datastores/datastore/entries/entry?datastoreName=${encodeURIComponent(datastoreName)}&scope=${encodeURIComponent(scope)}&entryKey=${encodeURIComponent(key)}`, { method: "DELETE" });
|
|
736
|
+
}
|
|
737
|
+
async listDataStoreEntries(datastoreName, scope = "global", prefix, limit = 100) {
|
|
738
|
+
let url = `/datastores/v1/universes/${this.config.universeId}/standard-datastores/datastore/entries?datastoreName=${encodeURIComponent(datastoreName)}&scope=${encodeURIComponent(scope)}&limit=${limit}`;
|
|
739
|
+
if (prefix) {
|
|
740
|
+
url += `&prefix=${encodeURIComponent(prefix)}`;
|
|
741
|
+
}
|
|
742
|
+
const response = await this.request(url);
|
|
743
|
+
return {
|
|
744
|
+
keys: response.keys.map((k) => k.key),
|
|
745
|
+
nextPageCursor: response.nextPageCursor
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
async getUserById(userId) {
|
|
749
|
+
const response = await this.request(`/v1/users/${userId}`, {}, USERS_API_BASE);
|
|
750
|
+
return {
|
|
751
|
+
id: response.id,
|
|
752
|
+
username: response.name,
|
|
753
|
+
displayName: response.displayName,
|
|
754
|
+
createdAt: new Date(response.created),
|
|
755
|
+
isBanned: response.isBanned
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
async getUserByUsername(username) {
|
|
759
|
+
try {
|
|
760
|
+
const response = await this.request(`/v1/usernames/users`, {
|
|
761
|
+
method: "POST",
|
|
762
|
+
body: JSON.stringify({
|
|
763
|
+
usernames: [username],
|
|
764
|
+
excludeBannedUsers: false
|
|
765
|
+
})
|
|
766
|
+
}, USERS_API_BASE);
|
|
767
|
+
if (response.data.length === 0) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
const user = response.data[0];
|
|
771
|
+
return {
|
|
772
|
+
id: user.id,
|
|
773
|
+
username: user.name,
|
|
774
|
+
displayName: user.displayName
|
|
775
|
+
};
|
|
776
|
+
} catch {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
async getUsersByIds(userIds) {
|
|
781
|
+
if (userIds.length === 0) {
|
|
782
|
+
return [];
|
|
783
|
+
}
|
|
784
|
+
const response = await this.request(`/v1/users`, {
|
|
785
|
+
method: "POST",
|
|
786
|
+
body: JSON.stringify({ userIds, excludeBannedUsers: false })
|
|
787
|
+
}, USERS_API_BASE);
|
|
788
|
+
return response.data.map((user) => ({
|
|
789
|
+
id: user.id,
|
|
790
|
+
username: user.name,
|
|
791
|
+
displayName: user.displayName
|
|
792
|
+
}));
|
|
793
|
+
}
|
|
794
|
+
async getAvatarUrl(userId, size = "150x150") {
|
|
795
|
+
try {
|
|
796
|
+
const response = await this.request(`/v1/users/avatar-headshot?userIds=${userId}&size=${size}&format=Png`, {}, "https://thumbnails.roblox.com");
|
|
797
|
+
return response.data[0]?.imageUrl;
|
|
798
|
+
} catch {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async getExperienceInfo(universeId) {
|
|
803
|
+
const targetUniverseId = universeId || this.config.universeId;
|
|
804
|
+
const response = await this.request(`/v1/games?universeIds=${targetUniverseId}`, {}, "https://games.roblox.com");
|
|
805
|
+
const experience = response.data[0];
|
|
806
|
+
if (!experience) {
|
|
807
|
+
throw new RobloxApiError(`Experience not found: ${targetUniverseId}`, 404, `/v1/games?universeIds=${targetUniverseId}`);
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
universeId: targetUniverseId,
|
|
811
|
+
name: experience.name,
|
|
812
|
+
description: experience.description,
|
|
813
|
+
creator: {
|
|
814
|
+
id: experience.creator.id,
|
|
815
|
+
type: experience.creator.type,
|
|
816
|
+
name: experience.creator.name
|
|
817
|
+
},
|
|
818
|
+
playing: experience.playing,
|
|
819
|
+
visits: experience.visits,
|
|
820
|
+
rootPlaceId: String(experience.rootPlaceId)
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
getConfig() {
|
|
824
|
+
return { ...this.config };
|
|
825
|
+
}
|
|
826
|
+
isDryRun() {
|
|
827
|
+
return this.config.dryRun;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// utils/config.ts
|
|
832
|
+
var ROBLOX_DEFAULTS = {
|
|
833
|
+
MESSAGING_TOPIC: "eliza-agent",
|
|
834
|
+
DRY_RUN: false
|
|
835
|
+
};
|
|
836
|
+
function hasRobloxEnabled(runtime) {
|
|
837
|
+
const apiKey = runtime.getSetting("ROBLOX_API_KEY");
|
|
838
|
+
const universeId = runtime.getSetting("ROBLOX_UNIVERSE_ID");
|
|
839
|
+
return Boolean(apiKey && universeId);
|
|
840
|
+
}
|
|
841
|
+
function validateRobloxConfig(runtime) {
|
|
842
|
+
const apiKey = runtime.getSetting("ROBLOX_API_KEY");
|
|
843
|
+
const universeId = runtime.getSetting("ROBLOX_UNIVERSE_ID");
|
|
844
|
+
if (!apiKey) {
|
|
845
|
+
throw new Error("ROBLOX_API_KEY is required but not configured");
|
|
846
|
+
}
|
|
847
|
+
if (!universeId) {
|
|
848
|
+
throw new Error("ROBLOX_UNIVERSE_ID is required but not configured");
|
|
849
|
+
}
|
|
850
|
+
const placeId = runtime.getSetting("ROBLOX_PLACE_ID");
|
|
851
|
+
const webhookSecret = runtime.getSetting("ROBLOX_WEBHOOK_SECRET");
|
|
852
|
+
const messagingTopic = runtime.getSetting("ROBLOX_MESSAGING_TOPIC") || ROBLOX_DEFAULTS.MESSAGING_TOPIC;
|
|
853
|
+
const dryRunStr = runtime.getSetting("ROBLOX_DRY_RUN");
|
|
854
|
+
const dryRun = dryRunStr === "true";
|
|
855
|
+
return {
|
|
856
|
+
apiKey,
|
|
857
|
+
universeId,
|
|
858
|
+
placeId,
|
|
859
|
+
webhookSecret,
|
|
860
|
+
messagingTopic,
|
|
861
|
+
dryRun
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// services/RobloxService.ts
|
|
866
|
+
class RobloxAgentManager {
|
|
867
|
+
runtime;
|
|
868
|
+
client;
|
|
869
|
+
config;
|
|
870
|
+
isRunning = false;
|
|
871
|
+
constructor(runtime, config) {
|
|
872
|
+
this.runtime = runtime;
|
|
873
|
+
this.config = config;
|
|
874
|
+
this.client = new RobloxClient(config);
|
|
875
|
+
}
|
|
876
|
+
async start() {
|
|
877
|
+
if (this.isRunning) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
this.isRunning = true;
|
|
881
|
+
this.runtime.logger.info({ universeId: this.config.universeId }, "Roblox agent manager started");
|
|
882
|
+
}
|
|
883
|
+
async stop() {
|
|
884
|
+
this.isRunning = false;
|
|
885
|
+
this.runtime.logger.info("Roblox agent manager stopped");
|
|
886
|
+
}
|
|
887
|
+
async sendMessage(content, targetPlayerIds) {
|
|
888
|
+
await this.client.sendAgentMessage({
|
|
889
|
+
topic: this.config.messagingTopic,
|
|
890
|
+
data: {
|
|
891
|
+
type: "agent_message",
|
|
892
|
+
content,
|
|
893
|
+
targetPlayerIds,
|
|
894
|
+
timestamp: Date.now()
|
|
895
|
+
},
|
|
896
|
+
sender: {
|
|
897
|
+
agentId: this.runtime.agentId,
|
|
898
|
+
agentName: this.runtime.character.name ?? this.runtime.agentId
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
async executeAction(actionName2, parameters, targetPlayerIds) {
|
|
903
|
+
await this.client.sendAgentMessage({
|
|
904
|
+
topic: this.config.messagingTopic,
|
|
905
|
+
data: {
|
|
906
|
+
type: "agent_action",
|
|
907
|
+
action: actionName2,
|
|
908
|
+
parameters,
|
|
909
|
+
targetPlayerIds,
|
|
910
|
+
timestamp: Date.now()
|
|
911
|
+
},
|
|
912
|
+
sender: {
|
|
913
|
+
agentId: this.runtime.agentId,
|
|
914
|
+
agentName: this.runtime.character.name ?? this.runtime.agentId
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
class RobloxService extends Service {
|
|
921
|
+
static instance;
|
|
922
|
+
managers = new Map;
|
|
923
|
+
static serviceType = ROBLOX_SERVICE_NAME;
|
|
924
|
+
description = "Roblox integration service for game communication";
|
|
925
|
+
capabilityDescription = "The agent can communicate with Roblox games and players";
|
|
926
|
+
static getInstance() {
|
|
927
|
+
if (!RobloxService.instance) {
|
|
928
|
+
RobloxService.instance = new RobloxService;
|
|
929
|
+
}
|
|
930
|
+
return RobloxService.instance;
|
|
931
|
+
}
|
|
932
|
+
async initialize(runtime) {
|
|
933
|
+
await RobloxService.start(runtime);
|
|
934
|
+
}
|
|
935
|
+
static async start(runtime) {
|
|
936
|
+
const service = RobloxService.getInstance();
|
|
937
|
+
let manager = service.managers.get(runtime.agentId);
|
|
938
|
+
if (manager) {
|
|
939
|
+
runtime.logger.warn({ agentId: runtime.agentId }, "Roblox service already started");
|
|
940
|
+
return service;
|
|
941
|
+
}
|
|
942
|
+
if (!hasRobloxEnabled(runtime)) {
|
|
943
|
+
runtime.logger.debug({ agentId: runtime.agentId }, "Roblox service not enabled - missing API key or Universe ID");
|
|
944
|
+
return service;
|
|
945
|
+
}
|
|
946
|
+
const robloxConfig = validateRobloxConfig(runtime);
|
|
947
|
+
manager = new RobloxAgentManager(runtime, robloxConfig);
|
|
948
|
+
service.managers.set(runtime.agentId, manager);
|
|
949
|
+
await manager.start();
|
|
950
|
+
runtime.logger.success({ agentId: runtime.agentId, universeId: robloxConfig.universeId }, "Roblox service started");
|
|
951
|
+
return service;
|
|
952
|
+
}
|
|
953
|
+
static async stop(runtime) {
|
|
954
|
+
const service = RobloxService.getInstance();
|
|
955
|
+
const manager = service.managers.get(runtime.agentId);
|
|
956
|
+
if (manager) {
|
|
957
|
+
await manager.stop();
|
|
958
|
+
service.managers.delete(runtime.agentId);
|
|
959
|
+
runtime.logger.info({ agentId: runtime.agentId }, "Roblox service stopped");
|
|
960
|
+
} else {
|
|
961
|
+
runtime.logger.debug({ agentId: runtime.agentId }, "Roblox service not running");
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async stop() {
|
|
965
|
+
for (const manager of Array.from(this.managers.values())) {
|
|
966
|
+
const agentId = manager.runtime.agentId;
|
|
967
|
+
manager.runtime.logger.debug("Stopping Roblox service");
|
|
968
|
+
try {
|
|
969
|
+
await RobloxService.stop(manager.runtime);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
manager.runtime.logger.error({ agentId, error }, "Error stopping Roblox service");
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
getManager(agentId) {
|
|
976
|
+
return this.managers.get(agentId);
|
|
977
|
+
}
|
|
978
|
+
getClient(agentId) {
|
|
979
|
+
return this.managers.get(agentId)?.client;
|
|
980
|
+
}
|
|
981
|
+
async sendMessage(agentId, content, targetPlayerIds) {
|
|
982
|
+
const manager = this.managers.get(agentId);
|
|
983
|
+
if (!manager) {
|
|
984
|
+
throw new Error(`No Roblox manager found for agent ${agentId}`);
|
|
985
|
+
}
|
|
986
|
+
await manager.sendMessage(content, targetPlayerIds);
|
|
987
|
+
}
|
|
988
|
+
async executeAction(agentId, actionName2, parameters, targetPlayerIds) {
|
|
989
|
+
const manager = this.managers.get(agentId);
|
|
990
|
+
if (!manager) {
|
|
991
|
+
throw new Error(`No Roblox manager found for agent ${agentId}`);
|
|
992
|
+
}
|
|
993
|
+
await manager.executeAction(actionName2, parameters, targetPlayerIds);
|
|
994
|
+
}
|
|
995
|
+
async healthCheck() {
|
|
996
|
+
const managerStatuses = {};
|
|
997
|
+
let overallHealthy = true;
|
|
998
|
+
for (const [agentId, manager] of Array.from(this.managers.entries())) {
|
|
999
|
+
try {
|
|
1000
|
+
const experienceInfo = await manager.client.getExperienceInfo();
|
|
1001
|
+
managerStatuses[agentId] = {
|
|
1002
|
+
status: "healthy",
|
|
1003
|
+
universeId: manager.config.universeId,
|
|
1004
|
+
experienceName: experienceInfo.name,
|
|
1005
|
+
playing: experienceInfo.playing
|
|
1006
|
+
};
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
managerStatuses[agentId] = {
|
|
1009
|
+
status: "unhealthy",
|
|
1010
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1011
|
+
};
|
|
1012
|
+
overallHealthy = false;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
healthy: overallHealthy,
|
|
1017
|
+
details: {
|
|
1018
|
+
activeManagers: this.managers.size,
|
|
1019
|
+
managerStatuses
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
getActiveManagers() {
|
|
1024
|
+
return new Map(this.managers);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// index.ts
|
|
1029
|
+
var robloxPlugin = {
|
|
1030
|
+
name: "roblox",
|
|
1031
|
+
description: "Roblox game integration plugin for sending and receiving messages",
|
|
1032
|
+
services: [RobloxService],
|
|
1033
|
+
actions: robloxActions,
|
|
1034
|
+
providers: robloxProviders,
|
|
1035
|
+
tests: [new RobloxTestSuite],
|
|
1036
|
+
init: async (_config, runtime) => {
|
|
1037
|
+
const apiKey = runtime.getSetting("ROBLOX_API_KEY");
|
|
1038
|
+
const universeId = runtime.getSetting("ROBLOX_UNIVERSE_ID");
|
|
1039
|
+
if (!apiKey || apiKey.trim() === "") {
|
|
1040
|
+
runtime.logger.warn("ROBLOX_API_KEY not provided");
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (!universeId || universeId.trim() === "") {
|
|
1044
|
+
runtime.logger.warn("ROBLOX_UNIVERSE_ID not provided");
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
runtime.logger.info({ universeId }, "Roblox plugin initialized");
|
|
1048
|
+
},
|
|
1049
|
+
async dispose(runtime) {
|
|
1050
|
+
await runtime.getService(RobloxService.serviceType)?.stop();
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
var plugin_roblox_default = robloxPlugin;
|
|
1054
|
+
export {
|
|
1055
|
+
robloxPlugin,
|
|
1056
|
+
plugin_roblox_default as default,
|
|
1057
|
+
RobloxService,
|
|
1058
|
+
RobloxClient,
|
|
1059
|
+
RobloxApiError
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
//# debugId=3495E3233296F8F764756E2164756E21
|