feat(mission): wire Tier 1 emits — rank/free/challenge match events

Adds IMissionProgressService to RankBattle, FreeBattle, ArenaTwoPickBattle, and
ArenaTwoPick controllers. Each finish handler emits the appropriate
MissionEventKeys builder result:

- RankBattle.Finish (win) → Ranked.WinAll(classId)
  Includes AI-rank routes; on a private server AI wins count the same.
- FreeBattle.Finish (win) → Free.WinAll() (aggregates only)
- ArenaTwoPickBattle.Finish → Challenge.MatchPlayAll or MatchWinAll
  Always fires challenge_play; wins additionally fire challenge_win and
  the ranked/arena/daily aggregates.
- ArenaTwoPick.Finish (5-win full clear) → Challenge.FullClearAll
  Gated on RunFinishOutcome.WasFullClear from the previous commit.

This closes six of the eight remaining catalog wire-up gaps; class_level_up
and rank_achieved follow in Tier 2 (need pre-state detection).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-04 13:22:02 -04:00
parent 94622a526d
commit 42b0fd3d6e
4 changed files with 49 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Bridge;
using SVSim.Database.Enums; using SVSim.Database.Enums;
using SVSim.Database.Services;
using SVSim.Database.Services.Friend; using SVSim.Database.Services.Friend;
using SVSim.Database.Services.Replay; using SVSim.Database.Services.Replay;
using SVSim.EmulatedEntrypoint.Extensions; using SVSim.EmulatedEntrypoint.Extensions;
@@ -20,6 +21,7 @@ public class ArenaTwoPickBattleController : SVSimController
private readonly IBattleContextStore _battleContextStore; private readonly IBattleContextStore _battleContextStore;
private readonly IBattleHistoryWriter _historyWriter; private readonly IBattleHistoryWriter _historyWriter;
private readonly IPlayedTogetherWriter _playedTogetherWriter; private readonly IPlayedTogetherWriter _playedTogetherWriter;
private readonly IMissionProgressService _missionProgress;
public ArenaTwoPickBattleController( public ArenaTwoPickBattleController(
IArenaTwoPickService svc, IArenaTwoPickService svc,
@@ -27,7 +29,8 @@ public class ArenaTwoPickBattleController : SVSimController
IMatchingResolver resolver, IMatchingResolver resolver,
IBattleContextStore battleContextStore, IBattleContextStore battleContextStore,
IBattleHistoryWriter historyWriter, IBattleHistoryWriter historyWriter,
IPlayedTogetherWriter playedTogetherWriter) IPlayedTogetherWriter playedTogetherWriter,
IMissionProgressService missionProgress)
{ {
_svc = svc; _svc = svc;
_matchContextBuilder = matchContextBuilder; _matchContextBuilder = matchContextBuilder;
@@ -35,6 +38,7 @@ public class ArenaTwoPickBattleController : SVSimController
_battleContextStore = battleContextStore; _battleContextStore = battleContextStore;
_historyWriter = historyWriter; _historyWriter = historyWriter;
_playedTogetherWriter = playedTogetherWriter; _playedTogetherWriter = playedTogetherWriter;
_missionProgress = missionProgress;
} }
[HttpPost("do_matching")] [HttpPost("do_matching")]
@@ -117,6 +121,14 @@ public class ArenaTwoPickBattleController : SVSimController
} }
var result = await _svc.RecordBattleResultAsync(vid, isWin); var result = await _svc.RecordBattleResultAsync(vid, isWin);
// Mission counters — TK2 matches always advance challenge_play, wins additionally
// advance challenge_win + the ranked/arena/daily aggregates.
await _missionProgress.RecordEventAsync(
vid,
isWin ? MissionEventKeys.Challenge.MatchWinAll() : MissionEventKeys.Challenge.MatchPlayAll(),
ct: ct);
return Ok(new BattleFinishResponseDto return Ok(new BattleFinishResponseDto
{ {
BattleResult = result.BattleResult, BattleResult = result.BattleResult,

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SVSim.Database.Services;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.ArenaTwoPick; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.ArenaTwoPick;
using SVSim.EmulatedEntrypoint.Services; using SVSim.EmulatedEntrypoint.Services;
@@ -8,7 +9,13 @@ namespace SVSim.EmulatedEntrypoint.Controllers;
public class ArenaTwoPickController : SVSimController public class ArenaTwoPickController : SVSimController
{ {
private readonly IArenaTwoPickService _svc; private readonly IArenaTwoPickService _svc;
public ArenaTwoPickController(IArenaTwoPickService svc) => _svc = svc; private readonly IMissionProgressService _missionProgress;
public ArenaTwoPickController(IArenaTwoPickService svc, IMissionProgressService missionProgress)
{
_svc = svc;
_missionProgress = missionProgress;
}
[HttpPost("top")] [HttpPost("top")]
public async Task<IActionResult> Top([FromBody] TopRequest _) public async Task<IActionResult> Top([FromBody] TopRequest _)
@@ -46,12 +53,17 @@ public class ArenaTwoPickController : SVSimController
} }
[HttpPost("finish")] [HttpPost("finish")]
public async Task<IActionResult> Finish([FromBody] FinishRequest _) public async Task<IActionResult> Finish([FromBody] FinishRequest _, CancellationToken ct = default)
{ {
if (!TryGetViewerId(out var vid)) return Unauthorized(); if (!TryGetViewerId(out var vid)) return Unauthorized();
return await GuardAsync(async () => return await GuardAsync(async () =>
{ {
var outcome = await _svc.FinishAsync(vid); var outcome = await _svc.FinishAsync(vid);
if (outcome.WasFullClear)
{
await _missionProgress.RecordEventAsync(
vid, MissionEventKeys.Challenge.FullClearAll(), ct: ct);
}
return outcome.Response; return outcome.Response;
}); });
} }

View File

@@ -4,6 +4,7 @@ using SVSim.BattleNode.Bridge;
using SVSim.Database; using SVSim.Database;
using SVSim.Database.Enums; using SVSim.Database.Enums;
using SVSim.Database.Repositories.Viewer; using SVSim.Database.Repositories.Viewer;
using SVSim.Database.Services;
using SVSim.Database.Services.BattleXp; using SVSim.Database.Services.BattleXp;
using SVSim.Database.Services.Friend; using SVSim.Database.Services.Friend;
using SVSim.Database.Services.Replay; using SVSim.Database.Services.Replay;
@@ -37,6 +38,7 @@ public sealed class FreeBattleController : ControllerBase
private readonly IPlayedTogetherWriter _playedTogetherWriter; private readonly IPlayedTogetherWriter _playedTogetherWriter;
private readonly IViewerRepository _viewers; private readonly IViewerRepository _viewers;
private readonly IBattleXpService _xp; private readonly IBattleXpService _xp;
private readonly IMissionProgressService _missionProgress;
private readonly SVSimDbContext _db; private readonly SVSimDbContext _db;
private readonly ILogger<FreeBattleController> _log; private readonly ILogger<FreeBattleController> _log;
@@ -48,6 +50,7 @@ public sealed class FreeBattleController : ControllerBase
IPlayedTogetherWriter playedTogetherWriter, IPlayedTogetherWriter playedTogetherWriter,
IViewerRepository viewers, IViewerRepository viewers,
IBattleXpService xp, IBattleXpService xp,
IMissionProgressService missionProgress,
SVSimDbContext db, SVSimDbContext db,
ILogger<FreeBattleController> log) ILogger<FreeBattleController> log)
{ {
@@ -58,6 +61,7 @@ public sealed class FreeBattleController : ControllerBase
_playedTogetherWriter = playedTogetherWriter; _playedTogetherWriter = playedTogetherWriter;
_viewers = viewers; _viewers = viewers;
_xp = xp; _xp = xp;
_missionProgress = missionProgress;
_db = db; _db = db;
_log = log; _log = log;
} }
@@ -120,6 +124,13 @@ public sealed class FreeBattleController : ControllerBase
level = xp.Level == 0 ? 1 : xp.Level; level = xp.Level == 0 ? 1 : xp.Level;
} }
// Mission counters — unranked wins feed ranked_or_arena_win + daily_match_win.
if (isWin)
{
await _missionProgress.RecordEventAsync(
vid, MissionEventKeys.Free.WinAll(), ct: ct);
}
return Ok(new FreeBattleFinishResponseDto return Ok(new FreeBattleFinishResponseDto
{ {
BattleResult = req.BattleResult, BattleResult = req.BattleResult,

View File

@@ -5,6 +5,7 @@ using SVSim.BattleNode.Sessions;
using SVSim.Database; using SVSim.Database;
using SVSim.Database.Enums; using SVSim.Database.Enums;
using SVSim.Database.Repositories.Viewer; using SVSim.Database.Repositories.Viewer;
using SVSim.Database.Services;
using SVSim.Database.Services.BattleXp; using SVSim.Database.Services.BattleXp;
using SVSim.Database.Services.Friend; using SVSim.Database.Services.Friend;
using SVSim.Database.Services.RankProgress; using SVSim.Database.Services.RankProgress;
@@ -40,6 +41,7 @@ public sealed class RankBattleController : ControllerBase
private readonly IViewerRepository _viewers; private readonly IViewerRepository _viewers;
private readonly IBattleXpService _xp; private readonly IBattleXpService _xp;
private readonly IRankProgressService _rankProgress; private readonly IRankProgressService _rankProgress;
private readonly IMissionProgressService _missionProgress;
private readonly SVSimDbContext _db; private readonly SVSimDbContext _db;
private readonly ILogger<RankBattleController> _log; private readonly ILogger<RankBattleController> _log;
@@ -54,6 +56,7 @@ public sealed class RankBattleController : ControllerBase
IViewerRepository viewers, IViewerRepository viewers,
IBattleXpService xp, IBattleXpService xp,
IRankProgressService rankProgress, IRankProgressService rankProgress,
IMissionProgressService missionProgress,
SVSimDbContext db, SVSimDbContext db,
ILogger<RankBattleController> log) ILogger<RankBattleController> log)
{ {
@@ -67,6 +70,7 @@ public sealed class RankBattleController : ControllerBase
_viewers = viewers; _viewers = viewers;
_xp = xp; _xp = xp;
_rankProgress = rankProgress; _rankProgress = rankProgress;
_missionProgress = missionProgress;
_db = db; _db = db;
_log = log; _log = log;
} }
@@ -161,6 +165,13 @@ public sealed class RankBattleController : ControllerBase
level = xp.Level == 0 ? 1 : xp.Level; level = xp.Level == 0 ? 1 : xp.Level;
} }
// Mission counters — AI-rank routes emit the same keys as human-rank on a private server.
if (isWin)
{
await _missionProgress.RecordEventAsync(
vid, MissionEventKeys.Ranked.WinAll(req.ClassId), ct: ct);
}
return Ok(new RankBattleFinishResponseDto return Ok(new RankBattleFinishResponseDto
{ {
BattleResult = req.BattleResult, BattleResult = req.BattleResult,