From 59efab9300d746c2ee160d88da32dfa832fc22a5 Mon Sep 17 00:00:00 2001 From: Elijah R Date: Sun, 28 Jul 2024 23:01:26 -0400 Subject: [PATCH] extended animation support, add /anim command (TODO: animation list, command help) --- msagent.js/src/agent.ts | 13 ++++++++----- protocol/src/protocol.ts | 27 +++++++++++++++++++++++++++ server/config.example.toml | 27 +++++++++++++++++++++++---- server/src/client.ts | 12 +++++++++++- server/src/config.ts | 4 ++++ server/src/room.ts | 21 ++++++++++++++++++--- webapp/src/ts/client.ts | 31 ++++++++++++++++++++++++++++--- webapp/src/ts/commands.ts | 18 ++++++++++++++++++ webapp/src/ts/main.ts | 8 +++++++- webapp/src/ts/user.ts | 12 +++++++++++- 10 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 webapp/src/ts/commands.ts diff --git a/msagent.js/src/agent.ts b/msagent.js/src/agent.ts index e2f78d9..062bc1d 100644 --- a/msagent.js/src/agent.ts +++ b/msagent.js/src/agent.ts @@ -378,6 +378,12 @@ export class Agent { if (index !== -1) this.playAnimation(index, finishCallback); } + playAnimationByNamePromise(name: string): Promise { + return new Promise((res, rej) => { + this.playAnimationByName(name, () => res()); + }); + } + setUsername(username: string, color: string) { if (this.usernameBalloonState !== null) { this.usernameBalloonState.finish(); @@ -420,13 +426,10 @@ export class Agent { this.y = randint(0, document.documentElement.clientHeight - this.data.characterInfo.charHeight); this.setLoc(); this.cnv.style.display = 'block'; - this.playAnimationByName('Show', () => {}); } hide(remove: boolean = false) { - this.playAnimationByName('Hide', () => { - if (remove) this.remove(); - else this.cnv.style.display = 'none'; - }); + if (remove) this.remove(); + else this.cnv.style.display = 'none'; } } diff --git a/protocol/src/protocol.ts b/protocol/src/protocol.ts index 780a26a..1f756dd 100644 --- a/protocol/src/protocol.ts +++ b/protocol/src/protocol.ts @@ -5,6 +5,7 @@ export enum MSAgentProtocolMessageType { KeepAlive = 'nop', Join = 'join', Talk = 'talk', + PlayAnimation = 'anim', SendImage = 'img', Admin = 'admin', // Server-to-client @@ -20,6 +21,14 @@ export interface MSAgentProtocolMessage { op: MSAgentProtocolMessageType; } +export interface AgentAnimationConfig { + join: string[]; + chat: string[]; + idle: string[]; + rest: string[]; + leave: string[]; +} + // Client-to-server export interface MSAgentJoinMessage extends MSAgentProtocolMessage { @@ -37,6 +46,14 @@ export interface MSAgentTalkMessage extends MSAgentProtocolMessage { }; } +export interface MSAgentPlayAnimationMessage extends MSAgentProtocolMessage { + op: MSAgentProtocolMessageType.PlayAnimation; + data: { + anim: string; + }; +} + + export interface MSAgentSendImageMessage extends MSAgentProtocolMessage { op: MSAgentProtocolMessageType.SendImage; data: { @@ -56,6 +73,7 @@ export interface MSAgentInitMessage extends MSAgentProtocolMessage { username: string; agent: string; admin: boolean; + animations: AgentAnimationConfig; }[]; }; } @@ -65,6 +83,7 @@ export interface MSAgentAddUserMessage extends MSAgentProtocolMessage { data: { username: string; agent: string; + animations: AgentAnimationConfig; }; } @@ -84,6 +103,14 @@ export interface MSAgentChatMessage extends MSAgentProtocolMessage { }; } +export interface MSAgentAnimationMessage extends MSAgentProtocolMessage { + op: MSAgentProtocolMessageType.PlayAnimation; + data: { + username: string; + anim: string; + }; +} + export interface MSAgentImageMessage extends MSAgentProtocolMessage { op: MSAgentProtocolMessageType.SendImage; data: { diff --git a/server/config.example.toml b/server/config.example.toml index e426a33..a718dcb 100644 --- a/server/config.example.toml +++ b/server/config.example.toml @@ -23,6 +23,7 @@ bannedWords = [] [chat.ratelimits] chat = {seconds = 10, limit = 8} +anim = {seconds = 10, limit = 8} [motd] version = 1 @@ -58,71 +59,89 @@ expirySeconds = 60 [[agents]] friendlyName = "Clippy" filename = "CLIPPIT.ACS" +animations = { join = ["Greeting"], chat = ["Explain"], idle = [], leave = ["GoodBye"], rest = ["RestPose"] } [[agents]] friendlyName = "Courtney" filename = "courtney.acs" +animations = { join = ["Show", "Greet"], chat = ["GetAttentionMinor"], idle = [], leave = ["Disappear"], rest = ["RestPose"] } [[agents]] friendlyName = "Dot" filename = "DOT.ACS" +animations = { join = ["Greeting", "Show"], chat = ["Alert", "Show"], idle = [], leave = ["Goodbye"], rest = ["RestPose"] } [[agents]] friendlyName = "Earl" filename = "earl.acs" +animations = { join = ["Show", "Greet"], chat = ["LookUp"], idle = [], leave = ["Disappear"], rest = ["RestPose"] } [[agents]] friendlyName = "F1" filename = "F1.ACS" +animations = { join = ["Show"], chat = ["Explain"], idle = [], leave = ["Hide"], rest = ["RestPose"] } [[agents]] friendlyName = "Genie" filename = "Genie.acs" +animations = { join = ["Show"], chat = ["Suggest", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] } [[agents]] friendlyName = "James" filename = "James.acs" +animations = { join = ["Show"], chat = ["Suggest", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] } [[agents]] friendlyName = "Links" filename = "Links.ACS" +animations = { join = ["Greeting"], chat = ["GetAttention"], idle = [], leave = ["GoodBye"], rest = ["RestPose"] } [[agents]] friendlyName = "Merlin" filename = "merlin.acs" +animations = { join = ["Show"], chat = ["Explain", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] } [[agents]] friendlyName = "Mother Nature" filename = "Mother_NATURE.ACS" +animations = { join = ["Greeting"], chat = ["Explain"], idle = [], leave = ["Hide"], rest = ["RestPose"] } [[agents]] friendlyName = "Office Logo" filename = "Office_Logo.ACS" +animations = { join = ["Greeting"], chat = ["Explain"], idle = [], leave = ["Goodbye"], rest = ["RestPose"] } [[agents]] friendlyName = "Peedy" filename = "Peedy.acs" +animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] } -#[[agents]] -#friendlyName = "Question" -#filename = "question_mark.acs" +[[agents]] +friendlyName = "Question" +filename = "question_mark.acs" +animations = { join = ["Welcome"], chat = ["Shimmer"], idle = [], leave = ["Fade"], rest = ["Restpose"] } [[agents]] friendlyName = "Robby" filename = "Robby.acs" +animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] } [[agents]] friendlyName = "Rocky" filename = "ROCKY.ACS" +animations = { join = ["Show"], chat = ["Alert"], idle = [], leave = ["Hide"], rest = ["RestPose"] } [[agents]] friendlyName = "Rover" filename = "rover.acs" +animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] } [[agents]] friendlyName = "Victor" filename = "Victor.acs" +animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] } [[agents]] friendlyName = "Bonzi" -filename = "Bonzi.acs" \ No newline at end of file +filename = "Bonzi.acs" +animations = { join = ["Show", "Wave", "RestPose"], chat = ["Giggle"], idle = [], leave = ["Hide"], rest = ["RestPose"] } \ No newline at end of file diff --git a/server/src/client.ts b/server/src/client.ts index 7e94303..ee7f46b 100644 --- a/server/src/client.ts +++ b/server/src/client.ts @@ -14,7 +14,8 @@ import { MSAgentJoinMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, - MSAgentTalkMessage + MSAgentTalkMessage, + MSAgentPlayAnimationMessage } from '@msagent-chat/protocol'; import { MSAgentChatRoom } from './room.js'; import * as htmlentities from 'html-entities'; @@ -27,6 +28,7 @@ export interface Client { on(event: 'join', listener: () => void): this; on(event: 'close', listener: () => void): this; on(event: 'talk', listener: (msg: string) => void): this; + on(event: 'animation', listener: (anim: string) => void): this; on(event: 'image', listener: (id: string) => void): this; on(event: string, listener: Function): this; @@ -45,6 +47,7 @@ export class Client extends EventEmitter { nopLevel: number; chatRateLimit: RateLimiter; + animRateLimit: RateLimiter; constructor(socket: WebSocket, room: MSAgentChatRoom, ip: string) { super(); @@ -58,6 +61,7 @@ export class Client extends EventEmitter { this.nopLevel = 0; this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat); + this.animRateLimit = new RateLimiter(this.room.config.ratelimits.anim); this.socket.on('message', (msg, isBinary) => { if (isBinary) { @@ -162,6 +166,12 @@ export class Client extends EventEmitter { this.emit('talk', talkMsg.data.msg); break; } + case MSAgentProtocolMessageType.PlayAnimation: { + let animMsg = msg as MSAgentPlayAnimationMessage; + if (!animMsg.data || !animMsg.data.anim || !this.animRateLimit.request()) return; + this.emit('animation', animMsg.data.anim); + break; + } case MSAgentProtocolMessageType.SendImage: { let imgMsg = msg as MSAgentSendImageMessage; if (!imgMsg.data || !imgMsg.data.id || !this.chatRateLimit.request()) return; diff --git a/server/src/config.ts b/server/src/config.ts index da7148d..af9e8cd 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,3 +1,5 @@ +import { AgentAnimationConfig } from "@msagent-chat/protocol"; + export interface IConfig { http: { host: string; @@ -31,6 +33,7 @@ export interface ChatConfig { bannedWords: string[]; ratelimits: { chat: RateLimitConfig; + anim: RateLimitConfig; }; } @@ -42,6 +45,7 @@ export interface motdConfig { export interface AgentConfig { friendlyName: string; filename: string; + animations: AgentAnimationConfig; } export interface RateLimitConfig { diff --git a/server/src/room.ts b/server/src/room.ts index 1be053f..8a17a56 100644 --- a/server/src/room.ts +++ b/server/src/room.ts @@ -1,10 +1,10 @@ import { MSAgentAddUserMessage, + MSAgentAnimationMessage, MSAgentChatMessage, MSAgentImageMessage, MSAgentInitMessage, MSAgentPromoteMessage, - MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentRemoveUserMessage } from '@msagent-chat/protocol'; @@ -52,6 +52,7 @@ export class MSAgentChatRoom { } }); client.on('join', () => { + let agent = this.agents.find(a => a.filename === client.agent)!; let initmsg: MSAgentInitMessage = { op: MSAgentProtocolMessageType.Init, data: { @@ -64,7 +65,8 @@ export class MSAgentChatRoom { return { username: c.username!, agent: c.agent!, - admin: c.admin + admin: c.admin, + animations: this.agents.find(a => a.filename === c.agent)!.animations }; }) } @@ -74,7 +76,8 @@ export class MSAgentChatRoom { op: MSAgentProtocolMessageType.AddUser, data: { username: client.username!, - agent: client.agent! + agent: client.agent!, + animations: agent.animations } }; for (const _client of this.getActiveClients().filter((c) => c !== client)) { @@ -104,6 +107,18 @@ export class MSAgentChatRoom { } this.discord?.logMsg(client.username!, message); }); + client.on('animation', async anim => { + let msg: MSAgentAnimationMessage = { + op: MSAgentProtocolMessageType.PlayAnimation, + data: { + username: client.username!, + anim: anim + } + }; + for (const _client of this.getActiveClients()) { + _client.send(msg); + } + }); client.on('image', async (id) => { if (!this.img.has(id)) return; diff --git a/webapp/src/ts/client.ts b/webapp/src/ts/client.ts index 73bbcdc..7479442 100644 --- a/webapp/src/ts/client.ts +++ b/webapp/src/ts/client.ts @@ -9,11 +9,13 @@ import { MSAgentAdminLoginResponse, MSAgentAdminMessage, MSAgentAdminOperation, + MSAgentAnimationMessage, MSAgentChatMessage, MSAgentErrorMessage, MSAgentImageMessage, MSAgentInitMessage, MSAgentJoinMessage, + MSAgentPlayAnimationMessage, MSAgentPromoteMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, @@ -174,6 +176,16 @@ export class MSAgentClient { this.send(talkMsg); } + animation(anim: string) { + let msg: MSAgentPlayAnimationMessage = { + op: MSAgentProtocolMessageType.PlayAnimation, + data: { + anim + } + }; + this.send(msg); + } + async sendImage(img: ArrayBuffer, type: string) { // Upload image let res = await fetch(this.url + '/api/upload', { @@ -296,8 +308,9 @@ export class MSAgentClient { let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + _user.agent); agent.setUsername(_user.username, _user.admin ? '#FF0000' : '#000000'); agent.addToDom(this.agentContainer); + let user = new User(_user.username, agent, _user.animations); agent.show(); - let user = new User(_user.username, agent); + user.doAnim('join'); this.setContextMenu(user); this.users.push(user); } @@ -309,8 +322,9 @@ export class MSAgentClient { let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + addUserMsg.data.agent); agent.setUsername(addUserMsg.data.username, '#000000'); agent.addToDom(this.agentContainer); + let user = new User(addUserMsg.data.username, agent, addUserMsg.data.animations); agent.show(); - let user = new User(addUserMsg.data.username, agent); + user.doAnim('join'); this.setContextMenu(user); this.users.push(user); this.events.emit('adduser', user); @@ -320,7 +334,9 @@ export class MSAgentClient { let remUserMsg = msg as MSAgentRemoveUserMessage; let user = this.users.find((u) => u.username === remUserMsg.data.username); if (!user) return; - user.agent.hide(true); + user.doAnim('leave').then(() => { + user.agent.hide(true); + }); if (this.playingAudio.has(user!.username)) { this.playingAudio.get(user!.username)?.pause(); this.playingAudio.delete(user!.username); @@ -337,6 +353,7 @@ export class MSAgentClient { this.events.emit('chat', user, chatMsg.data.message); user?.agent.speak(chatMsg.data.message); + user?.doAnim('chat'); let msgId = ++user!.msgId; if (chatMsg.data.audio !== undefined) { @@ -368,6 +385,14 @@ export class MSAgentClient { } break; } + case MSAgentProtocolMessageType.PlayAnimation: { + let animMsg = msg as MSAgentAnimationMessage; + let user = this.users.find((u) => u.username === animMsg.data.username); + if (!user || user.muted) return; + await user.agent.playAnimationByNamePromise(animMsg.data.anim); + await user.doAnim('rest'); + break; + } case MSAgentProtocolMessageType.SendImage: { let imgMsg = msg as MSAgentImageMessage; let user = this.users.find((u) => u.username === imgMsg.data.username); diff --git a/webapp/src/ts/commands.ts b/webapp/src/ts/commands.ts new file mode 100644 index 0000000..91a4d88 --- /dev/null +++ b/webapp/src/ts/commands.ts @@ -0,0 +1,18 @@ +import { MSAgentClient } from "./client.js"; + +export function InitCommands() { + +} + +export function RunCommand(command: string, room: MSAgentClient) { + let arr = command.split(' '); + let cmd = arr[0]; + let args = arr.slice(1); + switch (cmd) { + case '/anim': { + let anim = args.join(' '); + room.animation(anim); + break; + } + } +} \ No newline at end of file diff --git a/webapp/src/ts/main.ts b/webapp/src/ts/main.ts index b059d2e..708c40c 100644 --- a/webapp/src/ts/main.ts +++ b/webapp/src/ts/main.ts @@ -2,6 +2,7 @@ import { MSWindow, MSWindowStartPosition } from './MSWindow.js'; import { agentInit } from '@msagent-chat/msagent.js'; import { MSAgentClient } from './client.js'; import { Config } from '../../config.js'; +import { RunCommand } from './commands.js'; const elements = { motdWindow: document.getElementById('motdWindow') as HTMLDivElement, @@ -120,7 +121,12 @@ document.addEventListener('DOMContentLoaded', async () => { function talk() { if (Room === null) return; - Room.talk(elements.chatInput.value); + let msg = elements.chatInput.value; + if (msg.startsWith('/')) { + RunCommand(msg, Room); + } else { + Room.talk(msg); + } elements.chatInput.value = ''; } diff --git a/webapp/src/ts/user.ts b/webapp/src/ts/user.ts index 903314e..1cad4f2 100644 --- a/webapp/src/ts/user.ts +++ b/webapp/src/ts/user.ts @@ -1,4 +1,5 @@ import { Agent } from '@msagent-chat/msagent.js'; +import { AgentAnimationConfig } from '@msagent-chat/protocol'; export class User { username: string; @@ -6,11 +7,20 @@ export class User { muted: boolean; admin: boolean; msgId: number = 0; + animations: AgentAnimationConfig; - constructor(username: string, agent: Agent) { + constructor(username: string, agent: Agent, animations: AgentAnimationConfig) { this.username = username; this.agent = agent; this.muted = false; this.admin = false; + this.animations = animations; + } + + async doAnim(action: string) { + // @ts-ignore + for (let anim of this.animations[action]) { + await this.agent.playAnimationByNamePromise(anim); + } } }