support for old ("steps") missions (#20)

* add support for old missions API (aka "steps missions")
* config option to disable loading non SoD Data (used only for missions and achievements for now)
* make AuthenticateUser endpoint compatible with games that use e-mail as login
* add api keys for lands
* add GetGameCurrency endpoint
* allow create empty stores and add store "8" (empty)

---------

Co-authored-by: Robert Paciorek <robert@opcode.eu.org>
This commit is contained in:
YoshiCraft64 2025-02-13 15:58:19 -06:00 committed by GitHub
parent 13df822608
commit cecaa50610
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 28693 additions and 35 deletions

View File

@ -44,6 +44,8 @@ By default (modifiable in appsettings.json), files for version 2.5.0 and newer w
A sample file is provided for `{PLATFORM} = WIN`, `{VERSION} = 3.31.0`.
It assumes that the server address are `localhost:5000` (API) and `localhost:5001` (assets).
To use this server with games other than School of Dragons you need to change the `LoadNonSoDData` value in `appsettings.json` to `true`.
#### Asset Server
Various settings for the asset server are customizable, with the key one being `ProviderURL`, which specifies the source for downloading assets in `partial` mode. By default, it's configured to use archive.org

View File

@ -6,6 +6,8 @@ public class ApiServerConfig {
public int MMOPort { get; set; } = 9933;
public uint MMOSupportMinVersion { get; set; } = 0;
public bool LoadNonSoDData { get; set; } = false;
public DbProviders DbProvider { get; set; } = DbProviders.SQLite;
public string DbPath { get; set; } = string.Empty;
public string DbConnection { get; set; } = string.Empty;

View File

@ -87,12 +87,14 @@ public class AuthenticationController : Controller {
[Route("v3/AuthenticationWebService.asmx/AuthenticateUser")]
[DecryptRequest("username")]
[DecryptRequest("password")]
public bool AuthenticateUser() {
public bool AuthenticateUser([FromForm] string apiKey) {
String username = Request.Form["username"];
String password = Request.Form["password"];
// Authenticate the user
User? user = ctx.Users.FirstOrDefault(e => e.Username == username);
User? user = (ClientVersion.GetVersion(apiKey) <= ClientVersion.Max_OldJS)
? ctx.Users.FirstOrDefault(e => e.Email == username)
: ctx.Users.FirstOrDefault(e => e.Username == username);
if (user is null || new PasswordHasher<object>().VerifyHashedPassword(null, user.Password, password) != PasswordVerificationResult.Success) {
return false;
}

View File

@ -15,6 +15,7 @@ public class ContentController : Controller {
private readonly DBContext ctx;
private KeyValueService keyValueService;
private ItemService itemService;
private MissionStoreSingleton missionStore;
private MissionService missionService;
private RoomService roomService;
private AchievementService achievementService;
@ -22,6 +23,7 @@ public class ContentController : Controller {
private GameDataService gameDataService;
private DisplayNamesService displayNamesService;
private NeighborhoodService neighborhoodService;
private WorldIdService worldIdService;
private Random random = new Random();
private readonly IOptions<ApiServerConfig> config;
@ -29,6 +31,7 @@ public class ContentController : Controller {
DBContext ctx,
KeyValueService keyValueService,
ItemService itemService,
MissionStoreSingleton missionStore,
MissionService missionService,
RoomService roomService,
AchievementService achievementService,
@ -36,11 +39,13 @@ public class ContentController : Controller {
GameDataService gameDataService,
DisplayNamesService displayNamesService,
NeighborhoodService neighborhoodService,
WorldIdService worldIdService,
IOptions<ApiServerConfig> config
) {
this.ctx = ctx;
this.keyValueService = keyValueService;
this.itemService = itemService;
this.missionStore = missionStore;
this.missionService = missionService;
this.roomService = roomService;
this.achievementService = achievementService;
@ -48,6 +53,7 @@ public class ContentController : Controller {
this.gameDataService = gameDataService;
this.displayNamesService = displayNamesService;
this.neighborhoodService = neighborhoodService;
this.worldIdService = worldIdService;
this.config = config;
}
@ -1673,10 +1679,17 @@ public class ContentController : Controller {
[Route("ContentWebService.asmx/GetUserGameCurrency")]
[VikingSession]
public IActionResult GetUserGameCurrency(Viking viking) {
// TODO: This is a placeholder
return Ok(achievementService.GetUserCurrency(viking));
}
[HttpPost]
[Produces("application/xml")]
[Route("ContentWebService.asmx/GetGameCurrency")]
[VikingSession]
public IActionResult GetGameCurrency(Viking viking) {
return Ok(achievementService.GetUserCurrency(viking).GameCurrency);
}
[HttpPost]
[Produces("application/xml")]
[Route("ContentWebService.asmx/SetGameCurrency")] // used by World Of Jumpstart
@ -2121,10 +2134,72 @@ public class ContentController : Controller {
[HttpPost]
[Produces("application/xml")]
[Route("MissionWebService.asmx/GetWorldId")] // used by Math Blaster
public IActionResult GetWorldId() {
// TODO: This is a placeholder
return Ok(0);
[Route("MissionWebService.asmx/GetWorldId")] // used by Math Blaster and WoJS Adventureland
public IActionResult GetWorldId([FromForm] int gameId, [FromForm] string sceneName) {
return Ok(worldIdService.GetWorldID(sceneName));
}
[HttpPost]
[Produces("application/xml")]
[Route("MissionWebService.asmx/GetMission")] // old ("step") missions - used by MB and WoJS lands
public IActionResult GetMission([FromForm] int gameId, [FromForm] string name) {
return Ok(missionStore.GetStepsMissions(gameId, name));
}
[HttpPost]
[Produces("application/xml")]
[Route("MissionWebService.asmx/GetStep")] // old ("step") missions - used by MB and WoJS lands
public IActionResult GetMissionStep([FromForm] int stepId) {
return Ok(missionStore.GetStep(stepId));
}
[HttpPost]
[Produces("application/xml")]
[Route("ContentWebService.asmx/GetUserMission")] // old ("step") missions - used by MB and WoJS lands
[VikingSession]
public IActionResult GetUserMission(Viking viking, [FromForm] int worldId) {
return Ok(missionService.GetUserMissionData(viking, worldId));
}
[HttpPost]
[Produces("application/xml")]
[Route("ContentWebService.asmx/SetUserMission")] // old ("step") missions - used by MB and WoJS lands
[VikingSession(UseLock=true)]
public IActionResult SetUserMission(Viking viking, [FromForm] int worldId, [FromForm] int missionId, [FromForm] int stepId, [FromForm] int taskId) {
missionService.SetOrUpdateUserMissionData(viking, worldId, missionId, stepId, taskId);
return Ok(true);
}
[HttpPost]
[Produces("application/xml")]
[Route("ContentWebService.asmx/SetUserMissionComplete")] // old ("step") missions - used by MB and WoJS lands
[VikingSession]
public IActionResult SetUserMissionComplete(Viking viking, [FromForm] int worldId, [FromForm] int missionId) {
return Ok(missionService.SetUserMissionCompleted(viking, worldId, missionId, true));
}
[HttpPost]
//[Produces("application/xml")]
[Route("MissionWebService.asmx/GetBadge")] // old ("step") missions - used by MB and WoJS lands
public IActionResult GetBadge([FromForm] int gameId) {
if (gameId == 1) return Ok(XmlUtil.ReadResourceXmlString("missions.badge_wojs_al"));
return Ok();
}
[HttpPost]
[Produces("application/xml")]
[Route("ContentWebService.asmx/SetUserBadgeComplete")] // old ("step") missions - used by MB and WoJS lands
[VikingSession]
public IActionResult SetUserBadgeComplete(Viking viking, [FromForm] int badgeId) {
return Ok(missionService.SetUserBadgeComplete(viking, badgeId));
}
[HttpPost]
[Produces("application/xml")]
[Route("ContentWebService.asmx/GetUserBadgeComplete")] // old ("step") missions - used by MB and WoJS lands
[VikingSession]
public IActionResult GetUserBadgeComplete(Viking viking) {
return Ok(missionService.GetUserBadgesCompleted(viking));
}
[HttpPost]

View File

@ -28,6 +28,8 @@ public class DBContext : DbContext {
public DbSet<Group> Groups { get; set; } = null!;
public DbSet<Rating> Ratings { get; set; } = null!;
public DbSet<RatingRank> RatingRanks { get; set; } = null!;
public DbSet<UserMissionData> UserMissionData { get; set; } = null!;
public DbSet<UserBadgeCompleteData> UserBadgesCompleted { get; set; } = null!;
private readonly IOptions<ApiServerConfig> config;
@ -148,6 +150,12 @@ public class DBContext : DbContext {
builder.Entity<Viking>().HasMany(v => v.Ratings)
.WithOne(r => r.Viking);
builder.Entity<Viking>().HasMany(v => v.UserMissions)
.WithOne(r => r.Viking);
builder.Entity<Viking>().HasMany(v => v.UserBadgesCompleted)
.WithOne(r => r.Viking);
// Dragons
builder.Entity<Dragon>().HasOne(d => d.Viking)
.WithMany(e => e.Dragons)
@ -284,5 +292,14 @@ public class DBContext : DbContext {
builder.Entity<RatingRank>().HasMany(rr => rr.Ratings)
.WithOne(r => r.Rank);
// old ("step") missions
builder.Entity<UserMissionData>().HasOne(r => r.Viking)
.WithMany(v => v.UserMissions)
.HasForeignKey(r => r.VikingId);
builder.Entity<UserBadgeCompleteData>().HasOne(r => r.Viking)
.WithMany(v => v.UserBadgesCompleted)
.HasForeignKey(r => r.VikingId);
}
}

View File

@ -0,0 +1,11 @@
namespace sodoff.Model
{
public class UserBadgeCompleteData
{
public int Id { get; set; }
public int VikingId { get; set; }
public int BadgeId { get; set; }
public virtual Viking? Viking { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace sodoff.Model
{
[PrimaryKey(nameof(VikingId), nameof(WorldId), nameof(MissionId))]
public class UserMissionData
{
public int VikingId { get; set; }
public int WorldId { get; set; }
public int MissionId { get; set; }
public int StepId { get; set; }
public int TaskId { get; set; }
public bool IsCompleted { get; set; } = false;
public virtual Viking? Viking { get; set; }
}
}

View File

@ -42,6 +42,8 @@ public class Viking {
public virtual ICollection<Group> Groups { get; set; } = null!;
public virtual ICollection<Rating> Ratings { get; set; } = null!;
public virtual Dragon? SelectedDragon { get; set; }
public virtual ICollection<UserMissionData> UserMissions { get; set; } = null!;
public virtual ICollection<UserBadgeCompleteData> UserBadgesCompleted { get; set; } = null!;
public DateTime? CreationDate { get; set; }
public DateTime? BirthDate { get; set; }

View File

@ -30,6 +30,7 @@ builder.Services.AddSingleton<ItemService>();
builder.Services.AddSingleton<StoreService>();
builder.Services.AddSingleton<DisplayNamesService>();
builder.Services.AddSingleton<MMOConfigService>();
builder.Services.AddSingleton<WorldIdService>();
builder.Services.AddScoped<KeyValueService>();
builder.Services.AddScoped<MissionService>();

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<BadgeData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Badge>
<BadgeId>1</BadgeId>
<Name>Explorer Badge</Name>
<Description>Adventure Land Badge 01</Description>
<Experience>1</Experience>
<Pieces>10</Pieces>
<Mask>Badges01.unity3d/MskBadge10</Mask>
<Color>Badges01.unity3d/BadgeK07Color</Color>
<Grey>Badges01.unity3d/BadgeK07Grey</Grey>
<PieceDialog>
<FileName>DlgFrankieB1Piece</FileName>
<NPC>NPCs.unity3d/PfFrankie</NPC>
<Bundle>RS_DATA/Badges01.unity3d</Bundle>
</PieceDialog>
<CompleteDialog>
<FileName>DlgFrankieB1Complete</FileName>
<NPC>NPCs.unity3d/PfFrankie</NPC>
<Bundle>RS_DATA/Badges01.unity3d</Bundle>
</CompleteDialog>
</Badge>
</BadgeData>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1949,6 +1949,11 @@ SoD 3.31 main store section and subsection filtering:
<ii>1597</ii>
<ii>1627</ii>
</StoreData>
<StoreData>
<i>8</i>
<s>FL Avatar Default</s>
<d>The default avatar parts for Adventure Land -- empty</d>
</StoreData>
<StoreData>
<i>9</i>
<s>Main Street_Threadz</s>

152
src/Resources/worlds.xml Normal file
View File

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="utf-8"?>
<ArrayOfWorld xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<World>
<Scene>TrekAncient01</Scene>
<ID>1</ID>
</World>
<World>
<Scene>TrekAncient01Nav</Scene>
<ID>1</ID>
</World>
<World>
<Scene>TrekAncient01Roller</Scene>
<ID>1</ID>
</World>
<World>
<Scene>Ancient01Zone01</Scene>
<ID>1</ID>
</World>
<World>
<Scene>TrekIndustrial01</Scene>
<ID>2</ID>
</World>
<World>
<Scene>TrekIndustrial01Nav</Scene>
<ID>2</ID>
</World>
<World>
<Scene>TrekIndustrial01Roller</Scene>
<ID>2</ID>
</World>
<World>
<Scene>Industrial01Zone01</Scene>
<ID>2</ID>
</World>
<World>
<Scene>SeaLab</Scene>
<ID>3</ID>
</World>
<World>
<Scene>Aquarium</Scene>
<ID>3</ID>
</World>
<World>
<Scene>LrLetItRide</Scene>
<ID>3</ID>
</World>
<World>
<Scene>TrainingIsland</Scene>
<ID>4</ID>
</World>
<World>
<Scene>HomeBase</Scene>
<ID>4</ID>
</World>
<World>
<Scene>TownCenter</Scene>
<ID>4</ID>
</World>
<World>
<Scene>MountainJetpack</Scene>
<ID>4</ID>
</World>
<World>
<Scene>EnemyValley</Scene>
<ID>4</ID>
</World>
<World>
<Scene>DanceScene</Scene>
<ID>4</ID>
</World>
<World>
<Scene>ArtHouseAL</Scene>
<ID>4</ID>
</World>
<World>
<Scene>AvatarCreatorAL</Scene>
<ID>4</ID>
</World>
<World>
<Scene>KnCalendar</Scene>
<ID>4</ID>
</World>
<World>
<Scene>Ancient01Zone02</Scene>
<ID>5</ID>
</World>
<World>
<Scene>Ancient01Zone03</Scene>
<ID>6</ID>
</World>
<World>
<Scene>Ancient01Zone04</Scene>
<ID>7</ID>
</World>
<World>
<Scene>Ancient01Zone05</Scene>
<ID>8</ID>
</World>
<World>
<Scene>Industrial01Zone02</Scene>
<ID>9</ID>
</World>
<World>
<Scene>Industrial01Zone03</Scene>
<ID>10</ID>
</World>
<World>
<Scene>Industrial01Zone04</Scene>
<ID>11</ID>
</World>
<World>
<Scene>Industrial01Zone05</Scene>
<ID>12</ID>
</World>
<World>
<Scene>MerWreck</Scene>
<ID>14</ID>
</World>
<World>
<Scene>MerRuins</Scene>
<ID>14</ID>
</World>
<World>
<Scene>MerKelp</Scene>
<ID>14</ID>
</World>
<World>
<Scene>MerCaverns</Scene>
<ID>15</ID>
</World>
<World>
<Scene>MerTown</Scene>
<ID>16</ID>
</World>
<World>
<Scene>BubbleTrouble</Scene>
<ID>17</ID>
</World>
<World>
<Scene>PearlPush</Scene>
<ID>18</ID>
</World>
<World>
<Scene>MB Girls</Scene>
<ID>99</ID><!-- sodoff placeholder value -->
</World>
<World>
<Scene>MB Boys</Scene>
<ID>99</ID><!-- sodoff placeholder value -->
</World>
</ArrayOfWorld>

12
src/Schema/BadgeData.cs Normal file
View File

@ -0,0 +1,12 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "BadgeData", Namespace = "", IsNullable = true)]
[Serializable]
public class BadgeData
{
[XmlElement(ElementName = "Badge")]
public BadgeDataBadge[] Badge;
}
}

View File

@ -0,0 +1,39 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "BadgeDataBadge", Namespace = "")]
[Serializable]
public class BadgeDataBadge
{
[XmlElement(ElementName = "BadgeId")]
public int BadgeId;
[XmlElement(ElementName = "Name")]
public string Name;
[XmlElement(ElementName = "Description")]
public string Description;
[XmlElement(ElementName = "Experience")]
public int Experience;
[XmlElement(ElementName = "Pieces")]
public int Pieces;
[XmlElement(ElementName = "Mask")]
public string Mask;
[XmlElement(ElementName = "Color")]
public string Color;
[XmlElement(ElementName = "Grey")]
public string Grey;
[XmlElement(ElementName = "PieceDialog", IsNullable = true)]
public BadgeDataBadgePieceDialog PieceDialog;
[XmlElement(ElementName = "CompleteDialog", IsNullable = true)]
public BadgeDataBadgeCompleteDialog CompleteDialog;
}
}

View File

@ -0,0 +1,18 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "BadgeDataBadgeCompleteDialog", Namespace = "")]
[Serializable]
public class BadgeDataBadgeCompleteDialog
{
[XmlElement(ElementName = "FileName")]
public string FileName;
[XmlElement(ElementName = "NPC")]
public string NPC;
[XmlElement(ElementName = "Bundle")]
public string Bundle;
}
}

View File

@ -0,0 +1,18 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "BadgeDataBadgePieceDialog", Namespace = "")]
[Serializable]
public class BadgeDataBadgePieceDialog
{
[XmlElement(ElementName = "FileName")]
public string FileName;
[XmlElement(ElementName = "NPC")]
public string NPC;
[XmlElement(ElementName = "Bundle")]
public string Bundle;
}
}

12
src/Schema/MissionData.cs Normal file
View File

@ -0,0 +1,12 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "MissionData", Namespace = "", IsNullable = false)]
[Serializable]
public class MissionData
{
[XmlElement(ElementName = "Mission")]
public MissionDataMission[] Mission;
}
}

View File

@ -0,0 +1,36 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "MissionDataMission", Namespace = "")]
[Serializable]
public class MissionDataMission
{
[XmlElement(ElementName = "MissionID")]
public int MissionID;
[XmlElement(ElementName = "Name")]
public string Name;
[XmlElement(ElementName = "DisplayName", IsNullable = true)]
public string DisplayName;
[XmlElement(ElementName = "IconName", IsNullable = true)]
public string IconName;
[XmlElement(ElementName = "Description", IsNullable = true)]
public string Description;
[XmlElement(ElementName = "Experience")]
public int Experience;
[XmlElement(ElementName = "RewardDialog", IsNullable = true)]
public MissionDataMissionRewardDialog RewardDialog;
[XmlElement(ElementName = "UnlockMission")]
public int[] UnlockMission;
[XmlElement(ElementName = "Step", IsNullable = true)]
public MissionDataMissionStep[] Step;
}
}

View File

@ -0,0 +1,18 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "MissionDataMissionRewardDialog", Namespace = "")]
[Serializable]
public class MissionDataMissionRewardDialog
{
[XmlElement(ElementName = "FileName")]
public string FileName;
[XmlElement(ElementName = "NPC")]
public string NPC;
[XmlElement(ElementName = "Bundle")]
public string Bundle;
}
}

View File

@ -0,0 +1,15 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "MissionDataMissionStep", Namespace = "")]
[Serializable]
public class MissionDataMissionStep
{
[XmlElement(ElementName = "StepID")]
public int StepID;
[XmlElement(ElementName = "TaskID")]
public int[] TaskID;
}
}

36
src/Schema/Step.cs Normal file
View File

@ -0,0 +1,36 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "Step", Namespace = "", IsNullable = false)]
[Serializable]
public class Step
{
[XmlElement(ElementName = "StepID")]
public StepStepID StepID;
[XmlElement(ElementName = "OfferSpeech", IsNullable = true)]
public StepOfferSpeech OfferSpeech;
[XmlElement(ElementName = "EndSpeech", IsNullable = true)]
public StepEndSpeech EndSpeech;
[XmlElement(ElementName = "TasksNeeded")]
public int TasksNeeded;
[XmlElement(ElementName = "Task")]
public StepTask[] Task;
[XmlElement(ElementName = "Message")]
public StepMessage[] Message;
[XmlElement(ElementName = "NPCData")]
public StepNPCData[] NPCData;
[XmlElement(ElementName = "StoreItem")]
public StepStoreItem[] StoreItem;
[XmlElement(ElementName = "StartPlayerItem")]
public StepStartPlayerItem[] StartPlayerItem;
}
}

View File

@ -0,0 +1,18 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepEndSpeech", Namespace = "")]
[Serializable]
public class StepEndSpeech
{
[XmlElement(ElementName = "FileName")]
public string FileName;
[XmlElement(ElementName = "NPC")]
public string NPC;
[XmlElement(ElementName = "Bundle")]
public string Bundle;
}
}

18
src/Schema/StepMessage.cs Normal file
View File

@ -0,0 +1,18 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepMessage", Namespace = "")]
[Serializable]
public class StepMessage
{
[XmlElement(ElementName = "Text", IsNullable = true)]
public string Text;
[XmlElement(ElementName = "ItemID", IsNullable = true)]
public int? ItemID;
[XmlElement(ElementName = "Scale", IsNullable = true)]
public float? Scale;
}
}

21
src/Schema/StepNPCData.cs Normal file
View File

@ -0,0 +1,21 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepNPCData", Namespace = "")]
[Serializable]
public class StepNPCData
{
[XmlElement(ElementName = "NPC")]
public string NPC;
[XmlElement(ElementName = "Marker")]
public string Marker;
[XmlElement(ElementName = "Scene")]
public string Scene;
[XmlElement(ElementName = "Animation", IsNullable = true)]
public string Animation;
}
}

View File

@ -0,0 +1,18 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepOfferSpeech", Namespace = "")]
[Serializable]
public class StepOfferSpeech
{
[XmlElement(ElementName = "FileName")]
public string FileName;
[XmlElement(ElementName = "NPC")]
public string NPC;
[XmlElement(ElementName = "Bundle")]
public string Bundle;
}
}

View File

@ -0,0 +1,15 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepStartPlayerItem", Namespace = "")]
[Serializable]
public class StepStartPlayerItem
{
[XmlElement(ElementName = "ItemID")]
public int ItemID;
[XmlElement(ElementName = "Quantity")]
public int Quantity;
}
}

12
src/Schema/StepStepID.cs Normal file
View File

@ -0,0 +1,12 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepStepID", Namespace = "")]
[Serializable]
public class StepStepID
{
[XmlText]
public int Value;
}
}

View File

@ -0,0 +1,15 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepStoreItem", Namespace = "")]
[Serializable]
public class StepStoreItem
{
[XmlElement(ElementName = "StoreID")]
public int StoreID;
[XmlElement(ElementName = "ItemID")]
public int ItemID;
}
}

42
src/Schema/StepTask.cs Normal file
View File

@ -0,0 +1,42 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepTask", Namespace = "")]
[Serializable]
public class StepTask
{
[XmlElement(ElementName = "TaskID")]
public int TaskID;
[XmlElement(ElementName = "Type")]
public string Type;
[XmlElement(ElementName = "Dialog", IsNullable = true)]
public StepTaskDialog Dialog;
[XmlElement(ElementName = "Message")]
public StepTaskMessage[] Message;
[XmlElement(ElementName = "SetupGroup", IsNullable = true)]
public string SetupGroup;
[XmlElement(ElementName = "SetupScene", IsNullable = true)]
public string SetupScene;
[XmlElement(ElementName = "Help")]
public StepTaskHelp[] Help;
[XmlElement(ElementName = "RewardPlayerItem")]
public StepTaskRewardPlayerItem[] RewardPlayerItem;
[XmlElement(ElementName = "Experience")]
public int Experience;
[XmlElement(ElementName = "Time", IsNullable = true)]
public int? Time;
[XmlElement(ElementName = "Objective")]
public StepTaskObjective Objective;
}
}

View File

@ -0,0 +1,18 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepTaskDialog", Namespace = "")]
[Serializable]
public class StepTaskDialog
{
[XmlElement(ElementName = "FileName")]
public string FileName;
[XmlElement(ElementName = "NPC")]
public string NPC;
[XmlElement(ElementName = "Bundle")]
public string Bundle;
}
}

View File

@ -0,0 +1,18 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepTaskHelp", Namespace = "")]
[Serializable]
public class StepTaskHelp
{
[XmlElement(ElementName = "FileName")]
public string FileName;
[XmlElement(ElementName = "NPC")]
public string NPC;
[XmlElement(ElementName = "Bundle")]
public string Bundle;
}
}

View File

@ -0,0 +1,18 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepTaskMessage", Namespace = "")]
[Serializable]
public class StepTaskMessage
{
[XmlElement(ElementName = "Text", IsNullable = true)]
public string Text;
[XmlElement(ElementName = "ItemID", IsNullable = true)]
public int? ItemID;
[XmlElement(ElementName = "Scale", IsNullable = true)]
public float? Scale;
}
}

View File

@ -0,0 +1,57 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepTaskObjective", Namespace = "")]
[Serializable]
public class StepTaskObjective
{
[XmlElement(ElementName = "Beacon", IsNullable = true)]
public bool? Beacon;
[XmlElement(ElementName = "NPC", IsNullable = true)]
public string NPC;
[XmlElement(ElementName = "Marker", IsNullable = true)]
public string Marker;
[XmlElement(ElementName = "Scene", IsNullable = true)]
public string Scene;
[XmlElement(ElementName = "Range", IsNullable = true)]
public float? Range;
[XmlElement(ElementName = "Module", IsNullable = true)]
public string Module;
[XmlElement(ElementName = "Group", IsNullable = true)]
public string Group;
[XmlElement(ElementName = "Object", IsNullable = true)]
public string Object;
[XmlElement(ElementName = "StoreID", IsNullable = true)]
public int? StoreID;
[XmlElement(ElementName = "ItemID", IsNullable = true)]
public int? ItemID;
[XmlElement(ElementName = "ItemName", IsNullable = true)]
public string ItemName;
[XmlElement(ElementName = "CategoryID", IsNullable = true)]
public int? CategoryID;
[XmlElement(ElementName = "AttributeID")]
public int[] AttributeID;
[XmlElement(ElementName = "Quantity", IsNullable = true)]
public int? Quantity;
[XmlElement(ElementName = "Photo", IsNullable = true)]
public StepTaskObjectivePhoto Photo;
[XmlElement(ElementName = "Creative", IsNullable = true)]
public StepTaskObjectiveCreative Creative;
}
}

View File

@ -0,0 +1,15 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepTaskObjectiveCreative", Namespace = "")]
[Serializable]
public class StepTaskObjectiveCreative
{
[XmlElement(ElementName = "Type")]
public int Type;
[XmlElement(ElementName = "AttributeID")]
public int[] AttributeID;
}
}

View File

@ -0,0 +1,24 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepTaskObjectivePhoto", Namespace = "")]
[Serializable]
public class StepTaskObjectivePhoto
{
[XmlElement(ElementName = "ItemName")]
public string[] ItemName;
[XmlElement(ElementName = "NPC")]
public string[] NPC;
[XmlElement(ElementName = "CategoryID")]
public int[] CategoryID;
[XmlElement(ElementName = "AttributeID")]
public int[] AttributeID;
[XmlElement(ElementName = "Quantity")]
public int Quantity;
}
}

View File

@ -0,0 +1,15 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "StepTaskRewardPlayerItem", Namespace = "")]
[Serializable]
public class StepTaskRewardPlayerItem
{
[XmlElement(ElementName = "ItemID")]
public int ItemID;
[XmlElement(ElementName = "Quantity")]
public int Quantity;
}
}

View File

@ -0,0 +1,16 @@
using System.Xml.Serialization;
namespace sodoff.Schema;
[XmlRoot(ElementName = "StepsMissionsGroup", Namespace = "")]
[Serializable]
public class StepsMissionsGroup {
[XmlElement(ElementName = "GameId")]
public int GameId;
[XmlElement(ElementName = "WorldName")]
public string WorldName;
[XmlElement(ElementName = "MissionData")]
public MissionData MissionData;
}

12
src/Schema/UserBadge.cs Normal file
View File

@ -0,0 +1,12 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "UserBadge", Namespace = "", IsNullable = false)]
[Serializable]
public class UserBadge
{
[XmlElement(ElementName = "BadgeId")]
public int[] BadgeId;
}
}

View File

@ -0,0 +1,15 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "UserMissionData", Namespace = "")]
[Serializable]
public class UserMissionData
{
[XmlElement(ElementName = "Mission")]
public UserMissionDataMission[] Mission;
[XmlElement(ElementName = "MissionComplete")]
public int[] MissionComplete;
}
}

View File

@ -0,0 +1,15 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "UserMissionDataMission", Namespace = "")]
[Serializable]
public class UserMissionDataMission
{
[XmlElement(ElementName = "MissionId")]
public int MissionId;
[XmlElement(ElementName = "Step")]
public UserMissionDataMissionStep[] Step;
}
}

View File

@ -0,0 +1,15 @@
using System.Xml.Serialization;
namespace sodoff.Schema
{
[XmlRoot(ElementName = "UserMissionDataMissionStep", Namespace = "")]
[Serializable]
public class UserMissionDataMissionStep
{
[XmlElement(ElementName = "StepId")]
public int StepId;
[XmlElement(ElementName = "TaskId")]
public int[] TaskId;
}
}

12
src/Schema/World.cs Normal file
View File

@ -0,0 +1,12 @@
using System.Xml.Serialization;
[XmlRoot(ElementName = "World", Namespace = "")]
[Serializable]
public class World
{
[XmlElement(ElementName = "Scene")]
public string Scene;
[XmlElement(ElementName = "ID")]
public int ID;
}

View File

@ -1,10 +1,12 @@
using sodoff.Schema;
using sodoff.Model;
using sodoff.Util;
using sodoff.Configuration;
using System.Reflection;
using System.Xml;
using System.Xml.Linq;
using System.Collections.ObjectModel;
using Microsoft.Extensions.Options;
namespace sodoff.Services {
public class AchievementStoreSingleton {
@ -34,7 +36,7 @@ namespace sodoff.Services {
int dragonAdultMinXP;
int dragonTitanMinXP;
public AchievementStoreSingleton() {
public AchievementStoreSingleton(IOptions<ApiServerConfig> config) {
ArrayOfUserRank allranks = XmlUtil.DeserializeXml<ArrayOfUserRank>(XmlUtil.ReadResourceXmlString("ranks.allranks_sod"));
foreach (var pointType in Enum.GetValues<AchievementPointTypes>()) {
ranks[pointType] = allranks.UserRank.Where(r => r.PointTypeID == pointType).ToArray();
@ -46,11 +48,13 @@ namespace sodoff.Services {
}
achievementsTasks[ClientVersion.Min_SoD] = new AchievementTasks("achievements.achievementtaskinfo_sod");
if (config.Value.LoadNonSoDData) {
achievementsTasks[ClientVersion.MaM] = new AchievementTasks("achievements.achievementtaskinfo_mam");
achievementsTasks[ClientVersion.MB] = new AchievementTasks("achievements.achievementtaskinfo_mb");
achievementsTasks[ClientVersion.EMD] = new AchievementTasks("achievements.achievementtaskinfo_emd");
achievementsTasks[ClientVersion.SS] = new AchievementTasks("achievements.achievementtaskinfo_ss");
achievementsTasks[ClientVersion.WoJS] = new AchievementTasks("achievements.achievementtaskinfo_wojs");
}
dragonAdultMinXP = ranks[AchievementPointTypes.DragonXP][10].Value;
dragonTitanMinXP = ranks[AchievementPointTypes.DragonXP][20].Value;

View File

@ -54,6 +54,109 @@ public class MissionService {
return mission;
}
public Schema.UserMissionData GetUserMissionData(Viking viking, int worldId) {
Schema.UserMissionData umdRes = new();
// instantiate schema lists and int lists
List<int> userMissionsCompletedIds = new();
List<UserMissionDataMission> missions = new();
// get all initiated missions
List<Model.UserMissionData> vikingUmds = viking.UserMissions.Where(e => e.WorldId == worldId).ToList();
foreach (Model.UserMissionData mission in vikingUmds) {
missions.Add(new UserMissionDataMission {
MissionId = mission.MissionId,
Step = new UserMissionDataMissionStep[] { new UserMissionDataMissionStep {
// NOTE: we store in database only last StepId and TaskId this is different behavior than og
StepId = mission.StepId,
TaskId = new int[] {mission.TaskId}
}}
});
}
// add completed mission id's to usermissionscompletedids
List<Model.UserMissionData> vikingCompletedUmds = vikingUmds.Where(e => e.IsCompleted == true).ToList();
foreach (Model.UserMissionData mission in vikingCompletedUmds)
{
userMissionsCompletedIds.Add(mission.MissionId);
}
// construct response
umdRes.Mission = missions.ToArray();
umdRes.MissionComplete = userMissionsCompletedIds.ToArray();
// return
return umdRes;
}
public UserBadge GetUserBadgesCompleted(Viking viking)
{
// get badges
List<UserBadgeCompleteData> userBadgesCompleted = viking.UserBadgesCompleted.ToList();
List<int> completedBadgeIds = new List<int>();
foreach (var userBadge in userBadgesCompleted)
{
completedBadgeIds.Add(userBadge.BadgeId);
}
return new UserBadge { BadgeId = completedBadgeIds.ToArray() };
}
public void SetOrUpdateUserMissionData(Viking viking, int worldId, int missionId, int stepId, int taskId) {
// find any existing records of this mission
Model.UserMissionData? missionData = viking.UserMissions.Where(e => e.WorldId == worldId)
.Where(e => e.MissionId == missionId)
.FirstOrDefault();
if (missionData != null) {
missionData.StepId = stepId;
missionData.TaskId = taskId;
} else {
viking.UserMissions.Add(new Model.UserMissionData() {
WorldId = worldId,
MissionId = missionId,
StepId = stepId,
TaskId = taskId
});
}
ctx.SaveChanges();
}
public bool SetUserMissionCompleted(Viking viking, int worldId, int missionId, bool isCompleted)
{
Model.UserMissionData? mission = viking.UserMissions.Where(e => e.WorldId == worldId)
.Where(e => e.MissionId == missionId)
.FirstOrDefault();
if (mission != null)
{
// set mission complete
mission.IsCompleted = isCompleted;
// add jumpstars for completing the mission
if (isCompleted) achievementService.AddAchievementPoints(viking, AchievementPointTypes.PlayerXP, 25); // hardcoding earning 25 for now
ctx.SaveChanges();
return true;
}
return false;
}
public bool SetUserBadgeComplete(Viking viking, int gameId)
{
// add completed badge to database
UserBadgeCompleteData userBadgeCompleteData = new() { BadgeId = gameId };
viking.UserBadgesCompleted.Add(userBadgeCompleteData);
ctx.SaveChanges();
return true;
}
public List<MissionCompletedResult> UpdateTaskProgress(int missionId, int taskId, int userId, bool completed, string xmlPayload, uint gameVersion) {
SetTaskProgressDB(missionId, taskId, userId, completed, xmlPayload);

View File

@ -1,11 +1,15 @@
using sodoff.Schema;
using sodoff.Util;
using sodoff.Configuration;
using Microsoft.Extensions.Options;
using System.Runtime.Serialization.Formatters.Binary;
namespace sodoff.Services;
public class MissionStoreSingleton {
private Dictionary<int, Mission> missions = new();
private Dictionary<(int, string), MissionData> stepsMissions = new();
private Dictionary<int, Step> steps = new();
private int[] activeMissions;
private int[] upcomingMissions;
private int[] activeMissionsV1;
@ -15,7 +19,7 @@ public class MissionStoreSingleton {
private int[] activeMissionsWoJS;
private int[] upcomingMissionsWoJS;
public MissionStoreSingleton() {
public MissionStoreSingleton(IOptions<ApiServerConfig> config) {
ServerMissionArray missionArray = XmlUtil.DeserializeXml<ServerMissionArray>(XmlUtil.ReadResourceXmlString("missions.missions_sod"));
DefaultMissions defaultMissions = XmlUtil.DeserializeXml<DefaultMissions>(XmlUtil.ReadResourceXmlString("missions.defaultmissionlist"));
foreach (var mission in missionArray.MissionDataArray) {
@ -28,6 +32,7 @@ public class MissionStoreSingleton {
activeMissionsV1 = defaultMissions.Active;
upcomingMissionsV1 = defaultMissions.Upcoming;
if (config.Value.LoadNonSoDData) {
missionArray = XmlUtil.DeserializeXml<ServerMissionArray>(XmlUtil.ReadResourceXmlString("missions.missions_mam"));
defaultMissions = XmlUtil.DeserializeXml<DefaultMissions>(XmlUtil.ReadResourceXmlString("missions.defaultmissionlist_mam"));
foreach (var mission in missionArray.MissionDataArray) {
@ -43,6 +48,17 @@ public class MissionStoreSingleton {
}
activeMissionsWoJS = defaultMissions.Active;
upcomingMissionsWoJS = defaultMissions.Upcoming;
var stepsMissionsArray = XmlUtil.DeserializeXml<StepsMissionsGroup[]>(XmlUtil.ReadResourceXmlString("missions.step_missions"));
foreach (var missionGroup in stepsMissionsArray) {
stepsMissions.Add((missionGroup.GameId, missionGroup.WorldName), missionGroup.MissionData);
}
var stepArray = XmlUtil.DeserializeXml<Step[]>(XmlUtil.ReadResourceXmlString("missions.step_missions_steps"));
foreach (var step in stepArray) {
steps.Add(step.StepID.Value, step);
}
}
}
public Mission GetMission(int missionID) {
@ -81,6 +97,21 @@ public class MissionStoreSingleton {
return new int[0];
}
public MissionData GetStepsMissions(int gameId, string worldName) {
if (stepsMissions.ContainsKey((gameId, worldName))) {
return stepsMissions[(gameId, worldName)];
} else if (stepsMissions.ContainsKey((gameId, "_default_"))) {
return stepsMissions[(gameId, "_default_")];
} else {
Console.WriteLine($"Can't find missions for gameId={gameId} worldName={worldName}");
return new MissionData();
}
}
public Step GetStep(int stepID) {
return steps[stepID];
}
private void SetUpRecursive(Mission mission) {
missions.Add(mission.MissionID, mission);
foreach (var innerMission in mission.Missions) {

View File

@ -19,12 +19,14 @@ public class StoreService {
List<ItemData> itemsList = new();
IEnumerable<ItemsInStoreDataSale>? memberSales = s.SalesAtStore?.Where(x => x.ForMembers == true);
IEnumerable<ItemsInStoreDataSale>? normalSales = s.SalesAtStore?.Where(x => x.ForMembers == false || x.ForMembers == null);
if (s.ItemId != null) {
for (int i = 0; i < s.ItemId.Length; ++i) {
ItemData item = itemService.GetItem(s.ItemId[i]);
if (item is null) continue; // skip removed items
itemsList.Add(item);
UpdateItemSaleModifier(item, memberSales, normalSales);
}
}
foreach (int itemID in moddingService.GetStoreItem(s.Id)) {
ItemData item = itemService.GetItem(itemID);
itemsList.Add(item);

View File

@ -0,0 +1,25 @@
using sodoff.Schema;
using sodoff.Util;
namespace sodoff.Services;
public class WorldIdService {
Dictionary<string, int> worlds_id = new();
public WorldIdService()
{
var worlds = XmlUtil.DeserializeXml<World[]>(XmlUtil.ReadResourceXmlString("worlds"));
foreach (var w in worlds)
{
worlds_id[w.Scene] = w.ID;
}
}
public int GetWorldID(string mapName)
{
if (worlds_id.ContainsKey(mapName))
return worlds_id[mapName];
else
return 0;
}
}

View File

@ -8,6 +8,9 @@ public class ClientVersion {
public const uint SS = 0x02000000;
public const uint WoJS = 0x01000000;
public const uint WoJS_AdvLand = 0x01000100; // World of JumpStart -- Adventureland
public const uint WoJS_FutureLand = 0x01000200; // World of JumpStart -- Futureland
public const uint WoJS_MarineLand = 0x01000300; // World of JumpStart -- Marineland
public const uint WoJS_StoryLand = 0x01000400; // World of JumpStart -- Storyland
public const uint WoJS_NewAvatar = 0x01010000; // World of JumpStart with new avatars (e.g. 1.21)
public static uint GetVersion(string apiKey) {
@ -49,6 +52,18 @@ public class ClientVersion {
apiKey == "b4e0f71a-1cda-462a-97b3-0b355e87e0c8"
) {
return WoJS_AdvLand;
} else if (
apiKey == "4fb5e29f-64e7-4cbb-8554-6f6c54b57597"
) {
return WoJS_FutureLand;
} else if (
apiKey == "dc37ef0d-e1f8-4718-8239-73e68424e384"
) {
return WoJS_MarineLand;
} else if (
apiKey == "bd69b6b9-a921-4741-a2a0-92fc40cc2e58"
) {
return WoJS_StoryLand;
}
Console.WriteLine($"Unknown apiKey value: {apiKey}");
return 0;

View File

@ -57,6 +57,9 @@
"// MMOSupportMinVersion": "Minimum client version allowed to use MMO. For example: '0xa3a31a0a' mean SoD 3.31, '0xa0000000' mean all SoD version, 0 mean all games.",
"MMOSupportMinVersion": "0",
"// LoadNonSoDData": "set to 'true' to support non SoD games, set to 'false' to reduce memory usage",
"LoadNonSoDData": true,
"// DbProvider": "Select database backend to use: SQLite, PostgreSQL, MySQL (availability may depend on build options)",
"DbProvider": "SQLite",

View File

@ -142,5 +142,17 @@
<EmbeddedResource Include="Resources\defaulthouse.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\worlds.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\missions\step_missions_steps.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\missions\step_missions.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\missions\badge_wojs_al.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
</Project>