99 lines
6.1 KiB
C#
99 lines
6.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace MagnetismHeroes.Core
|
|
{
|
|
public static class SaveValidator
|
|
{
|
|
public static bool TryValidate(ProgressSave save, IReadOnlyList<StageDefinition> definitions, out string error)
|
|
{
|
|
if (save == null || save.schemaVersion != 1) return Fail("Missing save or unsupported schema version.", out error);
|
|
if (definitions == null || definitions.Any(definition => definition == null)
|
|
|| definitions.Select(definition => definition.Id).Distinct().Count() != definitions.Count)
|
|
return Fail("Invalid definition catalog.", out error);
|
|
if (save.completedStageIds == null || save.completedStageIds.Any(id => id < 1 || id > 30)
|
|
|| save.completedStageIds.Distinct().Count() != save.completedStageIds.Length)
|
|
return Fail("Invalid completed stage IDs.", out error);
|
|
if (save.seenTutorialIds == null || save.seenTutorialIds.Any(id => id != "merge" && id != "split")
|
|
|| save.seenTutorialIds.Distinct().Count() != save.seenTutorialIds.Length)
|
|
return Fail("Invalid tutorial IDs.", out error);
|
|
if (save.puzzleStates == null || save.puzzleStates.Length > 30 || save.puzzleStates.Any(state => state == null)
|
|
|| save.puzzleStates.Select(state => state.stageId).Distinct().Count() != save.puzzleStates.Length)
|
|
return Fail("Invalid puzzle state list.", out error);
|
|
foreach (var state in save.puzzleStates)
|
|
{
|
|
var definition = definitions.FirstOrDefault(candidate => candidate.Id == state.stageId);
|
|
if (!TryValidatePuzzle(state, definition, out error)) return false;
|
|
}
|
|
if (save.latestFreePlay != null)
|
|
{
|
|
if (!save.completedStageIds.Contains(save.latestFreePlay.sourceStageId)) return Fail("Free play source was not completed.", out error);
|
|
if (!TryValidateFreePlay(save.latestFreePlay, out error)) return false;
|
|
}
|
|
error = null;
|
|
return true;
|
|
}
|
|
|
|
public static bool TryValidateUpdate(ProgressSave previous, ProgressSave next, IReadOnlyList<StageDefinition> definitions, out string error)
|
|
{
|
|
if (!TryValidate(previous, definitions, out error) || !TryValidate(next, definitions, out error)) return false;
|
|
if (previous.completedStageIds.Except(next.completedStageIds).Any()) return Fail("Completed stage IDs cannot decrease.", out error);
|
|
return true;
|
|
}
|
|
|
|
public static bool TryValidatePuzzle(PuzzleState state, StageDefinition definition, out string error)
|
|
{
|
|
if (state == null || definition == null || state.stageId != definition.Id) return Fail("Puzzle stage has no matching definition.", out error);
|
|
if (!ValidateBoard(state.balls, state.groups, state.camera, out var ballIds, out error)) return false;
|
|
if (state.balls.Length != definition.Total) return Fail("Puzzle total differs from its definition.", out error);
|
|
if (!ValidateSubset(state.collectedBallIds, ballIds, out error)) return false;
|
|
if (!ValidateSubset(state.countedBallIds, new HashSet<string>(state.collectedBallIds, StringComparer.Ordinal), out error)) return false;
|
|
if (definition.Kind == StageKind.Composition && (state.collectedBallIds.Length != 0 || state.countedBallIds.Length != 0))
|
|
return Fail("Composition puzzles cannot contain counting records.", out error);
|
|
error = null;
|
|
return true;
|
|
}
|
|
|
|
public static bool TryValidateFreePlay(FreePlayState state, out string error)
|
|
{
|
|
if (state == null || state.sourceStageId < 1 || state.sourceStageId > 30) return Fail("Invalid free play source.", out error);
|
|
return ValidateBoard(state.balls, state.groups, state.camera, out _, out error);
|
|
}
|
|
|
|
private static bool ValidateBoard(BallState[] balls, GroupState[] groups, CameraState camera, out HashSet<string> ballIds, out string error)
|
|
{
|
|
ballIds = new HashSet<string>(StringComparer.Ordinal);
|
|
if (balls == null || groups == null || balls.Length > 10 || !ValidCamera(camera)) return Fail("Invalid board collections or camera.", out error);
|
|
foreach (var ball in balls)
|
|
if (ball == null || !BoardRules.ValidId(ball.id) || !ballIds.Add(ball.id) || !ball.position.IsValid)
|
|
return Fail("Invalid or duplicate ball ID or position.", out error);
|
|
var groupIds = new HashSet<string>(StringComparer.Ordinal);
|
|
var membership = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var group in groups)
|
|
{
|
|
if (group == null || !BoardRules.ValidId(group.id) || !groupIds.Add(group.id) || group.ballIds == null || group.ballIds.Length == 0)
|
|
return Fail("Invalid, duplicate, or empty group.", out error);
|
|
foreach (var id in group.ballIds)
|
|
if (!BoardRules.ValidId(id) || !ballIds.Contains(id) || !membership.Add(id)) return Fail("Invalid or duplicate group membership.", out error);
|
|
}
|
|
if (!membership.SetEquals(ballIds)) return Fail("Every ball must belong to exactly one group.", out error);
|
|
error = null;
|
|
return true;
|
|
}
|
|
|
|
private static bool ValidateSubset(string[] ids, HashSet<string> allowed, out string error)
|
|
{
|
|
if (ids == null || ids.Any(id => !BoardRules.ValidId(id) || !allowed.Contains(id)) || ids.Distinct().Count() != ids.Length)
|
|
return Fail("Counting IDs must be unique subsets of their parent set.", out error);
|
|
error = null;
|
|
return true;
|
|
}
|
|
|
|
internal static bool ValidCamera(CameraState camera) => camera != null && camera.target.IsValid && camera.rotation.IsValid
|
|
&& Position3.Finite(camera.distance) && camera.distance > 0;
|
|
|
|
private static bool Fail(string message, out string error) { error = message; return false; }
|
|
}
|
|
}
|