diff --git a/README.md b/README.md index ede22b79..36aa9bf1 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ Then run School of Dragons. - SetAchievementByEntityIDs - SetAvatar - SetCommonInventory +- SetDisplayName (V2) - SetDragonXP (used by account import tools) - SetImage - SetKeyValuePair diff --git a/mitm-redirect.py b/mitm-redirect.py index 86661cf6..360a2132 100644 --- a/mitm-redirect.py +++ b/mitm-redirect.py @@ -86,6 +86,7 @@ 'GetGameDataByGame', 'GetGameDataByGameForDateRange', 'GetTopAchievementPointUsers', + 'SetDisplayName', ] def routable(path): diff --git a/src/Controllers/Common/AuthenticationController.cs b/src/Controllers/Common/AuthenticationController.cs index cd144678..eff74bb1 100644 --- a/src/Controllers/Common/AuthenticationController.cs +++ b/src/Controllers/Common/AuthenticationController.cs @@ -54,7 +54,7 @@ public IActionResult LoginParent() { var response = new ParentLoginInfo { UserName = user.Username, - Email = user.Email, + //Email = user.Email, /* disabled to avoid put email in client debug logs */ ApiToken = session.ApiToken.ToString(), UserID = user.Id.ToString(), Status = MembershipUserStatus.Success, @@ -95,7 +95,7 @@ public IActionResult GetUserInfoByApiToken([FromForm] Guid apiToken, [FromForm] Username = user.Username, MembershipID = "ef84db9-59c6-4950-b8ea-bbc1521f899b", // placeholder FacebookUserID = 0, - MultiplayerEnabled = (apiKey != "a1a13a0a-7c6e-4e9b-b0f7-22034d799013" && apiKey != "a2a09a0a-7c6e-4e9b-b0f7-22034d799013" && apiKey != "a3a12a0a-7c6e-4e9b-b0f7-22034d799013"), + MultiplayerEnabled = !ClientVersion.IsOldSoD(apiKey), IsApproved = true, Age = 24, OpenChatEnabled = true @@ -110,7 +110,7 @@ public IActionResult GetUserInfoByApiToken([FromForm] Guid apiToken, [FromForm] UserID = viking.Uid.ToString(), Username = viking.Name, FacebookUserID = 0, - MultiplayerEnabled = (apiKey != "a1a13a0a-7c6e-4e9b-b0f7-22034d799013" && apiKey != "a2a09a0a-7c6e-4e9b-b0f7-22034d799013" && apiKey != "a3a12a0a-7c6e-4e9b-b0f7-22034d799013"), + MultiplayerEnabled = !ClientVersion.IsOldSoD(apiKey), IsApproved = true, Age = 24, OpenChatEnabled = true diff --git a/src/Controllers/Common/ContentController.cs b/src/Controllers/Common/ContentController.cs index eb66216f..95610352 100644 --- a/src/Controllers/Common/ContentController.cs +++ b/src/Controllers/Common/ContentController.cs @@ -102,6 +102,33 @@ public IActionResult ValidateName([FromForm] string nameValidationRequest) { } } + [HttpPost] + [Produces("application/xml")] + [Route("/V2/ContentWebService.asmx/SetDisplayName")] + [VikingSession] + public IActionResult SetDisplayName(Viking viking, [FromForm] string request) { + string newName = XmlUtil.DeserializeXml(request).DisplayName; + + if (String.IsNullOrWhiteSpace(newName) || ctx.Vikings.Count(e => e.Name == newName) > 0) { + return Ok(new SetAvatarResult { + Success = false, + StatusCode = AvatarValidationResult.AvatarDisplayNameInvalid + }); + } + + viking.Name = newName; + AvatarData avatarData = XmlUtil.DeserializeXml(viking.AvatarSerialized); + avatarData.DisplayName = newName; + viking.AvatarSerialized = XmlUtil.SerializeXml(avatarData); + ctx.SaveChanges(); + + return Ok(new SetAvatarResult { + Success = true, + DisplayName = viking.Name, + StatusCode = AvatarValidationResult.Valid + }); + } + [HttpPost] [Produces("application/xml")] [Route("ContentWebService.asmx/GetKeyValuePair")] @@ -208,6 +235,42 @@ public IActionResult SetCommonInventory(Viking viking, [FromForm] string commonI return Ok(response); } + [HttpPost] + [Produces("application/xml")] + [Route("ContentWebService.asmx/SetCommonInventoryAttribute")] + [VikingSession] + public IActionResult SetCommonInventoryAttribute(Viking viking, [FromForm] int commonInventoryID, [FromForm] string pairxml) { + InventoryItem? item = viking.InventoryItems.FirstOrDefault(e => e.Id == commonInventoryID); + + List itemAttributes; + if (item.AttributesSerialized != null) { + itemAttributes = XmlUtil.DeserializeXml(item.AttributesSerialized).Pairs.ToList(); + } else { + itemAttributes = new List(); + } + + Schema.PairData newItemAttributes = XmlUtil.DeserializeXml(pairxml); + foreach (var p in newItemAttributes.Pairs) { + var pairItem = itemAttributes.FirstOrDefault(e => e.PairKey == p.PairKey); + if (pairItem != null){ + pairItem.PairValue = p.PairValue; + } else { + itemAttributes.Add(p); + } + } + + if (itemAttributes.Count > 0) { + item.AttributesSerialized = XmlUtil.SerializeXml( + new Schema.PairData{ + Pairs = itemAttributes.ToArray() + } + ); + } + + ctx.SaveChanges(); + return Ok(true); + } + [HttpPost] [Produces("application/xml")] [Route("ContentWebService.asmx/UseInventory")] @@ -271,6 +334,53 @@ public IActionResult SetAvatar(Viking viking, [FromForm] string contentXML) { }); } + [HttpPost] + [Produces("application/xml")] + [Route("ContentWebService.asmx/CreateRaisedPet")] // used by SoD 1.6 + [VikingSession] + public RaisedPetData? CreateRaisedPet(Viking viking, int petTypeID) { + // Update the RaisedPetData with the info + String dragonId = Guid.NewGuid().ToString(); + + var raisedPetData = new RaisedPetData(); + raisedPetData.IsPetCreated = true; + raisedPetData.PetTypeID = petTypeID; + raisedPetData.RaisedPetID = 0; // Initially make zero, so the db auto-fills + raisedPetData.EntityID = Guid.Parse(dragonId); + raisedPetData.Name = string.Concat("Dragon-", dragonId.AsSpan(0, 8)); // Start off with a random name + raisedPetData.IsSelected = false; // The api returns false, not sure why + raisedPetData.CreateDate = new DateTime(DateTime.Now.Ticks); + raisedPetData.UpdateDate = new DateTime(DateTime.Now.Ticks); + raisedPetData.GrowthState = new RaisedPetGrowthState { Name = "BABY" }; + int imageSlot = (viking.Images.Select(i => i.ImageSlot).DefaultIfEmpty(-1).Max() + 1); + raisedPetData.ImagePosition = imageSlot; + // NOTE: Placing an egg into a hatchery slot calls CreatePet, but doesn't SetImage. + // NOTE: We need to force create an image slot because hatching multiple eggs at once would create dragons with the same slot + Image image = new Image { + ImageType = "EggColor", // NOTE: The game doesn't seem to use anything other than EggColor. + ImageSlot = imageSlot, + Viking = viking, + }; + // Save the dragon in the db + Dragon dragon = new Dragon { + EntityId = Guid.NewGuid(), + Viking = viking, + RaisedPetData = XmlUtil.SerializeXml(raisedPetData), + }; + + ctx.Dragons.Add(dragon); + ctx.Images.Add(image); + + if (petTypeID != 2) { + // Minisaurs should not be set as active pet + viking.SelectedDragon = dragon; + ctx.Update(viking); + } + ctx.SaveChanges(); + + return GetRaisedPetDataFromDragon(dragon); + } + [HttpPost] [Produces("application/xml")] [Route("V2/ContentWebService.asmx/CreatePet")] @@ -676,7 +786,7 @@ public IActionResult SetTaskStatev1(Viking viking, [FromForm] Guid userId, [From [Produces("application/xml")] [Route("V2/ContentWebService.asmx/SetTaskState")] [VikingSession] - public IActionResult SetTaskState(Viking viking, [FromForm] Guid userId, [FromForm] int missionId, [FromForm] int taskId, [FromForm] bool completed, [FromForm] string xmlPayload, [FromForm] string apiKey) { + public IActionResult SetTaskState(Viking viking, [FromForm] Guid userId, [FromForm] int missionId, [FromForm] int taskId, [FromForm] bool completed, [FromForm] string xmlPayload, [FromForm] string commonInventoryRequestXml, [FromForm] string apiKey) { if (viking.Uid != userId) return Unauthorized("Can't set not owned task"); @@ -687,6 +797,12 @@ public IActionResult SetTaskState(Viking viking, [FromForm] Guid userId, [FromFo Status = SetTaskStateStatus.TaskCanBeDone, }; + if (commonInventoryRequestXml.Length > 44) { // avoid process inventory on empty xml request, + // NOTE: client do not set this on empty string when no inventory change request, but send + SetCommonInventory(viking, commonInventoryRequestXml); + taskResult.CommonInvRes = new CommonInventoryResponse { Success = true }; + } + if (results.Count > 0) taskResult.MissionsCompleted = results.ToArray(); @@ -749,8 +865,19 @@ public IActionResult PurchaseItems(Viking viking, [FromForm] string purchaseItem PurchaseStoreItemRequest request = XmlUtil.DeserializeXml(purchaseItemRequest); List items = new List(); Gender gender = XmlUtil.DeserializeXml(viking.AvatarSerialized).GenderType; + bool success = true; for (int i = 0; i < request.Items.Length; i++) { int itemId = request.Items[i]; + ItemData item = itemService.GetItem(itemId); + UserGameCurrency currency = achievementService.GetUserCurrency(viking); + int coinCost = (int)Math.Round(item.FinalDiscoutModifier * item.Cost); + int gemCost = (int)Math.Round(item.FinalDiscoutModifier * item.CashCost); + if (currency.GameCurrency - coinCost < 0 && currency.CashCurrency - gemCost < 0) { + success = false; + break; + } + achievementService.AddAchievementPoints(viking, AchievementPointTypes.GameCurrency, -coinCost); + achievementService.AddAchievementPoints(viking, AchievementPointTypes.CashCurrency, -gemCost); if (request.AddMysteryBoxToInventory) { // force add boxes as item (without "opening") items.Add(inventoryService.AddItemToInventoryAndGetResponse(viking, itemId, 1)); @@ -762,7 +889,22 @@ public IActionResult PurchaseItems(Viking viking, [FromForm] string purchaseItem for (int j=0; j(itemIDArrayXml); List items = new List(); Gender gender = XmlUtil.DeserializeXml(viking.AvatarSerialized).GenderType; + bool success = true; for (int i = 0; i < itemIdArr.Length; i++) { - itemService.CheckAndOpenBox(itemIdArr[i], gender, out int itemId, out int quantity); - for (int j=0; j e.PartType == "Sword") is null) { var extraParts = new AvatarDataPart[] { new AvatarDataPart { @@ -139,7 +139,7 @@ private UserProfileData GetProfileDataFromViking(Viking viking, [FromForm] strin ParentUserID = viking.UserId.ToString(), Username = viking.Name, FirstName = viking.Name, - MultiplayerEnabled = (apiKey != "a1a13a0a-7c6e-4e9b-b0f7-22034d799013" && apiKey != "a2a09a0a-7c6e-4e9b-b0f7-22034d799013" && apiKey != "a3a12a0a-7c6e-4e9b-b0f7-22034d799013"), + MultiplayerEnabled = !ClientVersion.IsOldSoD(apiKey), Locale = "en-US", // placeholder GenderID = Gender.Male, // placeholder OpenChatEnabled = true, @@ -167,14 +167,16 @@ private UserProfileData GetProfileDataFromViking(Viking viking, [FromForm] strin } }; + UserGameCurrency currency = achievementService.GetUserCurrency(viking); + return new UserProfileData { ID = viking.Uid.ToString(), AvatarInfo = avatar, AchievementCount = 0, MythieCount = 0, AnswerData = new UserAnswerData { UserID = viking.Uid.ToString() }, - GameCurrency = 65536, - CashCurrency = 65536, + GameCurrency = currency.GameCurrency, + CashCurrency = currency.CashCurrency, ActivityCount = 0, BuddyCount = 0, UserGradeData = new UserGrade { UserGradeID = 0 } diff --git a/src/Controllers/Common/RegistrationController.cs b/src/Controllers/Common/RegistrationController.cs index d19cff51..45882e8d 100644 --- a/src/Controllers/Common/RegistrationController.cs +++ b/src/Controllers/Common/RegistrationController.cs @@ -131,7 +131,7 @@ public IActionResult RegisterChild([FromForm] Guid parentApiToken, [FromForm] st ctx.Vikings.Add(v); ctx.SaveChanges(); - if (apiKey == "a1a13a0a-7c6e-4e9b-b0f7-22034d799013") { + if (ClientVersion.Use2013SoDTutorial(apiKey)) { keyValueService.SetPairData(null, v, null, 2017, new Schema.PairData { Pairs = new Schema.Pair[]{ new Schema.Pair { diff --git a/src/Model/InventoryItem.cs b/src/Model/InventoryItem.cs index 02a62dcb..ea6c5d19 100644 --- a/src/Model/InventoryItem.cs +++ b/src/Model/InventoryItem.cs @@ -8,9 +8,11 @@ public class InventoryItem { public int ItemId { get; set; } public int VikingId { get; set; } - + public string? StatsSerialized { get; set; } + public string? AttributesSerialized { get; set; } + public virtual Viking Viking { get; set; } = null!; public int Quantity { get; set; } diff --git a/src/Resources/defaultmissionlistmam.xml b/src/Resources/defaultmissionlistmam.xml new file mode 100644 index 00000000..b71746b0 --- /dev/null +++ b/src/Resources/defaultmissionlistmam.xml @@ -0,0 +1,12 @@ + + + + + 1750 + 2298 + 1044 + 1074 + + + + diff --git a/src/Resources/items.xml b/src/Resources/items.xml index a53c2384..a43668e3 100644 --- a/src/Resources/items.xml +++ b/src/Resources/items.xml @@ -626930,8 +626930,8 @@ Contains 5x Perfect Aim power-ups. 12732 - 436 - Dragons Bundle + 425 + Dragons Treasure 12732 @@ -909766,8 +909766,8 @@ This bundle contains 10 Frostbite Powerups. 12721 - 436 - Dragons Bundle + 425 + Dragons Treasure 12721 diff --git a/src/Resources/missions.xml b/src/Resources/missions.xml index f0496989..369a3c33 100644 --- a/src/Resources/missions.xml +++ b/src/Resources/missions.xml @@ -2,7 +2,7 @@ 1000 - Quest 1 + Unlock Fishing 3 false @@ -27,7 +27,7 @@ 0 1 - 999 + 1000 1000 1 @@ -3842,7 +3842,7 @@ 5691 Dreadfall2019Q03T06 - <Data><Offer><Type>Popup</Type><ID>939093</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWMala</NPC><Text>That you are, my fierce flame. It's just as well that you're here, {{Name}}. Our dragon hasn't been too cooperative on the way over here.</Text><ItemID>0</ItemID><Priority>0</Priority></Offer><End><Type>Popup</Type><ID>939094</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWDagur</NPC><Text>My darling Mala was the one to discover the one you were looking for. We were out searching for Skrills to join the Berserkers when we came across her. We think she was trying to become friends with a Skrill at Glacier Island, but the Skrill was having none of it. No fault of our mystery dragon; Skrill just are very territorial!</Text><ItemID>0</ItemID><Priority>0</Priority></End><Objective><Pair><Key>Scene</Key><Value>HubTrainingDO</Value></Pair><Pair><Key>NPC</Key><Value>PfDWDagur</Value></Pair><Pair><Key>Time</Key><Value>0</Value></Pair><Pair><Key>HideArrow</Key><Value>False</Value></Pair></Objective><Type>Meet</Type><Title><Text>Talk to Dagur</Text><ID>928987</ID></Title><Desc><Text>Talk to Dagur.</Text><ID>931908</ID></Desc></Data> + <Data><Offer><Type>Popup</Type><ID>939093</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWMala</NPC><Text>That you are, my fierce flame. It's just as well that you're here, {{Name}}. Our dragon hasn't been too cooperative on the way over here.</Text><ItemID>0</ItemID><Priority>0</Priority></Offer><End><Type>Popup</Type><ID>939094</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWDagur</NPC><Text>My darling Mala was the one to discover the one you were looking for. We were out searching for Skrills to join the Berserkers when we came across her. We think she was trying to become friends with a Skrill at Glacier Island, but the Skrill was having none of it. No fault of our mystery dragon; Skrills just are very territorial!</Text><ItemID>0</ItemID><Priority>0</Priority></End><Objective><Pair><Key>Scene</Key><Value>HubTrainingDO</Value></Pair><Pair><Key>NPC</Key><Value>PfDWDagur</Value></Pair><Pair><Key>Time</Key><Value>0</Value></Pair><Pair><Key>HideArrow</Key><Value>False</Value></Pair></Objective><Type>Meet</Type><Title><Text>Talk to Dagur</Text><ID>928987</ID></Title><Desc><Text>Talk to Dagur.</Text><ID>931908</ID></Desc></Data> 0 false @@ -41969,7 +41969,7 @@ 6573 Thawfest2022Story02T01 - <Data><Offer><Type>Popup</Type><ID>942370</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWRuffnut</NPC><Text>Let’s chase down that new zipple-dragon! Now, which way did it go again…</Text><ItemID>0</ItemID><Priority>0</Priority></Offer><Offer><Type>Popup</Type><ID>942371</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWTuffnut</NPC><Text>Well, it left [c][3eebff]Old Berk[/c][ffffff] heading in a northeast direction. Based on that heading, and accounting for air currents, I surmise that it was headed for [c][3eebff]Scuttleclaw Island[/c][ffffff]!</Text><ItemID>0</ItemID><Priority>1</Priority></Offer><Offer><Type>Popup</Type><ID>942372</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWRuffnut</NPC><Text>You just picked a random island, aren’t you?</Text><ItemID>0</ItemID><Priority>2</Priority></Offer><Offer><Type>Popup</Type><ID>942373</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWTuffnut</NPC><Text>Of course I did, who do I look like Heather? @@Well, I guess my hair is long enough…</Text><ItemID>0</ItemID><Priority>3</Priority></Offer><Offer><Type>Popup</Type><ID>942374</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWRuffnut</NPC><Text>Race you to [c][3eebff]Scuttleclaw Island[/c][ffffff], {{Name}}!</Text><ItemID>0</ItemID><Priority>4</Priority></Offer><Objective><Pair><Key>Scene</Key><Value>ScuttleclawIslandDO</Value></Pair><Pair><Key>Time</Key><Value>0</Value></Pair><Pair><Key>HideArrow</Key><Value>False</Value></Pair></Objective><Type>Visit</Type><Title><Text>Go to Scuttleclaw Island</Text><ID>930024</ID></Title><Desc><Text>Go to Scuttleclaw Island</Text><ID>938741</ID></Desc></Data> + <Data><Offer><Type>Popup</Type><ID>942370</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWRuffnut</NPC><Text>Let’s chase down that new zipple-dragon! Now, which way did it go again…</Text><ItemID>0</ItemID><Priority>0</Priority></Offer><Offer><Type>Popup</Type><ID>942371</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWTuffnut</NPC><Text>Well, it left [c][3eebff]Old Berk[/c][ffffff] heading in a northeast direction. Based on that heading, and accounting for air currents, I surmise that it was headed for [c][3eebff]Scuttleclaw Island[/c][ffffff]!</Text><ItemID>0</ItemID><Priority>1</Priority></Offer><Offer><Type>Popup</Type><ID>942372</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWRuffnut</NPC><Text>You just picked a random island, didn’t you?</Text><ItemID>0</ItemID><Priority>2</Priority></Offer><Offer><Type>Popup</Type><ID>942373</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWTuffnut</NPC><Text>Of course I did, who do I look like Heather? @@Well, I guess my hair is long enough…</Text><ItemID>0</ItemID><Priority>3</Priority></Offer><Offer><Type>Popup</Type><ID>942374</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWRuffnut</NPC><Text>Race you to [c][3eebff]Scuttleclaw Island[/c][ffffff], {{Name}}!</Text><ItemID>0</ItemID><Priority>4</Priority></Offer><Objective><Pair><Key>Scene</Key><Value>ScuttleclawIslandDO</Value></Pair><Pair><Key>Time</Key><Value>0</Value></Pair><Pair><Key>HideArrow</Key><Value>False</Value></Pair></Objective><Type>Visit</Type><Title><Text>Go to Scuttleclaw Island</Text><ID>930024</ID></Title><Desc><Text>Go to Scuttleclaw Island</Text><ID>938741</ID></Desc></Data> 0 false @@ -92316,7 +92316,7 @@ 441 Task 2.1 collect 15 flowers - <Data><Setup><Scene>HubSchoolDO</Scene><Asset>RS_DATA/PfGrpQTeenHic02T01.unity3d/PfGrpQTeenHic02T01</Asset><Recursive>false</Recursive><Persistent>false</Persistent></Setup><Offer><Type>Popup</Type><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWHiccup</NPC><Text>Toothless is my best friend; the very first time I looked at him, I saw myself. I knew it would be just me and him forever. That's the bond you and your dragon need to make. You should trust {{dragon name}} as much as you do yourself. @@ How about some bonding exercises? Ride {{dragon name}} and collect the scented flowers around the school. Dragons love the smell!</Text><ID>904750</ID><ItemID>0</ItemID><Priority>0</Priority></Offer><Offer><Type>VO</Type><Asset>RS_DATA/DlgDWHiccupDO.unity3d/DlgHiccupToothless01</Asset><NPC>PfDWHiccup</NPC><ItemID>0</ItemID><Priority>0</Priority></Offer><End><Type>Popup</Type><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWHiccup</NPC><Text>That was amazing and some great team work! </Text><ID>904751</ID><ItemID>0</ItemID><Priority>0</Priority></End><End><Type>VO</Type><Asset>RS_DATA/DlgDWHiccupDO.unity3d/DlgHiccupPos03</Asset><NPC>PfDWHiccup</NPC><ItemID>0</ItemID><Priority>0</Priority></End><Objective><Pair><Key>Name</Key><Value>PfCollectDWFlower02</Value></Pair><Pair><Key>Quantity</Key><Value>15</Value></Pair></Objective><Type>Collect</Type><Title><Text>Work with your dragon to collect 12 flowers</Text><ID>904748</ID></Title><Desc><Text>Collect 12 scented flowers around the school.</Text><ID>922953</ID></Desc></Data> + <Data><Setup><Scene>HubSchoolDO</Scene><Asset>RS_DATA/PfGrpQTeenHic02T01.unity3d/PfGrpQTeenHic02T01</Asset><Recursive>false</Recursive><Persistent>false</Persistent></Setup><Offer><Type>Popup</Type><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWHiccup</NPC><Text>Toothless is my best friend; the very first time I looked at him, I saw myself. I knew it would be just me and him forever. That's the bond you and your dragon need to make. You should trust {{dragon name}} as much as you do yourself. @@ How about some bonding exercises? Ride {{dragon name}} and collect the scented flowers around the school. Dragons love the smell!</Text><ID>904750</ID><ItemID>0</ItemID><Priority>0</Priority></Offer><Offer><Type>VO</Type><Asset>RS_DATA/DlgDWHiccupDO.unity3d/DlgHiccupToothless01</Asset><NPC>PfDWHiccup</NPC><ItemID>0</ItemID><Priority>0</Priority></Offer><End><Type>Popup</Type><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWHiccup</NPC><Text>That was amazing and some great team work! </Text><ID>904751</ID><ItemID>0</ItemID><Priority>0</Priority></End><End><Type>VO</Type><Asset>RS_DATA/DlgDWHiccupDO.unity3d/DlgHiccupPos03</Asset><NPC>PfDWHiccup</NPC><ItemID>0</ItemID><Priority>0</Priority></End><Objective><Pair><Key>Name</Key><Value>PfCollectDWFlower02</Value></Pair><Pair><Key>Quantity</Key><Value>15</Value></Pair></Objective><Type>Collect</Type><Title><Text>Work with your dragon to collect 15 flowers</Text><ID>904748</ID></Title><Desc><Text>Collect 12 scented flowers around the school.</Text><ID>922953</ID></Desc></Data> 0 false @@ -93170,7 +93170,7 @@ 6118 2020SnoggletogStory01T04 - <Data><Offer><Type>Popup</Type><ID>940068</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWHarald</NPC><Text>Whatever do you mean, dear Hiccup? Do you think that you have cause to be worried? Fret not, old friend! We have a lot of history between us, and you know I will not attack you without provocation.</Text><ItemID>0</ItemID><Priority>0</Priority></Offer><Offer><Type>Popup</Type><ID>940069</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWHiccup</NPC><Text>'A lot of history between us' is why I'm tense, Harald.</Text><ItemID>0</ItemID><Priority>1</Priority></Offer><End><Type>Popup</Type><ID>940070</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWHarald</NPC><Text>Regardless: I know Snoggletog is important to Berkians. Stormheart and I have a preposition for you, in the spirit of the holidays. I only hope that you can approach it with the same courtesy.</Text><ItemID>0</ItemID><Priority>0</Priority></End><Objective><Pair><Key>Scene</Key><Value>HubDragonsEdgeDO</Value></Pair><Pair><Key>NPC</Key><Value>PfDWHarald</Value></Pair><Pair><Key>Time</Key><Value>0</Value></Pair><Pair><Key>HideArrow</Key><Value>False</Value></Pair></Objective><Type>Meet</Type><Title><Text>Talk to Harald</Text><ID>927532</ID></Title></Data> + <Data><Offer><Type>Popup</Type><ID>940068</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWHarald</NPC><Text>Whatever do you mean, dear Hiccup? Do you think that you have cause to be worried? Fret not, old friend! We have a lot of history between us, and you know I will not attack you without provocation.</Text><ItemID>0</ItemID><Priority>0</Priority></Offer><Offer><Type>Popup</Type><ID>940069</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWHiccup</NPC><Text>'A lot of history between us' is why I'm tense, Harald.</Text><ItemID>0</ItemID><Priority>1</Priority></Offer><End><Type>Popup</Type><ID>940070</ID><Asset>PfUiMissionActionDBDO</Asset><NPC>PfDWHarald</NPC><Text>Regardless: I know Snoggletog is important to Berkians. Stormheart and I have a proposition for you, in the spirit of the holidays. I only hope that you can approach it with the same courtesy.</Text><ItemID>0</ItemID><Priority>0</Priority></End><Objective><Pair><Key>Scene</Key><Value>HubDragonsEdgeDO</Value></Pair><Pair><Key>NPC</Key><Value>PfDWHarald</Value></Pair><Pair><Key>Time</Key><Value>0</Value></Pair><Pair><Key>HideArrow</Key><Value>False</Value></Pair></Objective><Type>Meet</Type><Title><Text>Talk to Harald</Text><ID>927532</ID></Title></Data> 0 false @@ -125774,4 +125774,186 @@ false + + 1750 + Unlock Fishing + 3 + + false + 1 + + + 2 + False + 0 + false + + + 1 + False + 0 + false + + + all + true + 1 + 0 + + 1 + 1750 + 1000 + 1 + + + + 1 + 0 + 0 + + 1000 + Unlock Fishing + + 1 + false + + false + + + 2298 + Unlock Fishing + 3 + + false + 1 + + + 2 + False + 0 + false + + + 1 + False + 0 + false + + + all + true + 1 + 0 + + 1 + 2298 + 1000 + 1 + + + + 1 + 0 + 0 + + 1000 + Unlock Fishing + + 1 + false + + false + + + 11044 + + Unlock Grow Up + 3 + + false + 0 + + + 1 + False + 0 + false + + + 4 + 8,5 + 0 + true + + + all + true + 1 + 1 + + 1 + 1044 + 11044 + 0 + + + + 1 + 0 + 0 + + 11044 + Unlock Grow Up + <Data><Objective><Pair><Key>Name</Key><Value>GrowDragon</Value></Pair></Objective><Type>Action</Type></Data> + 0 + false + + true + + + 11074 + + Unlock Grow Up + 3 + + false + 0 + + + 1 + False + 0 + false + + + 4 + 8,10 + 0 + true + + + all + true + 1 + 1 + + 1 + 1074 + 11074 + 0 + + + + 1 + 0 + 0 + + 11074 + Unlock Grow Up + <Data><Objective><Pair><Key>Name</Key><Value>GrowDragon</Value></Pair></Objective><Type>Action</Type></Data> + 0 + false + + true + diff --git a/src/Resources/store.xml b/src/Resources/store.xml index 7aaf1083..3ecb7fee 100644 --- a/src/Resources/store.xml +++ b/src/Resources/store.xml @@ -17100,6 +17100,8 @@ SoD 3.31 main store section and subsection filtering: 2013-01-01T00:00:00 2030-12-31T00:00:00 + 12732 + 12721 12238 12239 12016 diff --git a/src/Schema/ItemData.cs b/src/Schema/ItemData.cs index bd959334..fc189477 100644 --- a/src/Schema/ItemData.cs +++ b/src/Schema/ItemData.cs @@ -97,4 +97,17 @@ public class ItemData [XmlElement(ElementName = "p", IsNullable = true)] public int? Points; + + [XmlIgnore] + public float NormalDiscoutModifier; + + [XmlIgnore] + public float MemberDiscountModifier; + + [XmlIgnore] + public float FinalDiscoutModifier { + get { + return Math.Min(1f, (1f - NormalDiscoutModifier) * (1f - MemberDiscountModifier)); + } + } } diff --git a/src/Schema/SetDisplayNameRequest.cs b/src/Schema/SetDisplayNameRequest.cs new file mode 100644 index 00000000..ce030aa1 --- /dev/null +++ b/src/Schema/SetDisplayNameRequest.cs @@ -0,0 +1,16 @@ +using System.Xml.Serialization; + +namespace sodoff.Schema; + +[XmlRoot(ElementName = "sdnr", Namespace = "")] +[Serializable] +public class SetDisplayNameRequest { + [XmlElement(ElementName = "dn")] + public string DisplayName { get; set; } + + [XmlElement(ElementName = "iid")] + public int ItemID { get; set; } + + [XmlElement(ElementName = "sid")] + public int StoreID { get; set; } +} diff --git a/src/Services/AchievementService.cs b/src/Services/AchievementService.cs index 87e8ad6b..9396dfa7 100644 --- a/src/Services/AchievementService.cs +++ b/src/Services/AchievementService.cs @@ -88,6 +88,8 @@ public void SetAchievementPoints(Viking viking, AchievementPointTypes type, int value = 0; } + ctx.SaveChanges(); + return new AchievementReward{ EntityID = viking.Uid, PointTypeID = type, @@ -157,10 +159,20 @@ public AchievementReward[] ApplyAchievementRewardsByTask(Viking viking, Achievem public UserGameCurrency GetUserCurrency(Viking viking) { // TODO: return real values (after implement currency collecting methods) + int? coins = viking.AchievementPoints.FirstOrDefault(x => x.Type == (int)AchievementPointTypes.GameCurrency)?.Value; + int? gems = viking.AchievementPoints.FirstOrDefault(x => x.Type == (int)AchievementPointTypes.CashCurrency)?.Value; + if (coins is null) { + coins = 300; + AddAchievementPoints(viking, AchievementPointTypes.GameCurrency, coins); + } + if (gems is null) { + gems = 75; + AddAchievementPoints(viking, AchievementPointTypes.CashCurrency, gems); + } return new UserGameCurrency { - CashCurrency = 65536, - GameCurrency = 65536, - UserGameCurrencyID = 1, // TODO: user's wallet ID? + CashCurrency = gems, + GameCurrency = coins, + UserGameCurrencyID = viking.Id, UserID = viking.Uid }; } diff --git a/src/Services/GameDataService.cs b/src/Services/GameDataService.cs index cd4eed2f..dc40974c 100644 --- a/src/Services/GameDataService.cs +++ b/src/Services/GameDataService.cs @@ -115,7 +115,9 @@ private void SavePairs(Model.GameData gameData, string xmlDocumentData) { GameDataPair? dbPair = gameData.GameDataPairs.FirstOrDefault(x => x.Name == pair.Name); if (dbPair == null) gameData.GameDataPairs.Add(pair); - else if (dbPair.Value <= pair.Value) + else if (pair.Name == "time" && dbPair.Value > pair.Value) + dbPair.Value = pair.Value; + else if (pair.Name != "time" && dbPair.Value <= pair.Value) dbPair.Value = pair.Value; } } diff --git a/src/Services/InventoryService.cs b/src/Services/InventoryService.cs index 825e1184..ad2c0622 100644 --- a/src/Services/InventoryService.cs +++ b/src/Services/InventoryService.cs @@ -26,8 +26,8 @@ public InventoryItem AddItemToInventory(Viking viking, int itemID, int quantity) ItemId = itemID, Quantity = 0 }; - if (itemData.ItemStatsMap is null && itemData.PossibleStatsMap != null) { - // battle item without default stats + if (itemData.ItemStatsMap is null && itemData.PossibleStatsMap != null && !itemService.ItemHasCategory(itemData, 651)) { + // battle item without default stats, but not blueprints Random random = new Random(); int itemTier = random.Next(1, 3); item.StatsSerialized = XmlUtil.SerializeXml(new ItemStatsMap { @@ -136,6 +136,9 @@ public CommonInventoryData GetCommonInventoryData(Viking viking) { uid.ItemStats = itemData.ItemStatsMap?.ItemStats; uid.ItemTier = itemData.ItemStatsMap?.ItemTier; } + if (item.AttributesSerialized != null) { + uid.UserItemAttributes = XmlUtil.DeserializeXml(item.AttributesSerialized); + } userItemData.Add(uid); } @@ -149,7 +152,7 @@ public bool ItemNeedUniqueInventorySlot(int itemId) { ItemData itemData = itemService.GetItem(itemId); if (itemData.PossibleStatsMap != null) // dragons tactics (battle) items return true; - if (itemService.ItemHasCategory(itemData, 541)) // farm expansion + if (itemService.ItemHasCategory(itemData, new int[] {541, 657})) // farm expansion or customizable items return true; return false; } diff --git a/src/Services/ItemService.cs b/src/Services/ItemService.cs index 13c1ae52..38c0e94f 100644 --- a/src/Services/ItemService.cs +++ b/src/Services/ItemService.cs @@ -90,6 +90,22 @@ public bool IsBundleItem(int itemId) { return items[itemId].Relationship?.FirstOrDefault(e => e.Type == "Bundle") != null; } + public bool IsGemBundle(int itemId, out int value) { + value = 0; + ItemAttribute? attribute = items[itemId].Attribute?.FirstOrDefault(e => e.Key == "VCashRedemptionValue"); + if (attribute != null && int.TryParse(attribute.Value, out int result)) + value = result; + return attribute != null; + } + + public bool IsCoinBundle(int itemId, out int value) { + value = 0; + ItemAttribute? attribute = items[itemId].Attribute?.FirstOrDefault(e => e.Key == "CoinRedemptionValue"); + if (attribute != null && int.TryParse(attribute.Value, out int result)) + value = result; + return attribute != null; + } + public bool CheckItemGender(ItemData itemData, Gender gender) { string? itemGender = itemData.Attribute?.FirstOrDefault(e => e.Key == "Gender")?.Value; if (itemGender != null) { diff --git a/src/Services/MissionService.cs b/src/Services/MissionService.cs index 3a28f010..767d4234 100644 --- a/src/Services/MissionService.cs +++ b/src/Services/MissionService.cs @@ -1,5 +1,6 @@ using sodoff.Model; using sodoff.Schema; +using sodoff.Util; using System.Runtime.Serialization.Formatters.Binary; using System.Threading.Tasks; @@ -17,19 +18,29 @@ public MissionService(DBContext ctx, MissionStoreSingleton missionStore, Achieve } public Mission GetMissionWithProgress(int missionId, int userId, string apiKey) { - Mission mission; - if (missionId == 999 && apiKey == "a3a12a0a-7c6e-4e9b-b0f7-22034d799013") { // TODO This is not a pretty solution with hard-coded values. - mission = missionStore.GetMission(10999); - mission.MissionID = 999; - } else if (missionId == 999 && apiKey == "a2a09a0a-7c6e-4e9b-b0f7-22034d799013") { - mission = missionStore.GetMission(20999); - mission.MissionID = 999; - } else if (missionId == 999 && apiKey == "a1a13a0a-7c6e-4e9b-b0f7-22034d799013") { - mission = missionStore.GetMission(30999); - mission.MissionID = 999; - } else { + Mission mission = null; + + if (missionId == 999) { // TODO This is not a pretty solution with hard-coded values. + if (ClientVersion.Use2013SoDTutorial(apiKey)) { + mission = missionStore.GetMission(30999); + } else if (ClientVersion.Use2016SoDTutorial(apiKey)) { + mission = missionStore.GetMission(20999); + } else if (ClientVersion.Use2019SoDTutorial(apiKey)) { + mission = missionStore.GetMission(10999); + } + } else if (missionId == 1044 && ClientVersion.IsMaM(apiKey)) { + mission = missionStore.GetMission(11044); + } else if (missionId == 1074 && ClientVersion.IsMaM(apiKey)) { + mission = missionStore.GetMission(11074); + } + + if (mission is null) { mission = missionStore.GetMission(missionId); + } else { + // mission use overwrite variant ... so we need fix MissionID + mission.MissionID = missionId; } + UpdateMissionRecursive(mission, userId); return mission; } @@ -63,7 +74,10 @@ public List UpdateTaskProgress(int missionId, int taskId task.Payload = null; task.Completed = false; } - missionState.MissionStatus = MissionStatus.Upcoming; + if (missionStore.GetActiveMissions(apiKey).Contains(missionId)) + missionState.MissionStatus = MissionStatus.Active; + else + missionState.MissionStatus = MissionStatus.Upcoming; } else { missionState.MissionStatus = MissionStatus.Completed; } diff --git a/src/Services/MissionStoreSingleton.cs b/src/Services/MissionStoreSingleton.cs index 4acadabd..6e294dbd 100644 --- a/src/Services/MissionStoreSingleton.cs +++ b/src/Services/MissionStoreSingleton.cs @@ -10,6 +10,8 @@ public class MissionStoreSingleton { private int[] upcomingMissions; private int[] activeMissionsV1; private int[] upcomingMissionsV1; + private int[] activeMissionsMaM; + private int[] upcomingMissionsMaM; public MissionStoreSingleton() { ServerMissionArray missionArray = XmlUtil.DeserializeXml(XmlUtil.ReadResourceXmlString("missions")); @@ -19,10 +21,14 @@ public MissionStoreSingleton() { } activeMissions = defaultMissions.Active; upcomingMissions = defaultMissions.Upcoming; - + defaultMissions = XmlUtil.DeserializeXml(XmlUtil.ReadResourceXmlString("defaultmissionlistv1")); activeMissionsV1 = defaultMissions.Active; upcomingMissionsV1 = defaultMissions.Upcoming; + + defaultMissions = XmlUtil.DeserializeXml(XmlUtil.ReadResourceXmlString("defaultmissionlistmam")); + activeMissionsMaM = defaultMissions.Active; + upcomingMissionsMaM = defaultMissions.Upcoming; } public Mission GetMission(int missionID) { @@ -30,16 +36,22 @@ public Mission GetMission(int missionID) { } public int[] GetActiveMissions(string apiKey) { - if (apiKey == "a1a13a0a-7c6e-4e9b-b0f7-22034d799013") { + if (ClientVersion.Use2013SoDTutorial(apiKey)) { return activeMissionsV1; } + if (ClientVersion.IsMaM(apiKey)) { + return activeMissionsMaM; + } return activeMissions; } public int[] GetUpcomingMissions(string apiKey) { - if (apiKey == "a1a13a0a-7c6e-4e9b-b0f7-22034d799013") { + if (ClientVersion.Use2013SoDTutorial(apiKey)) { return upcomingMissionsV1; } + if (ClientVersion.IsMaM(apiKey)) { + return upcomingMissionsMaM; + } return upcomingMissions; } diff --git a/src/Services/StoreService.cs b/src/Services/StoreService.cs index b20a7ced..8994043c 100644 --- a/src/Services/StoreService.cs +++ b/src/Services/StoreService.cs @@ -17,8 +17,11 @@ public StoreService(ItemService itemService) { SalesAtStore = s.SalesAtStore, PopularItems = s.PopularItems }; - for (int i=0; i? memberSales = s.SalesAtStore?.Where(x => x.ForMembers == true); + IEnumerable? normalSales = s.SalesAtStore?.Where(x => x.ForMembers == false || x.ForMembers == null); + for (int i = 0; i < s.ItemId.Length; ++i) { newStore.Items[i] = itemService.GetItem(s.ItemId[i]); + UpdateItemSaleModifier(newStore.Items[i], memberSales, normalSales); } stores.Add(s.Id, newStore); } @@ -27,4 +30,39 @@ public StoreService(ItemService itemService) { public ItemsInStoreData GetStore(int id) { return stores[id]; } + + private bool IsSaleOutdated(ItemsInStoreDataSale sale) { + if (sale.EndDate == null) + return false; + return sale.EndDate < DateTime.Now; + } + + private void UpdateItemSaleModifier(ItemData item, IEnumerable? memberSales, IEnumerable? normalSales) { + if (memberSales != null) { + foreach (var memberSale in memberSales) { + if (IsSaleOutdated(memberSale)) continue; + if (item.Category != null && memberSale.CategoryIDs != null && item.Category.Any(x => memberSale.CategoryIDs.Contains(x.CategoryId))) { + item.MemberDiscountModifier = memberSale.Modifier; + break; + } + if (memberSale.ItemIDs != null && memberSale.ItemIDs.Contains(item.ItemID)) { + item.MemberDiscountModifier = memberSale.Modifier; + break; + } + } + } + if (normalSales != null) { + foreach (var normalSale in normalSales) { + if (IsSaleOutdated(normalSale)) continue; + if (item.Category != null && normalSale.CategoryIDs != null && item.Category.Any(x => normalSale.CategoryIDs.Contains(x.CategoryId))) { + item.NormalDiscoutModifier = normalSale.Modifier; + break; + } + if (normalSale.ItemIDs != null && normalSale.ItemIDs.Contains(item.ItemID)) { + item.NormalDiscoutModifier = normalSale.Modifier; + break; + } + } + } + } } diff --git a/src/Util/ClientVersion.cs b/src/Util/ClientVersion.cs new file mode 100644 index 00000000..88e1697f --- /dev/null +++ b/src/Util/ClientVersion.cs @@ -0,0 +1,34 @@ +namespace sodoff.Util; +public class ClientVersion { + public static bool IsOldSoD(string apiKey) { + return ( + apiKey == "a1a06a0a-7c6e-4e9b-b0f7-22034d799013" || + apiKey == "a1a13a0a-7c6e-4e9b-b0f7-22034d799013" || + apiKey == "a2a09a0a-7c6e-4e9b-b0f7-22034d799013" || + apiKey == "a3a12a0a-7c6e-4e9b-b0f7-22034d799013" + ); + } + public static bool Use2013SoDTutorial(string apiKey) { + return ( + apiKey == "a1a06a0a-7c6e-4e9b-b0f7-22034d799013" || + apiKey == "a1a13a0a-7c6e-4e9b-b0f7-22034d799013" + ); + } + public static bool Use2016SoDTutorial(string apiKey) { + return ( + apiKey == "a2a09a0a-7c6e-4e9b-b0f7-22034d799013" + ); + } + public static bool Use2019SoDTutorial(string apiKey) { + return ( + apiKey == "a3a12a0a-7c6e-4e9b-b0f7-22034d799013" + ); + } + public static bool Use2021SoDTutorial(string apiKey) { + return !IsOldSoD(apiKey); + } + + public static bool IsMaM(string apiKey) { + return apiKey == "e20150cc-ff70-435c-90fd-341dc9161cc3"; + } +} diff --git a/src/Util/XmlUtil.cs b/src/Util/XmlUtil.cs index ec31f938..aaaaa2e6 100644 --- a/src/Util/XmlUtil.cs +++ b/src/Util/XmlUtil.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Xml.Serialization; +using System.Text; namespace sodoff.Util; public class XmlUtil { @@ -9,9 +10,13 @@ public static T DeserializeXml(string xmlString) { return (T)serializer.Deserialize(reader); } + private class Utf8StringWriter : StringWriter { + public override Encoding Encoding => Encoding.UTF8; + } + public static string SerializeXml(T xmlObject) { var serializer = new XmlSerializer(typeof(T)); - using (var writer = new StringWriter()) { + using (var writer = new Utf8StringWriter()) { serializer.Serialize(writer, xmlObject); return writer.ToString(); } diff --git a/src/sodoff.csproj b/src/sodoff.csproj index 069b2dfe..5bded66d 100644 --- a/src/sodoff.csproj +++ b/src/sodoff.csproj @@ -25,6 +25,7 @@ + @@ -89,5 +90,8 @@ PreserveNewest + + PreserveNewest +