Files
2026-10-29 02:20:20 -07:00

9.8 KiB

What a room needs from the server (client 554.8)

Findings for T5DE-hosted rooms (design.md): what the client asks and expects when it joins and uses a chat room, so the hub (through the proxy) can play IMVU's part for rooms it hosts. From the client's code and a capture of the test account joining an empty public room, chatting once and leaving (2026-10-29). Our own description; IDs are placeholders.

Two channels

A room uses both of the client's connections to IMVU:

  • XML-RPC (chat.imvu.com/api/xmlrpc/chat.php for chat.*, client-dynamic.imvu.com/api/xmlrpc/client.php for test.*): joining, the room's scene, the participant list, leaving.
  • IMQ, the real-time connection (one per client, through the proxy as plaintext): everything said and done in the room, as messages on a queue per chat.

Joining a room

In order, as captured:

  1. test.getRoomCardInfo({room_instance_id}): the room's card (name, owner, flags like allowApProducts, allowLoadNewRoom). Room instance IDs look like <owner id>-<n>.
  2. chat.getOrMakeChat({userId, activity: "publicroom-<room instance id>", chatId: 0, publicroom: true, private}): returns chatId (an integer), seat, lastMessageId, participantUrl, partnerId. A "declined" response carries reason and explanation (shown as an error). A "waiting" response makes the client wait and retry with inviteId.
  3. test.getRoomState({room_instance_id}): the scene: room_hash, and room_state.room_contents, a list of [furniture instance ID, product ID, placement], where placement is a string of numbers (position, rotation, scale, parent slot...). The client caches it by hash.
  4. IMQ: subscribe to /chat/<chatId> (with property listen_to_self: false). The gateway answers with JoinedQueue and creates three mounts: control and messages (message type) and participants (state type). Then subscribe to inv:/room/room-<room instance id>, which has one node mount (message type); nothing arrived on it in a session without room edits.
  5. Chat commands sent on joining (below): *msg SeatAssignment ..., *seat <n>, *resume <userId> (or *accept <userId>).
  6. chat.getParticipants({chatId, userId}), then again about every 15 seconds: [{userId, seat, ...}]. People join and leave in the client when they appear in or drop out of this list; messages from people not in it are ignored.

Leaving: chat.terminateChat(userId, chatId). Booting: chat.bootOutOfChat({userId, ...}). Invitations: chat.attemptInvite, acceptInvite, declineInvite, leaveInviteAwayNote. Also seen: test.getBuddyState, test.updateAvatarDownloadSize.

The room list

  • Rooms come from client-dynamic.imvu.com/api/rooms/rooms_list_paginated.php (signed in; recent=1 for recently visited, search parameters otherwise). The response is column-oriented JSON: one array per field (customers_room_id, name, customers_name, num_participants, max_users, participants, image_url, description, rating, language, is_ap, is_av, is_vip, ...), plus number_of_rooms, rooms_per_page and welcome-room fields. The proxy can add hosted rooms by appending to every array.
  • For live counts, the list subscribes to room_occupancy/<room instance id> for each room shown (the client re-subscribes them as the list refreshes). Each is a state mount occupants whose properties are the people in the room, keyed by user ID; the gateway sends StateChange from user admin when someone joins (value set) or leaves (value empty).
  • Joining by link (imvu:room/...) calls test.getJoinPublicRoomInfo({publicroom_url}), which returns the room instance ID.

IMQ

Frames are protobuf Framing{type, data} back to back (t5de-proxy internal/imq). Message types and fields, as the client defines them (field numbers in brackets):

Type Fields
C2gConnect 1302 version [1], user_id [2] (string), cookie [3] (bytes, from login), op_id [4], metadata [5] (key, value)
G2cChallenge 3103 version [1], challenge [2]
C2gChallengeResponse 1303 op_id [1], response [2] (from the challenge and an auth token from login)
G2cResult 3102 op_id [1], status [2], error_message [3]: the answer to any operation with an op_id
C2gOpenFloodgates 1306 state [1]: (queue [1], got_sequence [2], op_id [3]) per queue, to resume after reconnecting
C2gPing 1301 / G2cPong 3101 empty; the client pings every 20 s
C2gSubscribe 1309 / C2gUnsubscribe 1308 queues [1], queues_with_results [2]: (name [1], op_id [2])
G2cJoinedQueue 3106 / G2cLeftQueue 3107 user_id [1], queue [2], sequence [3]: someone joined or left a queue
G2cCreateMount 3108 queue [1], mount [2], type [3] (1 message, 2 state), properties [4], sequence [5]
C2gSendMessage 1304 op_id [1], queue [2], mount [3], message [4] (bytes)
G2cSendMessage 3104 user_id [1] (bytes), queue [2], mount [3], message [4], sequence [5]
C2gStateChange 1305 op_id [1], queue [2], mount [3], properties [4]: (key [1], value [2])
G2cStateChange 3105 op_id [1], user_id [2], queue [3], mount [4], properties [5], sequence [6]
C2gSuicide 1307 / G2cConnectionClosed 3109 closing

Each queue's messages are numbered (sequence), which the client sends back in OpenFloodgates to catch up after a reconnect.

On /chat/<chatId>:

  • The gateway sends a sender's own messages back to them too (with the queue's next sequence number); the client ignores its own because it subscribed with listen_to_self: false.
  • Mount messages: JSON {"userId", "chatId", "message", "to"} (to 0 for everyone). The client takes the sender from the gateway's user_id (the last path segment) and flags a mismatch with userId as forged.
  • Mount control: JSON with a command, such as PlayAction and PlayCoopAction (actions on avatars and furniture).

Chat commands

Chat text starting with * is a command (handled in the client; others see the effect, not the text). Seen or handled:

  • Presence and seats: *seat <n>, *resume <id>, *accept <id>, *uid, *boot, *msg SeatAssignment <...>.
  • Outfit: *use <ids...> (the whole outfit, also re-sent about every 30 s), *remove, *putOn, *putOnOutfit, *takeOff, *removeMood, *saveOutfit, *undo, *redo.
  • Typing: *msg BeginText <...> before a line of chat.
  • Scene: *imvu:setRoomState <encoded scene> (from the room's owner; others ignore it from anyone else), *imvu:changeRoom, *imvu:trigger / *imvu:untrigger, *imvu:activateMusic / *imvu:deactivateMusic.
  • Other: *imvu:gift, *imvu:showGift, *imvu:purchase, *imvu:try, *imvu:goto, *imvu:recommend, *imvu:isPureUser, *imvu:txnBoot, *snap, *hiResSnap, *uploadSnap.

The scene

  • Encoding (*imvu:setRoomState): the letter c, then base64 (with line breaks) of zlib-compressed JSON: {"room_state": {"room_info": {"customers_id", "room_instance_id", "room_pid", "revision_id"}, "room_contents": [[furniture instance ID, product ID, placement], ...]}}. room_pid is the room shell's product. Placement is a string of numbers: a format number, then position, a parent reference (which slot of which furniture item it sits on), rotation and scale (the exact layout still to be worked out).
  • The owner's client broadcasts the whole scene with *imvu:setRoomState when entering the room and after a change, and saves it with test.setRoomState({room_instance_id, room_contents, ...}); others only accept it from the owner. getRoomState returns the same contents with a room_hash.
  • A hosted room's scene can carry community products the same way: they're product IDs like any other (the proxy's local IDs on each machine, global IDs on the wire).

What the hub must do for a hosted room

Through the proxy, which sees both channels:

  • XML-RPC: answer getRoomCardInfo, getOrMakeChat, getRoomState, getParticipants, terminateChat, bootOutOfChat, setRoomState (owners), and the invitation calls, for hosted rooms' IDs. These go to the hub, never to IMVU.
  • IMQ: be the gateway for hosted rooms' queues (/chat/<chatId>, and room_occupancy/ for counts): results for subscribe operations, joined and left notices, relaying SendMessage to the queue's other members with sequence numbers, catching up on OpenFloodgates, and control messages. The proxy splits the one IMQ connection by queue: hosted rooms' queues to the hub, the rest to IMVU. It answers operations on hub queues itself (keeping the client's op IDs) and never forwards them.
  • IDs: hosted rooms need room instance IDs and chat IDs IMVU will never use, so the proxy can tell them apart (to be chosen: a reserved range, as for products).
  • Room list: add hosted rooms to rooms_list_paginated.php responses, answer room_occupancy/ queues for them, and answer getJoinPublicRoomInfo for their links.
  • Scenes: store the scene from the owner's setRoomState, serve it from getRoomState, and relay the owner's *imvu:setRoomState like any message.

The hub keeps the authoritative participant list, seats, scene and the room's moderation (owner, moderators, bans), and checks every message's sender itself.

Other sessions (from the code, not driven)

  • Private rooms: the same getOrMakeChat with private: true.
  • Chat Now (random one-to-one) uses activity Chat now-<room instance id>. It pairs strangers, so it wasn't driven with the test account.
  • Invitations: chat.attemptInvite, acceptInvite, declineInvite, leaveInviteAwayNote, with getOrMakeChat returning waiting until the other person answers. Needs a second account to capture.

Not yet looked at

  • Messages on inv:/room/room-<id> (likely room edits made outside the client).
  • The placement string's exact layout.
  • The furniture panel: dragging furniture in (a browser page; synthesized mouse drags don't trigger it).