Hosted rooms: seats, owner-first room IDs, T5 items in their scenes
CI / build (push) Successful in 31s

- The seat chosen on joining, and "*msg SeatAssignment" room seats, go to the hub, so people joining get
  free seats (both stood on seat 1).
- Instance IDs are "<owner>-<2100000000+n>" (the hub's change): the client takes the owner from them, so
  owners can decorate their rooms.
- A hosted room's scene carries community products as the hub's IDs (placements too), turned into local
  IDs by each proxy, instead of being stripped; saved and loaded scenes translate placements too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-30 05:26:02 -07:00
1 parent 13ee023836
commit c483099558
6 files changed
+192 -31

No files matched your search

+79 -2
View File
@@ -17,6 +17,7 @@ import (
"encoding/json"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
@@ -135,7 +136,9 @@ func roomOwner(instance string) int64 {
// isHostedInstance reports whether a room is hosted by the hub (its scene is the hosted package's).
func isHostedInstance(instance string) bool {
return strings.HasPrefix(instance, strconv.Itoa(HostedOwnerID)+"-")
_, number, ok := strings.Cut(instance, "-")
n, err := strconv.ParseInt(number, 10, 64)
return ok && err == nil && n > HostedOwnerID // hosted rooms' numbers start above 2100000000
}
// decorFetch gets a room's decorations, from memory or the hub.
@@ -452,6 +455,16 @@ func (p *Products) outgoingScene(m *chatMessage) ([]byte, bool) {
}
holder, rows := sceneContents(scene)
instance := sceneInstance(scene)
if isHostedInstance(instance) {
// A hosted room's scene goes only to T5DE users (through the hub): its community products travel as the
// hub's product IDs, which each proxy turns into its own local IDs.
out, changed := p.globalScene(rows)
if !changed {
return nil, false
}
holder["room_contents"] = out
return m.withText(setRoomStateCommand + encodeScene(scene)), true
}
rest, items, stripped := p.splitCommunity(rows)
_, hadDecor := p.decor.get(instance)
if instance != "" && !isHostedInstance(instance) && (stripped > 0 || hadDecor) {
@@ -544,7 +557,16 @@ func (p *Products) incomingScene(m *chatMessage) ([]byte, bool) {
// decorateScene adds the room's decorations to a scene IMVU sent: the scene encoded again, and whether it changed.
func (p *Products) decorateScene(scene map[string]any) (string, bool) {
instance := sceneInstance(scene)
if instance == "" || isHostedInstance(instance) {
if isHostedInstance(instance) {
holder, rows := sceneContents(scene)
out, changed := p.localScene(rows)
if !changed {
return "", false
}
holder["room_contents"] = out
return encodeScene(scene), true
}
if instance == "" {
return "", false
}
holder, rows := sceneContents(scene)
@@ -610,3 +632,58 @@ func (p *Products) DecorChanged(instance string) {
}
p.logf("decor: gave the client %s again with its decorations", instance)
}
// globalScene gives a hosted room's community products as the hub's product IDs (in the row and its placement); a
// local pack's can't be shared and is left out.
func (p *Products) globalScene(rows []any) ([]any, bool) {
out, changed := make([]any, 0, len(rows)), false
for _, row := range rows {
r, ok := row.([]any)
product, isNumber := int64(0), false
if ok && len(r) >= 3 {
product, isNumber = numberOf(r[1])
}
if !isNumber || !IsCommunity(product) {
out = append(out, row)
continue
}
changed = true
id, ok := p.state.productID(product)
if !ok || !p.inHubDir(id) {
continue
}
placement, _ := r[2].(string)
out = append(out, []any{r[0], id, swapToken(placement, strconv.FormatInt(product, 10), id)})
}
return out, changed
}
// localScene turns hub product IDs in a hosted room's scene into local IDs (asking for products that aren't here).
func (p *Products) localScene(rows []any) ([]any, bool) {
out, changed := make([]any, 0, len(rows)), false
for _, row := range rows {
r, ok := row.([]any)
if !ok || len(r) < 3 {
out = append(out, row)
continue
}
id, isString := r[1].(string)
if !isString || !hubIDPattern.MatchString(id) {
out = append(out, row)
continue
}
changed = true
local, ok := p.LocalID(id)
if !ok {
if p.Want != nil {
p.Want(id)
}
continue
}
placement, _ := r[2].(string)
out = append(out, []any{r[0], local, swapToken(placement, id, strconv.FormatInt(local, 10))})
}
return out, changed
}
var hubIDPattern = regexp.MustCompile(`^[a-z2-7]{10}$`)
+27
View File
@@ -144,3 +144,30 @@ func TestDecorationsInServerRoomState(t *testing.T) {
t.Fatalf("delivered %s", in)
}
}
// In a hosted room, community products travel as the hub's IDs and come back as local IDs.
func TestHostedRoomScene(t *testing.T) {
h, _ := newHarness(t)
hubDir := t.TempDir()
writePack(t, hubDir, "sofasofa22", 1)
h.p.dirs = append(h.p.dirs, hubDir)
h.p.HubDir = hubDir
h.p.refresh(true)
sofa := h.p.byProductID("sofasofa22").local
scene := map[string]any{"room_state": map[string]any{
"room_info": map[string]any{"room_instance_id": "7-2100000003"},
"room_contents": []any{[]any{10, 301, "2-1"}, []any{11, sofa, fmt.Sprintf("3 0 11 %d 6", sofa)}}}}
msg, _ := json.Marshal(map[string]any{"userId": 7, "message": setRoomStateCommand + encodeScene(scene)})
out, _ := h.p.outgoing("/chat/2100000003", "messages", msg)
m, _ := parseChatMessage(out)
sent, _ := decodeScene(strings.TrimPrefix(m.text, setRoomStateCommand))
if _, rows := sceneContents(sent); fmt.Sprint(rows) != "[[10 301 2-1] [11 sofasofa22 3 0 11 sofasofa22 6]]" {
t.Fatalf("sent %v", rows)
}
in := h.p.Incoming(nil, imq.Delivered{UserID: "7", Queue: "/chat/2100000003", Mount: "messages", Message: out})
m, _ = parseChatMessage(in)
got, _ := decodeScene(strings.TrimPrefix(m.text, setRoomStateCommand))
if _, rows := sceneContents(got); fmt.Sprint(rows) != fmt.Sprintf("[[10 301 2-1] [11 %d 3 0 11 %d 6]]", sofa, sofa) {
t.Fatalf("delivered %v", rows)
}
}
+1 -1
View File
@@ -571,7 +571,7 @@ func (p *Products) interceptBaseLook(x *proxy.Exchange) error {
func (p *Products) Outgoing(queue, mount string, message []byte) ([]byte, bool) {
out, drop := p.outgoing(queue, mount, message)
// A hosted room's message goes to the hub (without community products, as to IMVU), never to IMVU.
if p.Hosted != nil && !drop && p.Hosted.Send(queue, mount, out) {
if p.Hosted != nil && p.Hosted.Send(queue, mount, out, message, drop) {
return nil, true
}
return out, drop
+3 -2
View File
@@ -115,10 +115,11 @@ func (p *Products) Ready(id string) {
const HostedOwnerID = 2100000000
// HostedRooms serves T5DE-hosted rooms' IMQ queues: Route answers subscriptions to them, Send takes the client's
// messages to them (reporting whether the queue was one), Closed forgets a connection.
// messages to them (message as for others, original as the client sent it, for the echo; keep: nothing to send
// others), reporting whether the queue was one; Closed forgets a connection.
type HostedRooms interface {
Route(c *proxy.IMQConn, unsubscribe bool, s imq.Subscription) imq.Subscription
Send(queue, mount string, message []byte) bool
Send(queue, mount string, message, original []byte, keep bool) bool
Closed(c *proxy.IMQConn)
}