Skip to content

Commit

Permalink
account for gametime drift
Browse files Browse the repository at this point in the history
see #78
see #21
  • Loading branch information
dealloc committed Apr 19, 2024
1 parent f626285 commit 3388b84
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 55 deletions.
39 changes: 17 additions & 22 deletions src/Helldivers-2-Core/Facades/V1Facade.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Helldivers.Core.Contracts;
using Helldivers.Core.Contracts.Collections;
using Helldivers.Core.Mapping;
using Helldivers.Core.Mapping.V1;
using Helldivers.Models.V1;

Expand All @@ -22,60 +23,54 @@ DispatchMapper dispatchMapper
)
{
/// <see cref="IStore{T,TKey}.SetStore" />
public async ValueTask UpdateStores(Models.ArrowHead.WarId warId, Models.ArrowHead.WarInfo warInfo, Dictionary<string, Models.ArrowHead.WarStatus> warStatuses, Models.ArrowHead.WarSummary warSummary, Dictionary<string, List<Models.ArrowHead.NewsFeedItem>> newsFeeds, Dictionary<string, List<Models.ArrowHead.Assignment>> assignments)
public async ValueTask UpdateStores(MappingContext context)
{
// TODO: map warId

// Fetch a WarStatus for mapping that don't need localized data.
var invariantStatus = warStatuses.FirstOrDefault().Value;

await UpdatePlanetStore(warInfo, invariantStatus, warSummary);
await UpdatePlanetStore(context);
// Some mappers need access to the list of planets, so we fetch it from the freshly-mapped store.
var planets = await planetStore.AllAsync();

await UpdateWarStore(warInfo, invariantStatus, warSummary, planets);
await UpdateCampaignStore(invariantStatus, planets);
await UpdateAssignmentsStore(assignments);
await UpdateDispatchStore(warInfo, newsFeeds);
await UpdateWarStore(context, planets);
await UpdateCampaignStore(context, planets);
await UpdateAssignmentsStore(context);
await UpdateDispatchStore(context);
}

private async ValueTask UpdateWarStore(Models.ArrowHead.WarInfo info, Models.ArrowHead.WarStatus status, Models.ArrowHead.WarSummary summary, List<Planet> planets)
private async ValueTask UpdateWarStore(MappingContext context, List<Planet> planets)
{
var war = warMapper.MapToV1(info, status, summary, planets);
var war = warMapper.MapToV1(context, planets);

await warStore.SetStore(war);
}

private async ValueTask UpdatePlanetStore(Models.ArrowHead.WarInfo warInfo, Models.ArrowHead.WarStatus warStatus, Models.ArrowHead.WarSummary summary)
private async ValueTask UpdatePlanetStore(MappingContext context)
{
var planets = planetMapper.MapToV1(warInfo, warStatus, summary).ToList();
var planets = planetMapper.MapToV1(context).ToList();

await planetStore.SetStore(planets);
}

private async ValueTask UpdateCampaignStore(Models.ArrowHead.WarStatus status, List<Planet> planets)
private async ValueTask UpdateCampaignStore(MappingContext context, List<Planet> planets)
{
var campaigns = status
.Campaigns
.Select(campaign => campaignMapper.MapToV1(campaign, planets))
.ToList();
var campaigns = campaignMapper.MapToV1(context, planets).ToList();

await campaignStore.SetStore(campaigns);
}

private async ValueTask UpdateAssignmentsStore(Dictionary<string, List<Models.ArrowHead.Assignment>> translations)
private async ValueTask UpdateAssignmentsStore(MappingContext context)
{
var assignments = assignmentMapper
.MapToV1(translations)
.MapToV1(context)
.ToList();

await assignmentStore.SetStore(assignments);
}

private async ValueTask UpdateDispatchStore(Models.ArrowHead.WarInfo info, Dictionary<string, List<Models.ArrowHead.NewsFeedItem>> translations)
private async ValueTask UpdateDispatchStore(MappingContext context)
{
var dispatches = dispatchMapper
.MapToV1(info, translations)
.MapToV1(context)
.OrderByDescending(dispatch => dispatch.Id)
.ToList();

Expand Down
63 changes: 63 additions & 0 deletions src/Helldivers-2-Core/Mapping/MappingContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Helldivers.Models.ArrowHead;

namespace Helldivers.Core.Mapping;

/// <summary>
/// Contains the context of the ArrowHead models currently being mapped.
/// It's main purpose is to provide a convenient way to pass around <b>all</b> ArrowHead data
/// at once without having to pass 7-8 parameters to each function (or when adding more data,
/// having to update all dependent signatures).
/// </summary>
public sealed class MappingContext
{
/// <summary>The <see cref="WarId" /> currently being mapped.</summary>
public WarId WarId { get; private init; }

/// <summary>The <see cref="WarInfo" /> currently being mapped.</summary>
public WarInfo WarInfo { get; private init; }

/// <summary>The <see cref="WarStatus" />es currently being mapped.</summary>
public Dictionary<string, WarStatus> WarStatuses { get; private init; }

/// <summary>A <see cref="WarStatus" /> that can be used when accessing non-localized data.</summary>
public WarStatus InvariantWarStatus { get; private init; }

/// <summary>The <see cref="WarSummary" /> currently being mapped.</summary>
public WarSummary WarSummary { get; private init; }

/// <summary>The <see cref="NewsFeedItem" />s currently being mapped.</summary>
public Dictionary<string, List<NewsFeedItem>> NewsFeeds { get; private init; }

/// <summary>The <see cref="Assignment" />s currently being mapped.</summary>
public Dictionary<string, List<Assignment>> Assignments { get; private init; }

/// <summary>
/// A <see cref="DateTime" /> that represents the 'start' of the time in Helldivers 2.
/// This accounts for the <see cref="Models.ArrowHead.WarInfo.StartDate" /> and <see cref="GameTimeDeviation" />.
/// </summary>
public DateTime RelativeGameStart { get; private init; }

/// <summary>
/// There's a deviation between gametime and real world time.
/// When calculating dates using ArrowHead timestamps, add this to compensate for the deviation.
/// </summary>
public TimeSpan GameTimeDeviation { get; private init; }

/// <summary>Initializes a new <see cref="MappingContext" />.</summary>
internal MappingContext(WarId warId, WarInfo warInfo, Dictionary<string, WarStatus> warStatuses, WarSummary warSummary, Dictionary<string, List<NewsFeedItem>> newsFeeds, Dictionary<string, List<Assignment>> assignments)
{
WarId = warId;
WarInfo = warInfo;
WarStatuses = warStatuses;
WarSummary = warSummary;
NewsFeeds = newsFeeds;
Assignments = assignments;

InvariantWarStatus = warStatuses.FirstOrDefault().Value
?? throw new InvalidOperationException("No warstatus available");

var gameTime = DateTime.UnixEpoch.AddSeconds(warInfo.StartDate + InvariantWarStatus.Time);
GameTimeDeviation = DateTime.UtcNow.Subtract(gameTime);
RelativeGameStart = DateTime.UnixEpoch.Add(GameTimeDeviation).AddSeconds(warInfo.StartDate);
}
}
6 changes: 3 additions & 3 deletions src/Helldivers-2-Core/Mapping/V1/AssignmentMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ public sealed class AssignmentMapper
/// <summary>
/// Maps a set of multi-language <see cref="Assignment" />s into a list of <see cref="Assignment" />.
/// </summary>
public IEnumerable<Assignment> MapToV1(Dictionary<string, List<Models.ArrowHead.Assignment>> assignments)
public IEnumerable<Assignment> MapToV1(MappingContext context)
{
// Get a list of all assignments across all translations.
var invariants = assignments
var invariants = context.Assignments
.SelectMany(pair => pair.Value)
.DistinctBy(assignment => assignment.Id32);

foreach (var assignment in invariants)
{
// Build a dictionary of all translations for this assignment
var translations = assignments.Select(pair =>
var translations = context.Assignments.Select(pair =>
new KeyValuePair<string, Models.ArrowHead.Assignment?>(
pair.Key,
pair.Value.FirstOrDefault(a => a.Id32 == assignment.Id32)
Expand Down
11 changes: 10 additions & 1 deletion src/Helldivers-2-Core/Mapping/V1/CampaignMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ namespace Helldivers.Core.Mapping.V1;
/// </summary>
public class CampaignMapper
{
/// <summary>
/// Maps all <see cref="Campaign" /> in the current context.
/// </summary>
public IEnumerable<Campaign> MapToV1(MappingContext context, List<Planet> planets)
{
foreach (var campaign in context.InvariantWarStatus.Campaigns)
yield return MapToV1(campaign, planets);
}

/// <summary>
/// Maps ArrowHead's <see cref="Models.ArrowHead.Status.Campaign" /> onto V1's.
/// </summary>
public Campaign MapToV1(Models.ArrowHead.Status.Campaign campaign, List<Planet> planets)
private Campaign MapToV1(Models.ArrowHead.Status.Campaign campaign, List<Planet> planets)
{
var planet = planets.First(p => p.Index == campaign.PlanetIndex);

Expand Down
12 changes: 6 additions & 6 deletions src/Helldivers-2-Core/Mapping/V1/DispatchMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,36 @@ public sealed class DispatchMapper
/// <summary>
/// Maps ArrowHead's <see cref="WarInfo" /> onto V1's <see cref="Dispatch" />es.
/// </summary>
public IEnumerable<Dispatch> MapToV1(WarInfo info, Dictionary<string, List<NewsFeedItem>> items)
public IEnumerable<Dispatch> MapToV1(MappingContext context)
{
// Get a list of all items across all translations.
var invariants = items
var invariants = context.NewsFeeds
.SelectMany(pair => pair.Value)
.DistinctBy(assignment => assignment.Id);

foreach (var item in invariants)
{
// Build a dictionary of all translations for this item
var translations = items.Select(pair =>
var translations = context.NewsFeeds.Select(pair =>
new KeyValuePair<string, NewsFeedItem?>(
pair.Key,
pair.Value.FirstOrDefault(a => a.Id == item.Id)
)
).Where(pair => pair.Value is not null)
.ToDictionary(pair => pair.Key, pair => pair.Value!);

yield return MapToV1(info, translations);
yield return MapToV1(context, translations);
}
}

private Dispatch MapToV1(WarInfo info, Dictionary<string, NewsFeedItem> translations)
private Dispatch MapToV1(MappingContext context, Dictionary<string, NewsFeedItem> translations)
{
var invariant = translations.Values.First();
var messages = translations.Select(pair => new KeyValuePair<string, string>(pair.Key, pair.Value.Message));

return new Dispatch(
Id: invariant.Id,
Published: DateTime.UnixEpoch.AddSeconds(info.StartDate + invariant.Published),
Published: context.RelativeGameStart.AddSeconds(invariant.Published),
Type: invariant.Type,
Message: LocalizedMessage.FromStrings(messages)
);
Expand Down
27 changes: 13 additions & 14 deletions src/Helldivers-2-Core/Mapping/V1/PlanetMapper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Helldivers.Models;
using Helldivers.Models.ArrowHead;
using Helldivers.Models.ArrowHead.Info;
using Helldivers.Models.ArrowHead.Status;
using Helldivers.Models.ArrowHead.Summary;
Expand All @@ -15,28 +14,28 @@ public sealed class PlanetMapper(StatisticsMapper statisticsMapper)
{
/// <summary>
/// Maps all planet information into a list of <see cref="Planet" /> objects.
/// see <see cref="MapToV1(Helldivers.Models.ArrowHead.WarInfo,Helldivers.Models.ArrowHead.WarStatus,Helldivers.Models.ArrowHead.WarSummary)" />
/// see <see cref="MapToV1(PlanetInfo, PlanetStatus, PlanetEvent, PlanetStats, List{int})" />

Check failure on line 17 in src/Helldivers-2-Core/Mapping/V1/PlanetMapper.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has cref attribute 'MapToV1(PlanetInfo, PlanetStatus, PlanetEvent, PlanetStats, List{int})' that could not be resolved

Check failure on line 17 in src/Helldivers-2-Core/Mapping/V1/PlanetMapper.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has cref attribute 'MapToV1(PlanetInfo, PlanetStatus, PlanetEvent, PlanetStats, List{int})' that could not be resolved
/// </summary>
public IEnumerable<Planet> MapToV1(WarInfo warInfo, WarStatus warStatus, WarSummary summary)
public IEnumerable<Planet> MapToV1(MappingContext context)
{
foreach (var info in warInfo.PlanetInfos)
foreach (var info in context.WarInfo.PlanetInfos)
{
var status = warStatus.PlanetStatus.First(status => status.Index == info.Index);
var stats = summary.PlanetsStats.FirstOrDefault(stats => stats.PlanetIndex == info.Index);
var @event = warStatus.PlanetEvents.FirstOrDefault(@event => @event.PlanetIndex == info.Index);
var attacking = warStatus.PlanetAttacks
var status = context.InvariantWarStatus.PlanetStatus.First(status => status.Index == info.Index);
var stats = context.WarSummary.PlanetsStats.FirstOrDefault(stats => stats.PlanetIndex == info.Index);
var @event = context.InvariantWarStatus.PlanetEvents.FirstOrDefault(@event => @event.PlanetIndex == info.Index);
var attacking = context.InvariantWarStatus.PlanetAttacks
.Where(attack => attack.Source == info.Index)
.Select(attack => attack.Target)
.ToList();

yield return MapToV1(info, status, @event, stats, attacking);
yield return MapToV1(context, info, status, @event, stats, attacking);
}
}

/// <summary>
/// Merges all ArrowHead data points on planets into a single <see cref="Planet" /> object.
/// </summary>
public Planet MapToV1(PlanetInfo info, PlanetStatus status, PlanetEvent? @event, PlanetStats? stats, List<int> attacking)
private Planet MapToV1(MappingContext context, PlanetInfo info, PlanetStatus status, PlanetEvent? @event, PlanetStats? stats, List<int> attacking)
{
Static.Planets.TryGetValue(info.Index, out var planet);
Static.Factions.TryGetValue(info.InitialOwner, out var initialOwner);
Expand All @@ -59,13 +58,13 @@ public Planet MapToV1(PlanetInfo info, PlanetStatus status, PlanetEvent? @event,
InitialOwner: initialOwner ?? string.Empty,
CurrentOwner: currentOwner ?? string.Empty,
RegenPerSecond: status.RegenPerSecond,
Event: MapToV1(@event),
Event: MapToV1(@event, context),
Statistics: statisticsMapper.MapToV1(stats, status),
Attacking: attacking
);
}

private Event? MapToV1(PlanetEvent? @event)
private Event? MapToV1(PlanetEvent? @event, MappingContext context)
{
if (@event is null)
return null;
Expand All @@ -78,8 +77,8 @@ public Planet MapToV1(PlanetInfo info, PlanetStatus status, PlanetEvent? @event,
Faction: faction ?? string.Empty,
Health: @event.Health,
MaxHealth: @event.MaxHealth,
StartTime: DateTime.UnixEpoch.AddSeconds(@event.StartTime),
EndTime: DateTime.UnixEpoch.AddSeconds(@event.ExpireTime),
StartTime: context.RelativeGameStart.AddSeconds(@event.StartTime),
EndTime: context.RelativeGameStart.AddSeconds(@event.ExpireTime),
CampaignId: @event.CampaignId,
JointOperationIds: @event.JointOperationIds
);
Expand Down
15 changes: 7 additions & 8 deletions src/Helldivers-2-Core/Mapping/V1/WarMapper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Helldivers.Models;
using Helldivers.Models.ArrowHead;
using Helldivers.Models.V1;

namespace Helldivers.Core.Mapping.V1;
Expand All @@ -12,16 +11,16 @@ public sealed class WarMapper(StatisticsMapper statisticsMapper)
/// <summary>
/// Handles mapping <see cref="War" /> to V1.
/// </summary>
public War MapToV1(WarInfo info, WarStatus status, WarSummary summary, List<Planet> planets)
public War MapToV1(MappingContext context, List<Planet> planets)
{
return new War(
Started: DateTime.UnixEpoch.AddSeconds(info.StartDate),
Ended: DateTime.UnixEpoch.AddSeconds(info.EndDate),
Now: DateTime.UnixEpoch.AddSeconds(status.Time),
ClientVersion: info.MinimumClientVersion,
Started: DateTime.UnixEpoch.AddSeconds(context.WarInfo.StartDate),
Ended: DateTime.UnixEpoch.AddSeconds(context.WarInfo.EndDate),
Now: DateTime.UnixEpoch.AddSeconds(context.InvariantWarStatus.Time),
ClientVersion: context.WarInfo.MinimumClientVersion,
Factions: Static.Factions.Values.ToList(),
ImpactMultiplier: status.ImpactMultiplier,
Statistics: statisticsMapper.MapToV1(summary.GalaxyStats, planets)
ImpactMultiplier: context.InvariantWarStatus.ImpactMultiplier,
Statistics: statisticsMapper.MapToV1(context.WarSummary.GalaxyStats, planets)
);
}
}
5 changes: 4 additions & 1 deletion src/Helldivers-2-Core/StorageFacade.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Helldivers.Core.Facades;
using Helldivers.Core.Mapping;
using Helldivers.Core.Storage.ArrowHead;
using Helldivers.Models;
using Helldivers.Models.Steam;
Expand Down Expand Up @@ -51,14 +52,16 @@ public async ValueTask UpdateStores(Memory<byte> rawWarId, Memory<byte> rawWarIn
pair => DeserializeOrThrow(pair.Value, ArrowHeadSerializerContext.Default.ListAssignment)
);

await v1.UpdateStores(
var context = new MappingContext(
warId,
warInfo,
warStatuses,
warSummary,
newsFeeds,
assignments
);

await v1.UpdateStores(context);
}

private T DeserializeOrThrow<T>(Memory<byte> memory, JsonTypeInfo<T> typeInfo)
Expand Down

0 comments on commit 3388b84

Please sign in to comment.