add player nicknames for socket clients
This commit is contained in:
parent
1b84edfd50
commit
416b3cc5d2
|
@ -221,7 +221,7 @@ def launch_bot():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
event = event_queue.get()
|
event = event_queue.get()
|
||||||
logger.info("broadcasting event %s", event.type)
|
logger.debug("broadcasting %s event", event.type)
|
||||||
|
|
||||||
if client:
|
if client:
|
||||||
event_task = client.loop.create_task(broadcast_event(event))
|
event_task = client.loop.create_task(broadcast_event(event))
|
||||||
|
@ -343,7 +343,7 @@ def embed_from_player(event: PlayerEvent):
|
||||||
description = f"{event.client} is now playing as {event.character}"
|
description = f"{event.client} is now playing as {event.character}"
|
||||||
else:
|
else:
|
||||||
title = "Player Left"
|
title = "Player Left"
|
||||||
description = f"{event.client} has left the game, {event.character} will be played by the AI"
|
description = f"{event.client} has left the game. {event.character} is now controlled by an LLM"
|
||||||
|
|
||||||
player_embed = Embed(title=title, description=description)
|
player_embed = Embed(title=title, description=description)
|
||||||
return player_embed
|
return player_embed
|
||||||
|
|
|
@ -3,7 +3,7 @@ from collections import deque
|
||||||
from json import dumps, loads
|
from json import dumps, loads
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Literal
|
from typing import Dict, Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
@ -27,6 +27,11 @@ logger = getLogger(__name__)
|
||||||
connected = set()
|
connected = set()
|
||||||
recent_events = deque(maxlen=100)
|
recent_events = deque(maxlen=100)
|
||||||
last_snapshot = None
|
last_snapshot = None
|
||||||
|
player_names: Dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_player_name(client_id: str) -> str:
|
||||||
|
return player_names.get(client_id, client_id)
|
||||||
|
|
||||||
|
|
||||||
async def handler(websocket):
|
async def handler(websocket):
|
||||||
|
@ -70,15 +75,40 @@ async def handler(websocket):
|
||||||
try:
|
try:
|
||||||
# if this socket is attached to a character and that character's turn is active, wait for input
|
# if this socket is attached to a character and that character's turn is active, wait for input
|
||||||
message = await websocket.recv()
|
message = await websocket.recv()
|
||||||
logger.info(f"Received message for {id}: {message}")
|
player_name = get_player_name(id)
|
||||||
|
logger.info(f"Received message for {player_name}: {message}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = loads(message)
|
data = loads(message)
|
||||||
message_type = data.get("type", None)
|
message_type = data.get("type", None)
|
||||||
if message_type == "player":
|
if message_type == "player":
|
||||||
|
if "name" in data:
|
||||||
|
new_player_name = data["name"]
|
||||||
|
existing_id = next(
|
||||||
|
(
|
||||||
|
k
|
||||||
|
for k, v in player_names.items()
|
||||||
|
if v == new_player_name
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if existing_id is not None:
|
||||||
|
logger.error(
|
||||||
|
f"Name {new_player_name} is already in use by {existing_id}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"changing player name for {id} to {new_player_name}"
|
||||||
|
)
|
||||||
|
player_names[id] = new_player_name
|
||||||
|
|
||||||
|
elif "become" in data:
|
||||||
character_name = data["become"]
|
character_name = data["become"]
|
||||||
if has_player(character_name):
|
if has_player(character_name):
|
||||||
logger.error(f"Character {character_name} is already in use")
|
logger.error(
|
||||||
|
f"Character {character_name} is already in use"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# TODO: should this always remove?
|
# TODO: should this always remove?
|
||||||
|
@ -96,18 +126,22 @@ async def handler(websocket):
|
||||||
)
|
)
|
||||||
llm_agent = llm_agent.fallback_agent
|
llm_agent = llm_agent.fallback_agent
|
||||||
|
|
||||||
# player_name = data["player"]
|
|
||||||
player = RemotePlayer(
|
player = RemotePlayer(
|
||||||
actor.name, actor.backstory, sync_turn, fallback_agent=llm_agent
|
actor.name,
|
||||||
|
actor.backstory,
|
||||||
|
sync_turn,
|
||||||
|
fallback_agent=llm_agent,
|
||||||
)
|
)
|
||||||
set_player(id, player)
|
set_player(id, player)
|
||||||
logger.info(f"Client {id} is now character {character_name}")
|
logger.info(
|
||||||
|
f"Client {player_name} is now character {character_name}"
|
||||||
|
)
|
||||||
|
|
||||||
# swap out the LLM agent
|
# swap out the LLM agent
|
||||||
set_actor_agent(actor.name, actor, player)
|
set_actor_agent(actor.name, actor, player)
|
||||||
|
|
||||||
# notify all clients that this character is now active
|
# notify all clients that this character is now active
|
||||||
player_event(character_name, id, "join")
|
player_event(character_name, player_name, "join")
|
||||||
player_list()
|
player_list()
|
||||||
elif message_type == "input":
|
elif message_type == "input":
|
||||||
player = get_player(id)
|
player = get_player(id)
|
||||||
|
@ -129,8 +163,9 @@ async def handler(websocket):
|
||||||
if player and isinstance(player, RemotePlayer):
|
if player and isinstance(player, RemotePlayer):
|
||||||
remove_player(id)
|
remove_player(id)
|
||||||
|
|
||||||
logger.info("Disconnecting player for %s", player.name)
|
player_name = get_player_name(id)
|
||||||
player_event(player.name, id, "leave")
|
logger.info("Disconnecting player %s from %s", player_name, player.name)
|
||||||
|
player_event(player.name, player_name, "leave")
|
||||||
player_list()
|
player_list()
|
||||||
|
|
||||||
actor, _ = get_actor_agent_for_name(player.name)
|
actor, _ = get_actor_agent_for_name(player.name)
|
||||||
|
@ -138,7 +173,7 @@ async def handler(websocket):
|
||||||
logger.info("Restoring LLM agent for %s", player.name)
|
logger.info("Restoring LLM agent for %s", player.name)
|
||||||
set_actor_agent(player.name, actor, player.fallback_agent)
|
set_actor_agent(player.name, actor, player.fallback_agent)
|
||||||
|
|
||||||
logger.info("Client disconnected: %s", id)
|
logger.info("Client disconnected: %s", player_name)
|
||||||
|
|
||||||
|
|
||||||
socket_thread = None
|
socket_thread = None
|
||||||
|
|
|
@ -20,6 +20,8 @@ import {
|
||||||
Switch,
|
Switch,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
|
TextField,
|
||||||
|
IconButton,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Allotment } from 'allotment';
|
import { Allotment } from 'allotment';
|
||||||
|
|
||||||
|
@ -30,6 +32,8 @@ import { PlayerPanel } from './player.js';
|
||||||
import 'allotment/dist/style.css';
|
import 'allotment/dist/style.css';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
import { HistoryPanel } from './history.js';
|
import { HistoryPanel } from './history.js';
|
||||||
|
import { set } from 'lodash';
|
||||||
|
import { CheckBox, Save } from '@mui/icons-material';
|
||||||
|
|
||||||
const useWebSocket = (useWebSocketModule as any).default;
|
const useWebSocket = (useWebSocketModule as any).default;
|
||||||
|
|
||||||
|
@ -74,15 +78,23 @@ export function DetailDialog(props: { setDetails: SetDetails; details: Maybe<Ite
|
||||||
}
|
}
|
||||||
|
|
||||||
export function App(props: AppProps) {
|
export function App(props: AppProps) {
|
||||||
|
// client state - slice 1
|
||||||
const [ activeTurn, setActiveTurn ] = useState<boolean>(false);
|
const [ activeTurn, setActiveTurn ] = useState<boolean>(false);
|
||||||
const [ autoScroll, setAutoScroll ] = useState<boolean>(true);
|
const [ autoScroll, setAutoScroll ] = useState<boolean>(true);
|
||||||
|
const [ themeMode, setThemeMode ] = useState('light');
|
||||||
|
|
||||||
|
// client identity - slice 2
|
||||||
|
const [ clientId, setClientId ] = useState<string>('');
|
||||||
|
const [ clientName, setClientName ] = useState<string>('');
|
||||||
|
|
||||||
|
// world state - slice 3
|
||||||
const [ detailEntity, setDetailEntity ] = useState<Maybe<Item | Actor | Room>>(undefined);
|
const [ detailEntity, setDetailEntity ] = useState<Maybe<Item | Actor | Room>>(undefined);
|
||||||
const [ character, setCharacter ] = useState<Maybe<Actor>>(undefined);
|
const [ character, setCharacter ] = useState<Maybe<Actor>>(undefined);
|
||||||
const [ clientId, setClientId ] = useState<string>('');
|
|
||||||
const [ world, setWorld ] = useState<Maybe<World>>(undefined);
|
const [ world, setWorld ] = useState<Maybe<World>>(undefined);
|
||||||
const [ themeMode, setThemeMode ] = useState('light');
|
|
||||||
const [ history, setHistory ] = useState<Array<string>>([]);
|
|
||||||
const [ players, setPlayers ] = useState<Record<string, string>>({});
|
const [ players, setPlayers ] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
// socket stuff
|
||||||
|
const [ history, setHistory ] = useState<Array<string>>([]);
|
||||||
const { lastMessage, readyState, sendMessage } = useWebSocket(props.socketUrl);
|
const { lastMessage, readyState, sendMessage } = useWebSocket(props.socketUrl);
|
||||||
|
|
||||||
function setPlayer(actor: Maybe<Actor>) {
|
function setPlayer(actor: Maybe<Actor>) {
|
||||||
|
@ -99,6 +111,11 @@ export function App(props: AppProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendName(name: string) {
|
||||||
|
sendMessage(JSON.stringify({ type: 'player', name: clientName }));
|
||||||
|
setClientName(name);
|
||||||
|
}
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
mode: themeMode as PaletteMode,
|
mode: themeMode as PaletteMode,
|
||||||
|
@ -175,6 +192,22 @@ export function App(props: AppProps) {
|
||||||
sx={{ marginLeft: 'auto' }}
|
sx={{ marginLeft: 'auto' }}
|
||||||
/>} label="Auto Scroll" />
|
/>} label="Auto Scroll" />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<FormGroup row>
|
||||||
|
<TextField
|
||||||
|
label="Player Name"
|
||||||
|
value={clientName}
|
||||||
|
onChange={(e) => setClientName(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
sendName(clientName);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ marginLeft: 'auto' }}
|
||||||
|
/>
|
||||||
|
<IconButton onClick={() => sendName(clientName)}>
|
||||||
|
<Save />
|
||||||
|
</IconButton>
|
||||||
|
</FormGroup>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Alert>
|
</Alert>
|
||||||
<Stack direction="row" spacing={2}>
|
<Stack direction="row" spacing={2}>
|
||||||
|
|
|
@ -16,6 +16,12 @@
|
||||||
- [Reply Events](#reply-events)
|
- [Reply Events](#reply-events)
|
||||||
- [Result Events](#result-events)
|
- [Result Events](#result-events)
|
||||||
- [Status Events](#status-events)
|
- [Status Events](#status-events)
|
||||||
|
- [Server-specific Events](#server-specific-events)
|
||||||
|
- [Websocket Server Events](#websocket-server-events)
|
||||||
|
- [Websocket New Client](#websocket-new-client)
|
||||||
|
- [Websocket Player Become Character](#websocket-player-become-character)
|
||||||
|
- [Websocket Player Input](#websocket-player-input)
|
||||||
|
- [Websocket Player Name](#websocket-player-name)
|
||||||
|
|
||||||
## Event Types
|
## Event Types
|
||||||
|
|
||||||
|
@ -88,3 +94,53 @@ more frequent progress updates when generating with slow models.
|
||||||
### Result Events
|
### Result Events
|
||||||
|
|
||||||
### Status Events
|
### Status Events
|
||||||
|
|
||||||
|
## Server-specific Events
|
||||||
|
|
||||||
|
### Websocket Server Events
|
||||||
|
|
||||||
|
The websocket server has a few unique message types that it uses to communicate metadata with socket clients.
|
||||||
|
|
||||||
|
#### Websocket New Client
|
||||||
|
|
||||||
|
Notify a new client of its unique ID.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: "id"
|
||||||
|
id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
This is an outgoing event from the server to clients.
|
||||||
|
|
||||||
|
#### Websocket Player Become Character
|
||||||
|
|
||||||
|
A socket client wants to play as a character in the world.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: "player"
|
||||||
|
become: str
|
||||||
|
```
|
||||||
|
|
||||||
|
This is an incoming event from clients to the server.
|
||||||
|
|
||||||
|
#### Websocket Player Input
|
||||||
|
|
||||||
|
A socket client has sent some input, usually in response to a prompt.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: "input"
|
||||||
|
input: str
|
||||||
|
```
|
||||||
|
|
||||||
|
This is an incoming event from clients to the server.
|
||||||
|
|
||||||
|
#### Websocket Player Name
|
||||||
|
|
||||||
|
Update the player name attached to a socket client.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: "player"
|
||||||
|
name: str
|
||||||
|
```
|
||||||
|
|
||||||
|
This is an incoming event from clients to the server.
|
||||||
|
|
Loading…
Reference in New Issue