mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: Update appVersion to 2023.03.21 and enhance avatar-item linkage in store service
This commit is contained in:
@@ -1051,7 +1051,11 @@ public class RoomsController(
|
|||||||
EncryptVoiceChat = currentPresence.EncryptVoiceChat,
|
EncryptVoiceChat = currentPresence.EncryptVoiceChat,
|
||||||
},
|
},
|
||||||
isOnline = true,
|
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);
|
await notifications.NotifyAsync(playerId, PushNotificationId.SubscriptionUpdatePresence, presencePayload);
|
||||||
}
|
}
|
||||||
@@ -3196,8 +3200,17 @@ public class RoomsController(
|
|||||||
public string? Filename { get; set; }
|
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")]
|
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
|
||||||
[HttpPut("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]
|
[Authorize]
|
||||||
public Task<IActionResult> SubRoomMaxPlayers(long roomId, long subRoomId, [FromBody] SubRoomIntRequest req) =>
|
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); });
|
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 Microsoft.AspNetCore.Mvc;
|
||||||
using DorkNet.Server.Services;
|
using DorkNet.Server.Services;
|
||||||
|
using DorkNet.Server.Controllers.API.Avatar.V4;
|
||||||
|
|
||||||
namespace DorkNet.Server.Controllers.Commerce;
|
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 available = i.IsActive && (i.AvailableUntil == null || i.AvailableUntil > DateTime.UtcNow);
|
||||||
var imageUrl = string.IsNullOrEmpty(i.ImageName) ? "" : $"https://cdn.{apex}/{i.ImageName}";
|
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?>
|
return new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
// Identity / naming — every alias the watch's strict
|
// 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,
|
||||||
["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,
|
||||||
["name"] = i.DisplayName,
|
["name"] = i.DisplayName,
|
||||||
["DisplayName"] = i.DisplayName,
|
["DisplayName"] = i.DisplayName,
|
||||||
@@ -205,13 +239,21 @@ public class CommerceCatalogController(StoreService store, DomainConfig domain)
|
|||||||
// string / null sub-purchase are valid.
|
// string / null sub-purchase are valid.
|
||||||
["Data"] = new Dictionary<string, object?>
|
["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,
|
["Message"] = string.Empty,
|
||||||
["SubscriptionPurchase"] = (object?)null,
|
["SubscriptionPurchase"] = (object?)null,
|
||||||
},
|
},
|
||||||
["data"] = new Dictionary<string, object?>
|
["data"] = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["giftDropIds"] = Array.Empty<int>(),
|
["avatarItemId"] = avatarItemId,
|
||||||
|
["avatarItemType"] = avatarItemType,
|
||||||
|
["giftDropIds"] = new[] { (int)(i.Id & 0x7fffffff) },
|
||||||
["message"] = string.Empty,
|
["message"] = string.Empty,
|
||||||
["subscriptionPurchase"] = (object?)null,
|
["subscriptionPurchase"] = (object?)null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -504,5 +504,8 @@ public class PresenceDto
|
|||||||
public bool IsOnline { get; set; } = true;
|
public bool IsOnline { get; set; } = true;
|
||||||
|
|
||||||
[JsonPropertyName("appVersion")]
|
[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;
|
public bool IsOnline { get; set; } = true;
|
||||||
|
|
||||||
[JsonPropertyName("appVersion")]
|
[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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,12 @@ public class GameSessionService(DorkNetDbContext db)
|
|||||||
// in, so we save once with a placeholder, then patch and save
|
// in, so we save once with a placeholder, then patch and save
|
||||||
// again. Two SaveChangesAsync calls is cheap at our QPS.
|
// again. Two SaveChangesAsync calls is cheap at our QPS.
|
||||||
PhotonRoomName = "pending",
|
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,
|
PlayerCount = 1,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
};
|
};
|
||||||
@@ -75,6 +80,28 @@ public class GameSessionService(DorkNetDbContext db)
|
|||||||
return ToDto(session);
|
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)
|
public async Task<GameSession?> GetByIdAsync(long id)
|
||||||
{
|
{
|
||||||
var s = await db.GameSessions.AsNoTracking()
|
var s = await db.GameSessions.AsNoTracking()
|
||||||
|
|||||||
@@ -297,7 +297,10 @@ public class NotificationService(
|
|||||||
},
|
},
|
||||||
// 2020.12 PlayerPresence.Deserialize requires these.
|
// 2020.12 PlayerPresence.Deserialize requires these.
|
||||||
["isOnline"] = redacted is not null,
|
["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
|
var envelope = new System.Text.Json.Nodes.JsonObject
|
||||||
|
|||||||
@@ -968,6 +968,15 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
|
|||||||
Tooltip = i.Description,
|
Tooltip = i.Description,
|
||||||
AvatarItemDesc = avatarItemDesc,
|
AvatarItemDesc = avatarItemDesc,
|
||||||
AvatarItemDescOrHairDyeDesc = 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,
|
AvatarItemType = string.IsNullOrEmpty(avatarItemDesc) ? (int?)null : avatarItemType,
|
||||||
ConsumableItemDesc = consumableItemDesc,
|
ConsumableItemDesc = consumableItemDesc,
|
||||||
EquipmentPrefabName = equipmentPrefabName,
|
EquipmentPrefabName = equipmentPrefabName,
|
||||||
|
|||||||
Reference in New Issue
Block a user