feat: Update appVersion to 2023.03.21 and enhance avatar-item linkage in store service

This commit is contained in:
Alexa
2026-06-28 01:31:21 +03:00
parent 50e03af296
commit 7da20f2047
7 changed files with 107 additions and 7 deletions
@@ -1051,7 +1051,11 @@ public class RoomsController(
EncryptVoiceChat = currentPresence.EncryptVoiceChat,
},
isOnline = true,
appVersion = 20201210,
// 2023.03.21 client build version (baked "20230317"). MUST match
// the joining client's own version or it reports a version
// mismatch and refuses to join. Was a stale December-2020 value
// (20201210) carried over when this branch forked.
appVersion = 20230317,
};
await notifications.NotifyAsync(playerId, PushNotificationId.SubscriptionUpdatePresence, presencePayload);
}
@@ -3196,8 +3200,17 @@ public class RoomsController(
public string? Filename { get; set; }
}
// The "Max Player Count in This Subroom" slider (SubroomModel.
// ChangeSubRoomMaxPlayers) POSTs here on the roomserver host. Like its
// siblings (accessibility/permissions/modify) the client prefixes the
// path with "roomserver/", so the bare-only routes 404'd and the slider's
// change silently never persisted — i.e. the limit couldn't be changed.
// The value flows back to the host via the subroom details MaxPlayers,
// which sets the Photon room cap.
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
[HttpPut("rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
[Authorize]
public Task<IActionResult> SubRoomMaxPlayers(long roomId, long subRoomId, [FromBody] SubRoomIntRequest req) =>
MutateScene(roomId, subRoomId, s => { if (req.Value is int v) s.MaxPlayers = Math.Max(1, v); });
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using DorkNet.Server.Services;
using DorkNet.Server.Controllers.API.Avatar.V4;
namespace DorkNet.Server.Controllers.Commerce;
@@ -85,6 +86,31 @@ public class CommerceCatalogController(StoreService store, DomainConfig domain)
{
var available = i.IsActive && (i.AvailableUntil == null || i.AvailableUntil > DateTime.UtcNow);
var imageUrl = string.IsNullOrEmpty(i.ImageName) ? "" : $"https://cdn.{apex}/{i.ImageName}";
// Avatar-item linkage — THE thing that makes wardrobe items appear in
// the Shop's Clothing tab. The 2023 client does NOT categorize a SKU by
// any field on the SKU itself; instead StoreItemListModel resolves the
// SKU to the client's baked AvatarItem (by GUID) and reads that item's
// OutfitType to place it in a tab. With no AvatarItemId the resolve
// returns OutfitType = -1, the item matches no Clothing/Consumables
// filter, and the whole Shop renders empty even though all 1531 SKUs
// parsed fine. The GUID is the tail of the "wardrobe-{guid}" slug; ship
// it (plus the OutfitType) under the keys the client reads
// (AvatarItemId / AvatarItemType — verified vs 2023 ISIL store-item
// context types EMBCEDNHFLB / CLIANCNHNIE). Coloured variants
// (wardrobe-colored-*) resolve through a different descriptor path and
// are left alone here.
string? avatarItemId = null;
int? avatarItemType = null;
if (i.Slug.StartsWith("wardrobe-", StringComparison.Ordinal)
&& !i.Slug.StartsWith("wardrobe-colored-", StringComparison.Ordinal))
{
var guid = i.Slug["wardrobe-".Length..];
avatarItemId = guid;
if (AvatarItemsController.TryGetOutfitType(guid, out var ot))
avatarItemType = ot;
}
return new Dictionary<string, object?>
{
// Identity / naming — every alias the watch's strict
@@ -116,6 +142,14 @@ public class CommerceCatalogController(StoreService store, DomainConfig domain)
["Slug"] = i.Slug,
["slug"] = i.Slug,
// Avatar-item join keys (see resolve block above). Emitted both at
// top level and inside Data so whichever the client reads, the
// SKU→AvatarItem resolve succeeds and the item lands in a tab.
["AvatarItemId"] = avatarItemId,
["avatarItemId"] = avatarItemId,
["AvatarItemType"] = avatarItemType,
["avatarItemType"] = avatarItemType,
["Name"] = i.DisplayName,
["name"] = i.DisplayName,
["DisplayName"] = i.DisplayName,
@@ -205,13 +239,21 @@ public class CommerceCatalogController(StoreService store, DomainConfig domain)
// string / null sub-purchase are valid.
["Data"] = new Dictionary<string, object?>
{
["GiftDropIds"] = Array.Empty<int>(),
["AvatarItemId"] = avatarItemId,
["AvatarItemType"] = avatarItemType,
// Link this SKU to its gift-drop store card (the EFFIEFEFHHB
// record `storefronts/v3/giftdropstore` builds, GiftDropId =
// this same id) so the client can resolve the card that carries
// the AvatarItemId join key. Empty here = SKU points at no card.
["GiftDropIds"] = new[] { (int)(i.Id & 0x7fffffff) },
["Message"] = string.Empty,
["SubscriptionPurchase"] = (object?)null,
},
["data"] = new Dictionary<string, object?>
{
["giftDropIds"] = Array.Empty<int>(),
["avatarItemId"] = avatarItemId,
["avatarItemType"] = avatarItemType,
["giftDropIds"] = new[] { (int)(i.Id & 0x7fffffff) },
["message"] = string.Empty,
["subscriptionPurchase"] = (object?)null,
},
@@ -504,5 +504,8 @@ public class PresenceDto
public bool IsOnline { get; set; } = true;
[JsonPropertyName("appVersion")]
public string AppVersion { get; set; } = "20201210";
// 2023.03.21 client build version ("20230317"); must match the joining
// client's own version or it reports a version mismatch. Was a stale 2020
// value (20201210) left over from the December branch this forked from.
public string AppVersion { get; set; } = "20230317";
}
@@ -179,5 +179,8 @@ public class PlayerPresenceDto
public bool IsOnline { get; set; } = true;
[JsonPropertyName("appVersion")]
public string AppVersion { get; set; } = "20201210";
// 2023.03.21 client build version ("20230317"); must match the joining
// client's own version or it reports a version mismatch. Stale 2020 value
// (20201210) carried over from the December branch.
public string AppVersion { get; set; } = "20230317";
}
+28 -1
View File
@@ -64,7 +64,12 @@ public class GameSessionService(DorkNetDbContext db)
// in, so we save once with a placeholder, then patch and save
// again. Two SaveChangesAsync calls is cheap at our QPS.
PhotonRoomName = "pending",
MaxCapacity = 8,
// Track the room's "Max Player Count in This Subroom" slider
// instead of a hardcoded 8, so matchmade joiners fill the room to
// the host's chosen limit rather than fragmenting into a new
// instance every 8 players. (Existing sessions keep whatever cap
// they were created with.)
MaxCapacity = await ResolveRoomMaxCapacityAsync(effectiveRoom),
PlayerCount = 1,
CreatedAt = DateTime.UtcNow,
};
@@ -75,6 +80,28 @@ public class GameSessionService(DorkNetDbContext db)
return ToDto(session);
}
// Default session cap when we can't resolve a room-specific value (system
// rooms like the dorm use a GUID roomId that doesn't map to a RoomEntity).
private const int DefaultMaxCapacity = 8;
/// <summary>Resolve a new session's capacity from the room's entry
/// subroom (lowest <c>OrderIndex</c>) <see cref="RoomSceneEntity.MaxPlayers"/>
/// — the value the in-game "Max Player Count in This Subroom" slider
/// writes. Matchmade sessions land in the entry subroom, so its cap is the
/// right ceiling. GameSession roomIds are the room's numeric Id as a
/// string for user rooms; non-numeric (dorm GUID, etc.) → default. Clamped
/// to a sane Photon ceiling.</summary>
private async Task<int> ResolveRoomMaxCapacityAsync(string roomId)
{
if (!long.TryParse(roomId, out var roomLongId)) return DefaultMaxCapacity;
var max = await db.RoomScenes
.Where(s => s.RoomId == roomLongId)
.OrderBy(s => s.OrderIndex)
.Select(s => (int?)s.MaxPlayers)
.FirstOrDefaultAsync();
return max is int v && v > 0 ? Math.Clamp(v, 1, 80) : DefaultMaxCapacity;
}
public async Task<GameSession?> GetByIdAsync(long id)
{
var s = await db.GameSessions.AsNoTracking()
@@ -297,7 +297,10 @@ public class NotificationService(
},
// 2020.12 PlayerPresence.Deserialize requires these.
["isOnline"] = redacted is not null,
["appVersion"] = 20201210,
// 2023.03.21 client build version ("20230317"); must match the
// joining client's own version or it reports a version mismatch.
// Stale 2020 value (20201210) carried over from the December branch.
["appVersion"] = 20230317,
};
var envelope = new System.Text.Json.Nodes.JsonObject
+9
View File
@@ -968,6 +968,15 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
Tooltip = i.Description,
AvatarItemDesc = avatarItemDesc,
AvatarItemDescOrHairDyeDesc = avatarItemDesc,
// AvatarItemId is the field the 2023 client's StoreItemListModel
// actually reads (store-item +0x38) to resolve the card to a baked
// AvatarItem and read its OutfitType for tab placement — verified
// vs 2023 ISIL (EMBCEDNHFLB/CLIANCNHNIE, AvatarItem.GIJPCPDKHPO).
// AvatarItemDesc alone is NOT the join key, so clothing cards
// resolved to OutfitType=-1 and the Shop rendered empty. Ship the
// same {guid},,, descriptor (GIJPCPDKHPO accepts bare {guid} or the
// composite and falls back to parsing it to the immutable Guid).
AvatarItemId = avatarItemDesc,
AvatarItemType = string.IsNullOrEmpty(avatarItemDesc) ? (int?)null : avatarItemType,
ConsumableItemDesc = consumableItemDesc,
EquipmentPrefabName = equipmentPrefabName,