feat: update PrivateInstanceService to include baseName in instance updates

This commit is contained in:
Alexa
2026-06-06 22:15:37 +03:00
parent 2375db50ed
commit 92bd22d164
8 changed files with 2081 additions and 33 deletions
@@ -198,6 +198,32 @@ public class AvatarGiftsController(
}
}
if (!string.IsNullOrWhiteSpace(gift.EquipmentPrefabName) ||
!string.IsNullOrWhiteSpace(gift.EquipmentModificationGuid))
{
var itemSlug = !string.IsNullOrWhiteSpace(gift.EquipmentModificationGuid)
? StoreService.EquipmentSkinSlug(gift.EquipmentPrefabName, gift.EquipmentModificationGuid)
: gift.EquipmentPrefabName;
if (!string.IsNullOrWhiteSpace(itemSlug))
{
var row = await db.PlayerInventory
.FirstOrDefaultAsync(p => p.PlayerId == pid && p.ItemSlug == itemSlug);
if (row is null)
{
db.PlayerInventory.Add(new PlayerInventoryEntity
{
PlayerId = pid,
ItemSlug = itemSlug,
Quantity = 1,
});
}
else
{
row.Quantity = Math.Max(row.Quantity, 1);
}
}
}
// Apply currency / XP / level rewards bundled with the gift.
// The previous implementation only handled the avatar item path,
// so token gifts from the admin SPA marked themselves consumed
@@ -302,9 +302,10 @@ public class MessagesController(
// PRIVATE instance: the sender created an invite-only match
// (via /goto/room/X?CreatePrivateInstance=1). The
// roomInstanceId the watch sends maps to a registered
// PrivateInstance and only the owner is allowed to invite
// anyone in. Enforces our private-match access control —
// no random can DM their way past the invite list.
// PrivateInstance and the owner or someone already inside
// that exact live instance can invite. That keeps access
// control intact while still letting a guest in someone
// else's dorm invite a friend from the in-game watch.
//
// PUBLIC room: the sender is in a public room / a dorm /
// any non-private room. RoomInstanceId is just
@@ -319,12 +320,23 @@ public class MessagesController(
var inst = await privateInstances.GetAsync(roomInstanceId);
if (inst is not null)
{
if (inst.OwnerPlayerId != Me) return Forbid();
var senderPresence = presence.GetRoom(Me);
var senderIsInInstance = senderPresence is not null
&& senderPresence.RoomInstanceId == roomInstanceId;
if (inst.OwnerPlayerId != Me && !senderIsInInstance)
{
logger.LogWarning(
"[invite] from={From} tried to invite to private instance={Instance} ownedBy={Owner} while presenceInstance={PresenceInstance}; rejecting",
Me, roomInstanceId, inst.OwnerPlayerId, senderPresence?.RoomInstanceId);
return Forbid();
}
resolvedRoomId = inst.RoomId;
roomName = await db.Rooms
.Where(r => r.Id == inst.RoomId)
.Select(r => r.Name)
.FirstOrDefaultAsync() ?? "Private match";
roomName = !string.IsNullOrWhiteSpace(inst.Name)
? inst.Name
: await db.Rooms
.Where(r => r.Id == inst.RoomId)
.Select(r => r.Name)
.FirstOrDefaultAsync() ?? "Private match";
}
else
{
@@ -371,6 +371,9 @@ public class RoomsController(
[HttpPost("api/rooms/v4/saveData")]
[Authorize]
public async Task<IActionResult> SaveData([FromBody] SaveRoomSceneRequest body)
=> await SaveDataCore(body);
private async Task<IActionResult> SaveDataCore(SaveRoomSceneRequest body, long? routeRoomId = null)
{
var pid = this.RequireCurrentPlayerId();
@@ -381,7 +384,7 @@ public class RoomsController(
// on every /goto). This is the only signal we have without a
// RoomId on the request body.
var current = presence.GetRoom(pid);
if (current is null)
if (current is null && routeRoomId is null)
{
// No active room — without it we can't pick a target row.
// Return a degenerate RoomScene so the deserializer doesn't
@@ -401,11 +404,18 @@ public class RoomsController(
});
}
var roomId = current.RoomId;
var roomId = routeRoomId ?? current!.RoomId;
var room = await rooms.GetByIdAsync(roomId);
if (room is null) return NotFound(new { error = "room_not_found" });
var newBlob = body.RoomDataFilename?.Trim() ?? string.Empty;
var newBlob = body.EffectiveRoomDataFilename.Trim();
if (string.IsNullOrWhiteSpace(newBlob))
{
logger.LogWarning(
"[rooms-save] missing blob filename player={PlayerId} room={RoomId} requestedScene={RequestedSceneId} routeRoom={RouteRoomId}",
pid, roomId, body.RoomSceneId, routeRoomId);
return BadRequest(new { error = "missing_room_data_filename" });
}
var savedAt = DateTime.UtcNow;
// Multi-scene rooms have a RoomScenes row per scene. The 2020
@@ -494,30 +504,35 @@ public class RoomsController(
logger.LogInformation(
"[rooms-save] saveData player={PlayerId} room={RoomId} requestedScene={RequestedSceneId} savedScene={SavedSceneId} blob={BlobName} sceneRow={SceneRowFound} scenes={SceneCount}",
pid, roomId, body.RoomSceneId, savedSceneId, newBlob, sceneRow is not null, sceneRowsForRoom.Count);
var playersInInstance = onlinePresence.OnlinePlayerIds()
.Where(playerId =>
{
var playerRoom = presence.GetRoom(playerId);
return playerRoom is not null
&& playerRoom.RoomId == roomId
&& playerRoom.RoomInstanceId == current.RoomInstanceId;
})
.Append(pid)
.Distinct()
.ToArray();
var activeInstanceId = current is not null && current.RoomId == roomId
? current.RoomInstanceId
: 0L;
var playersInInstance = activeInstanceId == 0
? new[] { pid }
: onlinePresence.OnlinePlayerIds()
.Where(playerId =>
{
var playerRoom = presence.GetRoom(playerId);
return playerRoom is not null
&& playerRoom.RoomId == roomId
&& playerRoom.RoomInstanceId == activeInstanceId;
})
.Append(pid)
.Distinct()
.ToArray();
logger.LogInformation(
"[rooms-save] fanout room update player={PlayerId} room={RoomId} instance={InstanceId} blob={BlobName} recipients={RecipientCount}",
pid, roomId, current.RoomInstanceId, newBlob, playersInInstance.Length);
pid, roomId, activeInstanceId, newBlob, playersInInstance.Length);
var updatedRoomInstances = new Dictionary<long, RoomInstanceDto>();
foreach (var playerId in playersInInstance)
{
var currentPresence = presence.GetRoom(playerId);
if (currentPresence is null && playerId == pid)
if (currentPresence is null && playerId == pid && current is not null)
{
currentPresence = current;
}
if (currentPresence is null) continue;
if (currentPresence is null || currentPresence.RoomId != roomId) continue;
currentPresence.DataBlob = newBlob;
presence.SetRoom(playerId, currentPresence);
@@ -601,9 +616,16 @@ public class RoomsController(
{
public long RoomSceneId { get; set; }
public string? RoomDataFilename { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("filename")]
public string? Filename { get; set; }
public List<long>? InventionUsages { get; set; }
public string? InventionUsageBase64 { get; set; }
public CreatorActionContextDto? CreatorActionContext { get; set; }
public long RequestPlayerId { get; set; }
public int SaveRequestPlayerId { get; set; }
public string EffectiveRoomDataFilename =>
RoomDataFilename ?? Filename ?? string.Empty;
}
public class CreatorActionContextDto
@@ -2403,7 +2425,7 @@ public class RoomsController(
[FromBody] SaveRoomSceneRequest body)
{
body.RoomSceneId = subRoomId;
return SaveData(body);
return SaveDataCore(body, roomId);
}
private async Task<IActionResult> MutateScene(long roomId, long subRoomId, Action<RoomSceneEntity> mutator)
@@ -103,6 +103,7 @@ public class StorefrontsBuyController(
StoreService.TryGetAvatarItemPayload(item.Slug, out var avatarItemType, out avatarItemDesc);
var inventoryAvatarDesc = StoreService.InventoryAvatarItemDesc(avatarItemDesc);
StoreService.TryGetConsumableItemDesc(item.Slug, out var consumableItemDesc);
StoreService.TryGetEquipmentPayload(item.Slug, out var equipmentPrefabName, out var equipmentModificationGuid);
if (StoreService.TryGetHairDyePayload(item.Slug, out var hairDyeConsumableDesc, out var hairDyeColorGuid))
{
avatarItemType = 1;
@@ -116,6 +117,8 @@ public class StorefrontsBuyController(
AvatarItemType = string.IsNullOrEmpty(inventoryAvatarDesc) ? null : avatarItemType,
AvatarItemDescOrHairDyeDesc = inventoryAvatarDesc,
ConsumableItemDesc = consumableItemDesc,
EquipmentPrefabName = equipmentPrefabName,
EquipmentModificationGuid = equipmentModificationGuid,
CurrencyType = req.CurrencyType,
Currency = 0,
Xp = 0,
@@ -158,6 +161,8 @@ public class StorefrontsBuyController(
AvatarItemType = gift.AvatarItemType,
AvatarItemDesc = avatarItemDesc,
ConsumableItemDesc = gift.ConsumableItemDesc ?? string.Empty,
EquipmentPrefabName = gift.EquipmentPrefabName,
EquipmentModificationGuid = gift.EquipmentModificationGuid,
Message = gift.Message ?? string.Empty,
});
}
@@ -341,10 +346,13 @@ public class StorefrontsBuyController(
var avatarItemType = 0;
var avatarItemDesc = string.Empty;
var consumableItemDesc = string.Empty;
var equipmentPrefabName = string.Empty;
var equipmentModificationGuid = string.Empty;
if (grantedSlug is not null)
{
StoreService.TryGetAvatarItemPayload(grantedSlug, out avatarItemType, out avatarItemDesc);
StoreService.TryGetConsumableItemDesc(grantedSlug, out consumableItemDesc);
StoreService.TryGetEquipmentPayload(grantedSlug, out equipmentPrefabName, out equipmentModificationGuid);
if (StoreService.TryGetHairDyePayload(grantedSlug, out var hairDyeConsumableDesc, out var hairDyeColorGuid))
{
avatarItemType = 1;
@@ -378,8 +386,8 @@ public class StorefrontsBuyController(
AvatarItemDesc = avatarItemDesc,
AvatarItemDescOrHairDyeDesc = StoreService.InventoryAvatarItemDesc(avatarItemDesc),
ConsumableItemDesc = consumableItemDesc,
EquipmentPrefabName = string.Empty,
EquipmentModificationGuid = string.Empty,
EquipmentPrefabName = equipmentPrefabName,
EquipmentModificationGuid = equipmentModificationGuid,
CurrencyType = currencyType,
Currency = 0,
Message = string.Empty,