Forgot unversioned xd
This commit is contained in:
147
SVSim.EmulatedEntrypoint/Controllers/AdminController.cs
Normal file
147
SVSim.EmulatedEntrypoint/Controllers/AdminController.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Repositories.Viewer;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Admin;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Admin;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Util endpoints for bootstrapping the dev environment. Anonymous-allowed today — security
|
||||
/// audit pending (don't expose these to the public internet).
|
||||
/// </summary>
|
||||
public class AdminController : SVSimController
|
||||
{
|
||||
private readonly IViewerRepository _viewerRepository;
|
||||
private readonly SVSimDbContext _dbContext;
|
||||
|
||||
public AdminController(IViewerRepository viewerRepository, SVSimDbContext dbContext)
|
||||
{
|
||||
_viewerRepository = viewerRepository;
|
||||
_dbContext = dbContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upsert a viewer from external data (typically captured from the live game via the
|
||||
/// SVSimLoader dump). Matches existing viewers by SteamId; creates a new one if missing.
|
||||
/// Only essential fields are imported today — extend as needed.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("import_viewer")]
|
||||
public async Task<ActionResult<ImportViewerResponse>> ImportViewer(ImportViewerRequest request)
|
||||
{
|
||||
if (request.SteamId == 0)
|
||||
{
|
||||
return BadRequest("steam_id is required");
|
||||
}
|
||||
|
||||
// SocialAccountConnection is [Owned]-by-Viewer — can't query the owned table directly;
|
||||
// look up the Viewer with a matching owned connection instead.
|
||||
var existing = await _dbContext.Viewers
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(v => v.SocialAccountConnections.Any(sac =>
|
||||
sac.AccountType == SocialAccountType.Steam && sac.AccountId == request.SteamId));
|
||||
|
||||
long viewerId;
|
||||
bool wasCreated;
|
||||
|
||||
if (existing is null)
|
||||
{
|
||||
var created = await _viewerRepository.RegisterViewer(
|
||||
request.DisplayName ?? "Imported Viewer",
|
||||
SocialAccountType.Steam,
|
||||
request.SteamId);
|
||||
viewerId = created.Id;
|
||||
wasCreated = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
viewerId = existing.Id;
|
||||
wasCreated = false;
|
||||
}
|
||||
|
||||
// Reload with all the nav properties we need to mutate. RegisterViewer SaveChanges'd
|
||||
// already, so we re-fetch with full graph and apply the updates.
|
||||
var viewer = await _dbContext.Viewers
|
||||
.Include(v => v.Info).ThenInclude(i => i.SelectedEmblem)
|
||||
.Include(v => v.Info).ThenInclude(i => i.SelectedDegree)
|
||||
.Include(v => v.Currency)
|
||||
.Include(v => v.MissionData)
|
||||
.Include(v => v.Classes).ThenInclude(c => c.Class)
|
||||
.Include(v => v.Sleeves)
|
||||
.Include(v => v.Emblems)
|
||||
.Include(v => v.Degrees)
|
||||
.Include(v => v.LeaderSkins)
|
||||
.Include(v => v.MyPageBackgrounds)
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
|
||||
if (request.DisplayName is not null) viewer.DisplayName = request.DisplayName;
|
||||
if (request.CountryCode is not null) viewer.Info.CountryCode = request.CountryCode;
|
||||
if (request.TutorialState.HasValue) viewer.MissionData.TutorialState = request.TutorialState.Value;
|
||||
|
||||
if (request.Currency is not null)
|
||||
{
|
||||
if (request.Currency.Crystals.HasValue) viewer.Currency.Crystals = request.Currency.Crystals.Value;
|
||||
if (request.Currency.Rupees.HasValue) viewer.Currency.Rupees = request.Currency.Rupees.Value;
|
||||
if (request.Currency.RedEther.HasValue) viewer.Currency.RedEther = request.Currency.RedEther.Value;
|
||||
}
|
||||
|
||||
if (request.SelectedEmblemId.HasValue)
|
||||
{
|
||||
var emblem = await _dbContext.Emblems.FindAsync(request.SelectedEmblemId.Value);
|
||||
if (emblem is not null) viewer.Info.SelectedEmblem = emblem;
|
||||
}
|
||||
if (request.SelectedDegreeId.HasValue)
|
||||
{
|
||||
var degree = await _dbContext.Degrees.FindAsync(request.SelectedDegreeId.Value);
|
||||
if (degree is not null) viewer.Info.SelectedDegree = degree;
|
||||
}
|
||||
|
||||
await ReplaceOwned(viewer.Sleeves, request.OwnedSleeveIds, _dbContext.Sleeves);
|
||||
await ReplaceOwned(viewer.Emblems, request.OwnedEmblemIds, _dbContext.Emblems);
|
||||
await ReplaceOwned(viewer.Degrees, request.OwnedDegreeIds, _dbContext.Degrees);
|
||||
await ReplaceOwned(viewer.LeaderSkins, request.OwnedLeaderSkinIds, _dbContext.LeaderSkins);
|
||||
await ReplaceOwned(viewer.MyPageBackgrounds, request.OwnedMyPageBackgroundIds, _dbContext.MyPageBackgrounds);
|
||||
|
||||
if (request.Classes is not null)
|
||||
{
|
||||
foreach (var importClass in request.Classes)
|
||||
{
|
||||
var existingClass = viewer.Classes.FirstOrDefault(c => c.Class.Id == importClass.ClassId);
|
||||
if (existingClass is not null)
|
||||
{
|
||||
existingClass.Level = importClass.Level;
|
||||
existingClass.Exp = importClass.Exp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
return new ImportViewerResponse
|
||||
{
|
||||
ViewerId = viewer.Id,
|
||||
ShortUdid = viewer.ShortUdid,
|
||||
WasCreated = wasCreated
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the owned-collection with the master rows matching the supplied ids.
|
||||
/// Null `ids` is a no-op (preserve existing). Empty list clears the collection.
|
||||
/// </summary>
|
||||
private async Task ReplaceOwned<TEntity>(List<TEntity> owned, List<int>? ids, DbSet<TEntity> table)
|
||||
where TEntity : class
|
||||
{
|
||||
if (ids is null) return;
|
||||
owned.Clear();
|
||||
if (ids.Count == 0) return;
|
||||
|
||||
var rows = await table.Where(e => ids.Contains(EF.Property<int>(e, "Id"))).ToListAsync();
|
||||
owned.AddRange(rows);
|
||||
}
|
||||
}
|
||||
213
SVSim.EmulatedEntrypoint/Controllers/DeckController.cs
Normal file
213
SVSim.EmulatedEntrypoint/Controllers/DeckController.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Repositories.Deck;
|
||||
using SVSim.EmulatedEntrypoint.Constants;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Common;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Common;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Deck;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Deck;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||
|
||||
public class DeckController : SVSimController
|
||||
{
|
||||
private readonly IDeckRepository _deckRepository;
|
||||
private readonly SVSimDbContext _dbContext;
|
||||
|
||||
public DeckController(IDeckRepository deckRepository, SVSimDbContext dbContext)
|
||||
{
|
||||
_deckRepository = deckRepository;
|
||||
_dbContext = dbContext;
|
||||
}
|
||||
|
||||
// TODO: API-side deck_format values may not match the Format enum (see audit on /load/index
|
||||
// open question — `Data.FormatConvertApi` reverse-mapping not yet captured). For now we cast
|
||||
// directly; verify against live traffic and add a conversion table if mismatched.
|
||||
private static Format AsFormat(int apiValue) => (Format)apiValue;
|
||||
|
||||
[HttpPost("info")]
|
||||
public async Task<ActionResult<DeckListResponse>> Info(DeckInfoRequest request)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
var decks = await _deckRepository.GetDecks(viewerId, AsFormat(request.DeckFormat));
|
||||
return new DeckListResponse
|
||||
{
|
||||
UserDeckList = decks.Select(d => new UserDeck(d)).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("my_list")]
|
||||
public async Task<ActionResult<DeckListResponse>> MyList(DeckFormatRequest request)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
var decks = await _deckRepository.GetDecks(viewerId, AsFormat(request.DeckFormat));
|
||||
return new DeckListResponse
|
||||
{
|
||||
UserDeckList = decks.Select(d => new UserDeck(d)).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("get_empty_deck_number")]
|
||||
public async Task<ActionResult<EmptyDeckNumberResponse>> GetEmptyDeckNumber(DeckFormatRequest request)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
return new EmptyDeckNumberResponse
|
||||
{
|
||||
EmptyDeckNum = await _deckRepository.GetEmptyDeckNumber(viewerId, AsFormat(request.DeckFormat))
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult<DeckUpdateResponse>> Update(DeckUpdateRequest request)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
var format = AsFormat(request.DeckFormat);
|
||||
|
||||
if (request.IsDelete == 1)
|
||||
{
|
||||
await _deckRepository.DeleteDecks(viewerId, format, new[] { request.DeckNo });
|
||||
}
|
||||
else
|
||||
{
|
||||
var cls = await _dbContext.Classes.FindAsync(request.ClassId);
|
||||
var sleeve = await _dbContext.Sleeves.FindAsync((int)request.SleeveId);
|
||||
var skin = await _dbContext.LeaderSkins.FindAsync(request.LeaderSkinId);
|
||||
var cards = await ResolveDeckCards(request.CardIdArray);
|
||||
|
||||
await _deckRepository.UpsertDeck(viewerId, format, request.DeckNo, deck =>
|
||||
{
|
||||
deck.Name = request.DeckName ?? string.Empty;
|
||||
if (cls is not null) deck.Class = cls;
|
||||
if (sleeve is not null) deck.Sleeve = sleeve;
|
||||
if (skin is not null) deck.LeaderSkin = skin;
|
||||
deck.RandomLeaderSkin = request.IsRandomLeaderSkin;
|
||||
deck.Cards = cards;
|
||||
});
|
||||
}
|
||||
|
||||
var decks = await _deckRepository.GetDecks(viewerId, format);
|
||||
return new DeckUpdateResponse
|
||||
{
|
||||
UserDeckList = decks.Select(d => new UserDeck(d)).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("update_name")]
|
||||
public async Task<ActionResult<SingleDeckResponse>> UpdateName(DeckUpdateNameRequest request)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
var deck = await _deckRepository.UpsertDeck(viewerId, AsFormat(request.DeckFormat), request.DeckNo,
|
||||
d => d.Name = request.DeckName ?? string.Empty);
|
||||
return new SingleDeckResponse { UserDeck = new UserDeck(deck) };
|
||||
}
|
||||
|
||||
[HttpPost("update_sleeve")]
|
||||
public async Task<ActionResult<SingleDeckResponse>> UpdateSleeve(DeckUpdateSleeveRequest request)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
var sleeve = await _dbContext.Sleeves.FindAsync((int)request.SleeveId);
|
||||
if (sleeve is null) return BadRequest($"Unknown sleeve {request.SleeveId}");
|
||||
|
||||
var deck = await _deckRepository.UpsertDeck(viewerId, AsFormat(request.DeckFormat), request.DeckNo,
|
||||
d => d.Sleeve = sleeve);
|
||||
return new SingleDeckResponse { UserDeck = new UserDeck(deck) };
|
||||
}
|
||||
|
||||
[HttpPost("update_leader_skin")]
|
||||
public async Task<ActionResult<SingleDeckResponse>> UpdateLeaderSkin(DeckUpdateLeaderSkinRequest request)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
var skin = await _dbContext.LeaderSkins.FindAsync(request.LeaderSkinId);
|
||||
if (skin is null) return BadRequest($"Unknown leader skin {request.LeaderSkinId}");
|
||||
|
||||
var deck = await _deckRepository.UpsertDeck(viewerId, AsFormat(request.DeckFormat), request.DeckNo,
|
||||
d =>
|
||||
{
|
||||
d.LeaderSkin = skin;
|
||||
d.RandomLeaderSkin = false;
|
||||
});
|
||||
return new SingleDeckResponse { UserDeck = new UserDeck(deck) };
|
||||
}
|
||||
|
||||
// TODO: schema doesn't yet model the random-leader-skin pool — we just pick one and persist
|
||||
// that. Add a join table (DeckLeaderSkinPool) when ranked play / random skins become a real
|
||||
// feature. For now the UI flow still works (server returns a single chosen skin per spec).
|
||||
[HttpPost("update_random_leader_skin")]
|
||||
public async Task<ActionResult<SingleDeckResponse>> UpdateRandomLeaderSkin(DeckUpdateRandomLeaderSkinRequest request)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
var pool = request.LeaderSkinIdList ?? new List<int>();
|
||||
if (pool.Count == 0) return BadRequest("leader_skin_id_list must contain at least one id");
|
||||
|
||||
int chosenId = pool[Random.Shared.Next(pool.Count)];
|
||||
var skin = await _dbContext.LeaderSkins.FindAsync(chosenId);
|
||||
if (skin is null) return BadRequest($"Unknown leader skin {chosenId}");
|
||||
|
||||
var deck = await _deckRepository.UpsertDeck(viewerId, AsFormat(request.DeckFormat), request.DeckNo,
|
||||
d =>
|
||||
{
|
||||
d.LeaderSkin = skin;
|
||||
d.RandomLeaderSkin = true;
|
||||
});
|
||||
return new SingleDeckResponse { UserDeck = new UserDeck(deck) };
|
||||
}
|
||||
|
||||
[HttpPost("update_order")]
|
||||
public async Task<ActionResult<EmptyResponse>> UpdateOrder(DeckOrderRequest request)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
// Deck `Number` IS the slot order — the client sends the same slot numbers in a new
|
||||
// sequence. Today we don't model "display order" separately from "slot number", so
|
||||
// reordering is a no-op server-side. When a separate Order column lands, persist here.
|
||||
return new EmptyResponse();
|
||||
}
|
||||
|
||||
[HttpPost("delete_deck_list")]
|
||||
public async Task<ActionResult<EmptyResponse>> DeleteDeckList(DeckDeleteListRequest request)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
var nos = request.DeckNoList ?? new List<int>();
|
||||
if (nos.Count > 0) await _deckRepository.DeleteDecks(viewerId, AsFormat(request.DeckFormat), nos);
|
||||
return new EmptyResponse();
|
||||
}
|
||||
|
||||
// /deck/set_deck_redis — server side is a Redis-cached "active deck per class" hint for
|
||||
// matchmaking. We don't model matchmaking yet; acknowledge the call and move on (real
|
||||
// server may not persist this either; the `_redis` suffix suggests cache-only).
|
||||
[HttpPost("set_deck_redis")]
|
||||
public Task<ActionResult<EmptyResponse>> SetDeckRedis(SetDeckRedisRequest request)
|
||||
{
|
||||
if (!TryGetViewerId(out long _)) return Task.FromResult<ActionResult<EmptyResponse>>(Unauthorized());
|
||||
return Task.FromResult<ActionResult<EmptyResponse>>(new EmptyResponse());
|
||||
}
|
||||
|
||||
private bool TryGetViewerId(out long viewerId)
|
||||
{
|
||||
viewerId = 0;
|
||||
var claim = User.Claims.FirstOrDefault(c => c.Type == ShadowverseClaimTypes.ViewerIdClaim)?.Value;
|
||||
return claim is not null && long.TryParse(claim, out viewerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a flat `card_id_array` (cards repeated for count) into a grouped DeckCard list.
|
||||
/// Cards not in the DB are silently dropped — until CardImport lands the result is always
|
||||
/// empty, which is acceptable for the deck-editing flow (UI saves what it can).
|
||||
/// </summary>
|
||||
private async Task<List<DeckCard>> ResolveDeckCards(List<long>? cardIdArray)
|
||||
{
|
||||
if (cardIdArray is null || cardIdArray.Count == 0) return new List<DeckCard>();
|
||||
|
||||
var grouped = cardIdArray.GroupBy(id => id).Select(g => new { Id = g.Key, Count = g.Count() }).ToList();
|
||||
var ids = grouped.Select(g => g.Id).ToList();
|
||||
var cards = await _dbContext.Cards.Where(c => ids.Contains(c.Id)).ToDictionaryAsync(c => c.Id);
|
||||
|
||||
return grouped
|
||||
.Where(g => cards.ContainsKey(g.Id))
|
||||
.Select(g => new DeckCard { Card = cards[g.Id], Count = g.Count })
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
133
SVSim.EmulatedEntrypoint/Controllers/PracticeController.cs
Normal file
133
SVSim.EmulatedEntrypoint/Controllers/PracticeController.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Repositories.Viewer;
|
||||
using SVSim.EmulatedEntrypoint.Constants;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Common;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Practice;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Practice;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||
|
||||
public class PracticeController : SVSimController
|
||||
{
|
||||
// Hand-curated AI opponents (audit B14 pattern). Replace with master data when the
|
||||
// practice subsystem is built out.
|
||||
private static readonly List<PracticeOpponent> StubOpponents = new()
|
||||
{
|
||||
new PracticeOpponent
|
||||
{
|
||||
PracticeId = 1,
|
||||
TextId = "Practice_001",
|
||||
ClassId = 1,
|
||||
CharaId = 1,
|
||||
DegreeId = 0,
|
||||
AiDeckLevel = 1,
|
||||
AiLogicLevel = 1,
|
||||
AiMaxLife = 20,
|
||||
Battle3dFieldId = "1",
|
||||
IsCampaignPractice = false
|
||||
},
|
||||
new PracticeOpponent
|
||||
{
|
||||
PracticeId = 2,
|
||||
TextId = "Practice_002",
|
||||
ClassId = 2,
|
||||
CharaId = 2,
|
||||
DegreeId = 0,
|
||||
AiDeckLevel = 2,
|
||||
AiLogicLevel = 2,
|
||||
AiMaxLife = 20,
|
||||
Battle3dFieldId = "1",
|
||||
IsCampaignPractice = false
|
||||
},
|
||||
new PracticeOpponent
|
||||
{
|
||||
PracticeId = 3,
|
||||
TextId = "Practice_003",
|
||||
ClassId = 3,
|
||||
CharaId = 3,
|
||||
DegreeId = 0,
|
||||
AiDeckLevel = 3,
|
||||
AiLogicLevel = 3,
|
||||
AiMaxLife = 25,
|
||||
Battle3dFieldId = "1",
|
||||
IsCampaignPractice = false
|
||||
}
|
||||
};
|
||||
|
||||
private readonly IViewerRepository _viewerRepository;
|
||||
|
||||
public PracticeController(IViewerRepository viewerRepository)
|
||||
{
|
||||
_viewerRepository = viewerRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// /practice/info — returns the AI opponent catalog. Response data is a JSON array
|
||||
/// directly (not wrapped in an object), per spec.
|
||||
/// </summary>
|
||||
[HttpPost("info")]
|
||||
public Task<List<PracticeOpponent>> Info(BaseRequest request)
|
||||
{
|
||||
return Task.FromResult(StubOpponents);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// /practice/deck_list — returns viewer's decks scoped by format (always Format.All
|
||||
/// per spec, server can ignore the request field).
|
||||
/// </summary>
|
||||
[HttpPost("deck_list")]
|
||||
public async Task<ActionResult<PracticeDeckListResponse>> DeckList(DeckFormatRequest request)
|
||||
{
|
||||
var shortUdidClaim = User.Claims.FirstOrDefault(c => c.Type == ShadowverseClaimTypes.ShortUdidClaim)?.Value;
|
||||
if (shortUdidClaim is null || !long.TryParse(shortUdidClaim, out long shortUdid))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
Viewer? viewer = await _viewerRepository.GetViewerByShortUdid(shortUdid);
|
||||
if (viewer is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return new PracticeDeckListResponse
|
||||
{
|
||||
MaintenanceCardList = new List<long>(),
|
||||
UserDeckRotation = viewer.Decks.Where(d => d.Format == Format.Rotation)
|
||||
.Select(d => new UserDeck(d)).ToList(),
|
||||
UserDeckUnlimited = viewer.Decks.Where(d => d.Format == Format.Unlimited)
|
||||
.Select(d => new UserDeck(d)).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// /practice/start — server is essentially a no-op for practice. Spec: empty body
|
||||
/// response is fine; client tolerates missing mission_parameter.
|
||||
/// </summary>
|
||||
[HttpPost("start")]
|
||||
public Task<PracticeStartResponse> Start(BaseRequest request)
|
||||
{
|
||||
return Task.FromResult(new PracticeStartResponse());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// /practice/finish — accept the recovery_data blob without validation; return zero
|
||||
/// XP / no rewards. Class XP bookkeeping is deferred until a per-class XP store exists.
|
||||
/// </summary>
|
||||
[HttpPost("finish")]
|
||||
public Task<PracticeFinishResponse> Finish(PracticeFinishRequest request)
|
||||
{
|
||||
return Task.FromResult(new PracticeFinishResponse
|
||||
{
|
||||
GetClassExperience = 0,
|
||||
ClassExperience = 0,
|
||||
ClassLevel = 1,
|
||||
AchievedInfo = new Dictionary<string, object>(),
|
||||
RewardList = new List<Models.Dtos.Common.Reward>()
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user