From 8d8b9a4566a753001b937aed0e24289ca7ae2334 Mon Sep 17 00:00:00 2001 From: Robert Paciorek Date: Sun, 17 Aug 2025 11:59:55 +0000 Subject: [PATCH 1/2] SoD bug fixes - fish trap on the farm now gives a reward - added support for revard via achievements in RoomService - added achievements for fish trap rewards - fixed DuplicateUserName message on SoD 1.13 - not encrypted responses with correct schema in V3/RegistrationWebService.asmx/RegisterChild - fixed putting racing sheep into farm - implemented /ContentWebService.asmx/RedeemItems thanks to @ABrokenTV for debug these issues --- src/Controllers/Common/ContentController.cs | 36 ++++ .../Common/RegistrationController.cs | 29 +++- .../achievements/achievementid_sod.xml | 158 ++++++++++++++++++ src/Schema/AchievementCompletion.cs | 11 ++ src/Schema/CompletionAction.cs | 3 + src/Services/RoomService.cs | 21 ++- 6 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 src/Schema/AchievementCompletion.cs diff --git a/src/Controllers/Common/ContentController.cs b/src/Controllers/Common/ContentController.cs index 1c2e6eb..8935881 100644 --- a/src/Controllers/Common/ContentController.cs +++ b/src/Controllers/Common/ContentController.cs @@ -1185,6 +1185,42 @@ public class ContentController : Controller { return Ok(new BuddyList { Buddy = new Buddy[0] }); } + [HttpPost] + [Produces("application/xml")] + [Route("/ContentWebService.asmx/RedeemItems")] + [VikingSession] + public IActionResult RedeemItems(Viking viking, [FromForm] string request) { + var req = XmlUtil.DeserializeXml(request); + Dictionary inventoryItemsToAdd = new(); + + // resolve items in the bundle + ItemData bundleItem = itemService.GetItem(req.ItemID); + foreach (var reward in bundleItem.Relationship.Where(e => e.Type == "Bundle")) { + int quantity = itemService.GetItemQuantity(reward, 1); + inventoryItemsToAdd.TryAdd(reward.ItemId, 0); + inventoryItemsToAdd[reward.ItemId] += quantity; + } + + var addedItems = inventoryService.AddItemsToInventoryBulk(viking, inventoryItemsToAdd); + + // build response + List items = new List(); + foreach (var i in inventoryItemsToAdd) { + items.AddRange(Enumerable.Repeat( + new CommonInventoryResponseItem { + CommonInventoryID = addedItems.ContainsKey(i.Key) ? addedItems[i.Key] : 0, // return inventory id if this item was added to the DB + ItemID = i.Key, + Quantity = 0 + }, i.Value)); + } + + return Ok(new CommonInventoryResponse{ + Success = true, + CommonInventoryIDs = items.ToArray(), + UserGameCurrency = achievementService.GetUserCurrency(viking) + }); + } + [HttpPost] [Produces("application/xml")] [Route("/ContentWebService.asmx/RedeemMysteryBoxItems")] diff --git a/src/Controllers/Common/RegistrationController.cs b/src/Controllers/Common/RegistrationController.cs index 977da30..708dad1 100644 --- a/src/Controllers/Common/RegistrationController.cs +++ b/src/Controllers/Common/RegistrationController.cs @@ -100,7 +100,6 @@ public class RegistrationController : Controller { [HttpPost] [Produces("application/xml")] - [Route("V3/RegistrationWebService.asmx/RegisterChild")] // used by Magic & Mythies [Route("V4/RegistrationWebService.asmx/RegisterChild")] [DecryptRequest("childRegistrationData")] [EncryptResponse] @@ -131,6 +130,34 @@ public class RegistrationController : Controller { }); } + [HttpPost] + [Produces("application/xml")] + [Route("V3/RegistrationWebService.asmx/RegisterChild")] // used by SoD 1.13 and Magic & Mythies + [DecryptRequest("childRegistrationData")] + public IActionResult RegisterChildV3([FromForm] Guid parentApiToken, [FromForm] string apiKey) { + User? user = ctx.Sessions.FirstOrDefault(e => e.ApiToken == parentApiToken)?.User; + if (user is null) { + return Ok(new RegistrationResult{ + Status = MembershipUserStatus.InvalidApiToken + }); + } + + // Check if name populated + ChildRegistrationData data = XmlUtil.DeserializeXml(Request.Form["childRegistrationData"]); + if (String.IsNullOrWhiteSpace(data.ChildName)) { + return Ok(MembershipUserStatus.ValidationError); + } + + // Check if viking exists + if (ctx.Vikings.Count(e => e.Name == data.ChildName) > 0) { + return Ok(MembershipUserStatus.DuplicateUserName); + } + + Viking v = CreateViking(user, data, ClientVersion.GetVersion(apiKey)); + + return Ok(MembershipUserStatus.Success); + } + private Viking CreateViking(User user, ChildRegistrationData data, uint gameVersion) { List items = new(); if (gameVersion >= ClientVersion.Min_SoD) { diff --git a/src/Resources/achievements/achievementid_sod.xml b/src/Resources/achievements/achievementid_sod.xml index 6ca3194..d95b24f 100644 --- a/src/Resources/achievements/achievementid_sod.xml +++ b/src/Resources/achievements/achievementid_sod.xml @@ -3029,6 +3029,164 @@ + + + 201388 + +

6

+ 3 + 1 + 8 + 7143 +
+ +

6

+ 2 + 1 + 8 + 7139 +
+ +

6

+ 1 + 1 + 8 + 7144 +
+
+ + 201389 + +

6

+ 4 + 1 + 8 + 7143 +
+
+ + 201390 + +

6

+ 2 + 1 + 8 + 7143 +
+ +

6

+ 1 + 1 + 8 + 7139 +
+
+ + 201391 + +

6

+ 3 + 1 + 8 + 7140 +
+
+ + 201392 + +

6

+ 1 + 1 + 8 + 7140 +
+ +

6

+ 3 + 1 + 8 + 7144 +
+
+ + 201393 + +

6

+ 5 + 1 + 8 + 7143 +
+ +

6

+ 1 + 1 + 8 + 7140 +
+
+ + 201394 + +

6

+ 1 + 1 + 8 + 7143 +
+ +

6

+ 2 + 1 + 8 + 7139 +
+ +

6

+ 2 + 1 + 8 + 7144 +
+
+ + 201395 + +

6

+ 5 + 1 + 8 + 7139 +
+
+ + 201396 + +

6

+ 5 + 1 + 8 + 7144 +
+
+ + 201397 + +

6

+ 3 + 1 + 8 + 7139 +
+ +

6

+ 2 + 1 + 8 + 7144 +
+
+ 201323 diff --git a/src/Schema/AchievementCompletion.cs b/src/Schema/AchievementCompletion.cs new file mode 100644 index 0000000..8182bbd --- /dev/null +++ b/src/Schema/AchievementCompletion.cs @@ -0,0 +1,11 @@ +using System.Xml.Serialization; + +namespace sodoff.Schema; + +[XmlRoot(ElementName = "AchievementCompletion", Namespace = "")] +[Serializable] +public class AchievementCompletion +{ + [XmlElement(ElementName = "AchievementID")] + public int AchievementID; +} diff --git a/src/Schema/CompletionAction.cs b/src/Schema/CompletionAction.cs index 5d49da3..98cd35a 100644 --- a/src/Schema/CompletionAction.cs +++ b/src/Schema/CompletionAction.cs @@ -12,4 +12,7 @@ public class CompletionAction { [XmlElement(ElementName = "Transition")] public StateTransition Transition; + + [XmlElement(ElementName = "AchievementCompletion", IsNullable = true)] + public AchievementCompletion[] AchievementCompletion; } diff --git a/src/Services/RoomService.cs b/src/Services/RoomService.cs index c16c342..a5d30b8 100644 --- a/src/Services/RoomService.cs +++ b/src/Services/RoomService.cs @@ -12,6 +12,7 @@ public class RoomService { private ItemService itemService; private AchievementService achievementService; + private Random random = new Random(); public RoomService(DBContext ctx, ItemService itemService, AchievementService achievementService) { this.ctx = ctx; @@ -125,8 +126,9 @@ public class RoomService { UserItemPosition pos = XmlUtil.DeserializeXml(item.RoomItemData); AchievementReward[]? rewards; + int? achievementID; List consumables; - int nextStateID = GetNextStateID(pos, speedup, out rewards, out consumables); + int nextStateID = GetNextStateID(pos, speedup, out rewards, out achievementID, out consumables); foreach (var consumable in consumables) { ItemStateCriteriaConsumable c = (ItemStateCriteriaConsumable)consumable; @@ -138,6 +140,13 @@ public class RoomService { if (rewards != null) { response.Rewards = achievementService.ApplyAchievementRewards(item.Room.Viking, rewards); } + if (achievementID != null) { + var newrewards = achievementService.ApplyAchievementRewardsByID(item.Room.Viking, (int)achievementID); + if (response.Rewards is null) + response.Rewards = newrewards; + else + response.Rewards = response.Rewards.Concat(newrewards).ToArray(); + } DateTime stateChange = new DateTime(DateTime.Now.Ticks); if (nextStateID == -1) { @@ -164,8 +173,9 @@ public class RoomService { return response; } - private int GetNextStateID(UserItemPosition pos, bool speedup, out AchievementReward[]? rewards, out List consumables) { + private int GetNextStateID(UserItemPosition pos, bool speedup, out AchievementReward[]? rewards, out int? achievementID, out List consumables) { rewards = null; + achievementID = null; consumables = new List(); if (pos.UserItemState == null) @@ -175,6 +185,13 @@ public class RoomService { rewards = currState.Rewards; consumables = currState.Rule.Criterias.FindAll(x => x.Type == ItemStateCriteriaType.ConsumableItem); + // achievementID = currState.AchievementID; // TODO we should do this or not? some items use the same rewards in `currState.Rewards` and achievement definition, but some do not contain only achievementID (but then there is generally no definition for achievement) + if (currState.Rule.CompletionAction.AchievementCompletion != null) { + achievementID = currState.Rule.CompletionAction.AchievementCompletion[ + random.Next(0, currState.Rule.CompletionAction.AchievementCompletion.Length) + ].AchievementID; + } + if (speedup) return ((ItemStateCriteriaSpeedUpItem)currState.Rule.Criterias.Find(x => x.Type == ItemStateCriteriaType.SpeedUpItem)!).EndStateID; From 693a73a71e080520d6f1781eec2e5bbaeaa4423b Mon Sep 17 00:00:00 2001 From: Robert Paciorek Date: Mon, 18 Aug 2025 19:53:12 +0000 Subject: [PATCH 2/2] do not store item defintion in database for rooms --- src/Schema/UserItemPosition.cs | 4 ++-- src/Services/RoomService.cs | 38 ++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/Schema/UserItemPosition.cs b/src/Schema/UserItemPosition.cs index ce75e00..2dfd353 100644 --- a/src/Schema/UserItemPosition.cs +++ b/src/Schema/UserItemPosition.cs @@ -31,8 +31,8 @@ public class UserItemPosition { [XmlElement(ElementName = "uicid")] public int? UserInventoryCommonID; - [XmlElement(ElementName = "i")] - public ItemData Item; + [XmlElement(ElementName = "i", IsNullable = true)] + public ItemData? Item; [XmlElement(ElementName = "px")] public double? PositionX; diff --git a/src/Services/RoomService.cs b/src/Services/RoomService.cs index a5d30b8..ab6d505 100644 --- a/src/Services/RoomService.cs +++ b/src/Services/RoomService.cs @@ -29,15 +29,21 @@ public class RoomService { List ids = new(); List states = new(); foreach (var itemRequest in roomItemRequest) { + ItemData itemData = itemRequest.Item; + // TODO: Remove item from inventory (using CommonInventoryID) InventoryItem? i = room.Viking?.InventoryItems.FirstOrDefault(x => x.Id == itemRequest.UserInventoryCommonID); if (i != null) { i.Quantity--; - if (itemRequest.Item is null) { - itemRequest.Item = itemService.GetItem(i.ItemId); + if (itemData is null) { + itemData = itemService.GetItem(i.ItemId); } } + // do not store item definition in serialised xml in database (store item id instead) + itemRequest.Item = null; + itemRequest.ItemID = itemData.ItemID; + RoomItem roomItem = new RoomItem { RoomItemData = XmlUtil.SerializeXml(itemRequest).Replace(" xsi:type=\"UserItemPositionSetRequest\"", "") // NOTE: No way to avoid this hack when we're serializing a child class into a base class }; @@ -45,12 +51,12 @@ public class RoomService { room.Items.Add(roomItem); ctx.SaveChanges(); ids.Add(roomItem.Id); - if (itemRequest.Item.ItemStates.Count > 0) { - ItemState defaultState = itemRequest.Item.ItemStates.Find(x => x.Order == 1)!; + if (itemData.ItemStates.Count > 0) { + ItemState defaultState = itemData.ItemStates.Find(x => x.Order == 1)!; UserItemState userDefaultState = new UserItemState { CommonInventoryID = (int)itemRequest.UserInventoryCommonID!, UserItemPositionID = roomItem.Id, - ItemID = (int)itemRequest.Item.ItemID, + ItemID = (int)itemData.ItemID, ItemStateID = defaultState.ItemStateID, StateChangeDate = new DateTime(DateTime.Now.Ticks) }; @@ -76,7 +82,7 @@ public class RoomService { if (itemRequest.UserItemState != null) itemPosition.UserItemState = itemRequest.UserItemState; if (itemRequest.UserItemAttributes != null) itemPosition.UserItemAttributes = itemRequest.UserItemAttributes; if (itemRequest.UserItemStat != null) itemPosition.UserItemStat = itemRequest.UserItemStat; - if (itemRequest.Item != null) itemPosition.Item = itemRequest.Item; + if (itemRequest.Item != null) itemPosition.ItemID = itemRequest.Item.ItemID; if (itemRequest.PositionX != null) itemPosition.PositionX = itemRequest.PositionX; if (itemRequest.PositionY != null) itemPosition.PositionY = itemRequest.PositionY; if (itemRequest.PositionZ != null) itemPosition.PositionZ = itemRequest.PositionZ; @@ -110,7 +116,10 @@ public class RoomService { foreach (var item in room.Items) { UserItemPosition data = XmlUtil.DeserializeXml(item.RoomItemData); data.UserItemPositionID = item.Id; - data.ItemID = data.Item?.ItemID; + if (data.ItemID is null) + data.ItemID = data.Item?.ItemID; // for backward compatibility with database entries without set `data.ItemID` + else + data.Item = itemService.GetItem((int)data.ItemID); if (gameVersion < 0xa3a00a0a && data.Uses is null) data.Uses = -1; itemPosition.Add(data); @@ -123,7 +132,10 @@ public class RoomService { Success = true, ErrorCode = ItemStateChangeError.Success }; + UserItemPosition pos = XmlUtil.DeserializeXml(item.RoomItemData); + if (pos.ItemID is null) + pos.ItemID = pos.Item?.ItemID; // for backward compatibility with database entries without set `data.ItemID` AchievementReward[]? rewards; int? achievementID; @@ -159,7 +171,7 @@ public class RoomService { response.UserItemState = new UserItemState { CommonInventoryID = (int)pos.UserInventoryCommonID!, UserItemPositionID = item.Id, - ItemID = pos.Item.ItemID, + ItemID = (int)pos.ItemID, ItemStateID = nextStateID, StateChangeDate = stateChange }; @@ -177,11 +189,12 @@ public class RoomService { rewards = null; achievementID = null; consumables = new List(); + var itemStates = itemService.GetItem((int)pos.ItemID).ItemStates; if (pos.UserItemState == null) - return pos.Item.ItemStates.Find(x => x.Order == 1)!.ItemStateID; + return itemStates.Find(x => x.Order == 1)!.ItemStateID; - ItemState currState = pos.Item.ItemStates.Find(x => x.ItemStateID == pos.UserItemState.ItemStateID)!; + ItemState currState = itemStates.Find(x => x.ItemStateID == pos.UserItemState.ItemStateID)!; rewards = currState.Rewards; consumables = currState.Rule.Criterias.FindAll(x => x.Type == ItemStateCriteriaType.ConsumableItem); @@ -204,12 +217,11 @@ public class RoomService { switch (currState.Rule.CompletionAction.Transition) { default: - return pos.Item.ItemStates.Find(x => x.Order == currState.Order + 1)!.ItemStateID; + return itemStates.Find(x => x.Order == currState.Order + 1)!.ItemStateID; case StateTransition.InitialState: - return pos.Item.ItemStates.Find(x => x.Order == 1)!.ItemStateID; + return itemStates.Find(x => x.Order == 1)!.ItemStateID; case StateTransition.Deletion: return -1; - } } }