Files
bd2/plugins/LocalIdentity/AgeGateState.cs
T
Flechazo 113e941bef feat: expand gameplay and state persistence
- add character imprint, awakening, and equipment refinement

- add equipment ticket gacha and persistent progression fields

- add request-scoped account transactions with crash recovery

- persist age confirmation and isolate client plugin patches

- separate one-off save repair tools from shipped tooling
2026-09-23 00:55:36 +08:00

108 lines
3.2 KiB
C#

using System;
using System.IO;
using System.Text;
using BepInEx;
namespace Bd2LocalIdentity;
// The server's UpdateAgeGate response is intentionally empty. Once that
// response succeeds, the only client-side fact needed on the next login is
// that this local installation has completed the age-gate flow. Do not keep
// the date of birth: it is not consumed by the client after the request and
// retaining it would add unnecessary personal data to a local plugin file.
internal static class AgeGateState
{
private const string FileName = "bd2.localidentity.age-gate.state";
private const string Content = "format=1\nconfirmed=true\n";
private static readonly object Gate = new object();
private static bool? confirmed;
internal static bool IsConfirmed()
{
lock (Gate)
{
if (confirmed.HasValue)
{
return confirmed.Value;
}
try
{
string path = StatePath();
confirmed = File.Exists(path) &&
string.Equals(File.ReadAllText(path, Encoding.UTF8), Content, StringComparison.Ordinal);
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
confirmed = false;
Plugin.LogWarning("Could not read local age-gate state: " + ex.Message);
}
return confirmed.Value;
}
}
internal static void MarkConfirmed()
{
lock (Gate)
{
if (confirmed == true)
{
return;
}
string path = StatePath();
string directory = Path.GetDirectoryName(path);
if (string.IsNullOrEmpty(directory))
{
throw new IOException("BepInEx configuration directory is unavailable.");
}
Directory.CreateDirectory(directory);
string temporaryPath = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
try
{
WriteThrough(temporaryPath, Content);
if (File.Exists(path))
{
// Both files are in BepInEx/config, so replacement is an
// atomic same-volume operation on the supported Windows client.
File.Replace(temporaryPath, path, null);
}
else
{
File.Move(temporaryPath, path);
}
confirmed = true;
}
finally
{
if (File.Exists(temporaryPath))
{
File.Delete(temporaryPath);
}
}
}
}
private static string StatePath()
{
return Path.Combine(Paths.ConfigPath, FileName);
}
private static void WriteThrough(string path, string content)
{
byte[] bytes = new UTF8Encoding(false).GetBytes(content);
using FileStream stream = new FileStream(
path,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
4096,
FileOptions.WriteThrough);
stream.Write(bytes, 0, bytes.Length);
stream.Flush(true);
}
}