feat: support CNBETAWin4.5.53
This commit is contained in:
1 parent
b87874f2fe
commit
e344e699de
10 files changed
+260
-27
No files matched your search
+3
-7
@@ -10,10 +10,6 @@ logs/
|
|||||||
data/
|
data/
|
||||||
freesr-data.json
|
freesr-data.json
|
||||||
|
|
||||||
# game data and reference servers are separate checkouts, not part of CapySR
|
|
||||||
turnbasedgamedata/
|
stuff/
|
||||||
March7thHoney-OpenSource/
|
FetchHotfix/
|
||||||
pearl-sr/
|
|
||||||
reference-robinsr/
|
|
||||||
reference-himeko-nova-sr/
|
|
||||||
TurnBaseGameProto/
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# CapySR
|
# CapySR
|
||||||
|
|
||||||
A Honkai: Star Rail server reimplementation rewritten in C#.
|
A Honkai: Star Rail server reimplementation rewritten in C#.
|
||||||
Current version: `CNBETAWin4.5.52`
|
Current version: `CNBETAWin4.5.53`
|
||||||
|
|
||||||
Goals: a full combat system including combat events, and
|
Goals: a full combat system including combat events, and
|
||||||
[srtools](https://srtools.neonteam.dev/) support. Currency Wars and Divergent Universe might be focused on in the far future.
|
[srtools](https://srtools.neonteam.dev/) support. Currency Wars and Divergent Universe might be focused on in the far future.
|
||||||
@@ -11,6 +11,11 @@ Goals: a full combat system including combat events, and
|
|||||||
- .NET 10 SDK
|
- .NET 10 SDK
|
||||||
- `protoc` (only to regenerate the protocol)
|
- `protoc` (only to regenerate the protocol)
|
||||||
|
|
||||||
|
Everything the server reads but does not own lives in `stuff/` and is not tracked here: the
|
||||||
|
game data it merges (`stuff/turnbasedgamedata/ExcelOutput` from Dimbreath and
|
||||||
|
`stuff/pearl-sr/resources` for the beta overlay), the proto dump, and the reference servers.
|
||||||
|
`Data.Sources` in `config/config.json` points at the first two, in priority order.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -71,6 +76,17 @@ rotation. Challenges always play their own stage.
|
|||||||
|
|
||||||
### Hotfix URLs
|
### Hotfix URLs
|
||||||
|
|
||||||
|
Unknown versions are fetched from the official dispatch once and cached in
|
||||||
|
`config/versions.json`. A beta dispatch stops answering as soon as its window closes, so a
|
||||||
|
client newer than anything cached cannot be looked up by anyone any more. When that happens
|
||||||
|
the closest older build of the same channel is lent out, which keeps a packaged client
|
||||||
|
booting: an empty `ex_resource_url` hangs it at about 99% before it ever reaches the game
|
||||||
|
server. Turn `Hotfix.EnableDesignDataUpdate` off while a version is running on borrowed urls,
|
||||||
|
or it will try to download design data that does not match its build.
|
||||||
|
|
||||||
|
The version a client will ask for, and its dispatch seed, are in
|
||||||
|
`StarRail_Data/StreamingAssets/BinaryVersion.bytes`.
|
||||||
|
|
||||||
For OS:
|
For OS:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -99,11 +115,9 @@ dotnet run --project tools/CapySR.ProtoGen
|
|||||||
```
|
```
|
||||||
|
|
||||||
The generated protocol is committed, so this is only needed after a proto dump changes. Clone
|
The generated protocol is committed, so this is only needed after a proto dump changes. Clone
|
||||||
[TurnBaseGameProto](https://github.com/Mar7thLover/TurnBaseGameProto) next to the solution first;
|
[TurnBaseGameProto](https://github.com/Mar7thLover/TurnBaseGameProto) into `stuff/` first.
|
||||||
game data (`turnbasedgamedata`, `pearl-sr/resources`) and the reference servers are separate
|
|
||||||
checkouts too and are not tracked here.
|
|
||||||
|
|
||||||
Reads `TurnBaseGameProto/Raw/StarRail.proto`, resolves the 11 duplicate type names the dump's
|
Reads `stuff/TurnBaseGameProto/Raw/StarRail.proto`, resolves the 11 duplicate type names the dump's
|
||||||
translation pass produces, runs protoc, and writes `CmdId.g.cs` / `CmdIdTable.g.cs`. Duplicate
|
translation pass produces, runs protoc, and writes `CmdId.g.cs` / `CmdIdTable.g.cs`. Duplicate
|
||||||
resolution is scored against the `CmdXxxType` service enums and `packetIds.txt`; deliberate calls
|
resolution is scored against the `CmdXxxType` service enums and `packetIds.txt`; deliberate calls
|
||||||
live in `DuplicateResolver.Overrides`.
|
live in `DuplicateResolver.Overrides`.
|
||||||
|
|||||||
+2
-2
@@ -36,8 +36,8 @@
|
|||||||
},
|
},
|
||||||
"Data": {
|
"Data": {
|
||||||
"Sources": [
|
"Sources": [
|
||||||
"turnbasedgamedata/ExcelOutput",
|
"stuff/turnbasedgamedata/ExcelOutput",
|
||||||
"pearl-sr/resources"
|
"stuff/pearl-sr/resources"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Client": {
|
"Client": {
|
||||||
|
|||||||
@@ -82,5 +82,11 @@
|
|||||||
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_16343198_0b109c2dab94_dd42d0213ac00f",
|
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_16343198_0b109c2dab94_dd42d0213ac00f",
|
||||||
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_16317476_035e0a79224b_ed91499b6ef70b",
|
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_16317476_035e0a79224b_ed91499b6ef70b",
|
||||||
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_0_40d2ce0253_c61ba99f70b885"
|
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_0_40d2ce0253_c61ba99f70b885"
|
||||||
|
},
|
||||||
|
"CNBETAWin4.5.53": {
|
||||||
|
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_16397599_dc132a900809_7e7a4d9792c13e",
|
||||||
|
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_16399740_f7f17473a4be_cd161900e78e53",
|
||||||
|
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_16399471_bf17e3d5c3d7_5605fcd6b48271",
|
||||||
|
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_0_40d2ce0253_c61ba99f70b885"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace CapySR.Common;
|
||||||
|
|
||||||
|
// A client version reads as a channel followed by dotted numbers: CNBETAWin4.5.53.
|
||||||
|
// Ordering them matters because a beta dispatch stops answering once its window closes,
|
||||||
|
// so a build newer than anything on hand has to fall back to the closest one.
|
||||||
|
public readonly struct GameVersion(string channel, int[] parts) : IComparable<GameVersion>
|
||||||
|
{
|
||||||
|
private static readonly char[] Digits = "0123456789".ToCharArray();
|
||||||
|
|
||||||
|
public string Channel { get; } = channel;
|
||||||
|
|
||||||
|
public int[] Parts { get; } = parts;
|
||||||
|
|
||||||
|
public static bool TryParse(string? version, out GameVersion parsed)
|
||||||
|
{
|
||||||
|
parsed = default;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(version))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var digit = version.IndexOfAny(Digits);
|
||||||
|
|
||||||
|
if (digit <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pieces = version[digit..].Split('.');
|
||||||
|
var parts = new int[pieces.Length];
|
||||||
|
|
||||||
|
for (var i = 0; i < pieces.Length; i++)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(pieces[i], out parts[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = new GameVersion(version[..digit], parts);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SameChannel(GameVersion other) =>
|
||||||
|
Channel.Equals(other.Channel, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public int CompareTo(GameVersion other)
|
||||||
|
{
|
||||||
|
var length = Math.Max(Parts.Length, other.Parts.Length);
|
||||||
|
|
||||||
|
for (var i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
var mine = i < Parts.Length ? Parts[i] : 0;
|
||||||
|
var theirs = i < other.Parts.Length ? other.Parts[i] : 0;
|
||||||
|
|
||||||
|
if (mine != theirs)
|
||||||
|
{
|
||||||
|
return mine.CompareTo(theirs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the closest build of the same channel: the newest one no later than what was asked
|
||||||
|
// for, or failing that the newest there is. never crosses channels.
|
||||||
|
public static bool TryFindNearest(IEnumerable<string> candidates, string wanted, [NotNullWhen(true)] out string? nearest)
|
||||||
|
{
|
||||||
|
nearest = null;
|
||||||
|
|
||||||
|
if (!TryParse(wanted, out var target))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameVersion older = default, any = default;
|
||||||
|
var haveOlder = false;
|
||||||
|
var haveAny = false;
|
||||||
|
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
if (!TryParse(candidate, out var version) || !version.SameChannel(target))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!haveAny || version.CompareTo(any) > 0)
|
||||||
|
{
|
||||||
|
any = version;
|
||||||
|
haveAny = true;
|
||||||
|
|
||||||
|
if (!haveOlder)
|
||||||
|
{
|
||||||
|
nearest = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version.CompareTo(target) <= 0 && (!haveOlder || version.CompareTo(older) > 0))
|
||||||
|
{
|
||||||
|
older = version;
|
||||||
|
haveOlder = true;
|
||||||
|
nearest = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearest is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => Channel + string.Join('.', Parts);
|
||||||
|
}
|
||||||
@@ -121,8 +121,8 @@ public sealed class DataConfig
|
|||||||
|
|
||||||
private static readonly string[] DefaultSources =
|
private static readonly string[] DefaultSources =
|
||||||
[
|
[
|
||||||
"turnbasedgamedata/ExcelOutput",
|
"stuff/turnbasedgamedata/ExcelOutput",
|
||||||
"pearl-sr/resources",
|
"stuff/pearl-sr/resources",
|
||||||
];
|
];
|
||||||
|
|
||||||
public IEnumerable<string> ResolvedSources => (Sources.Count > 0 ? Sources : DefaultSources.ToList())
|
public IEnumerable<string> ResolvedSources => (Sources.Count > 0 ? Sources : DefaultSources.ToList())
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ public sealed class HotfixCache
|
|||||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||||
private readonly string _path;
|
private readonly string _path;
|
||||||
|
|
||||||
|
// versions already told about, so a borrowed hotfix is reported once and not per request
|
||||||
|
private readonly HashSet<string> _borrowed = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private Dictionary<string, VersionHotfix> _versions = new(StringComparer.OrdinalIgnoreCase);
|
private Dictionary<string, VersionHotfix> _versions = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public HotfixCache(HotfixConfig config, ILogger<HotfixCache> logger)
|
public HotfixCache(HotfixConfig config, ILogger<HotfixCache> logger)
|
||||||
@@ -90,11 +93,8 @@ public sealed class HotfixCache
|
|||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_config.AutoFetch)
|
if (_config.AutoFetch)
|
||||||
{
|
{
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _lock.WaitAsync();
|
await _lock.WaitAsync();
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -105,21 +105,47 @@ public sealed class HotfixCache
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fetched = await FetchAsync(version, dispatchSeed);
|
var fetched = await FetchAsync(version, dispatchSeed);
|
||||||
if (fetched is null)
|
|
||||||
{
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (fetched is not null)
|
||||||
|
{
|
||||||
_versions[version] = fetched;
|
_versions[version] = fetched;
|
||||||
Save();
|
Save();
|
||||||
return fetched;
|
return fetched;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_lock.Release();
|
_lock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cached is { IsUsable: true } ? cached : Nearest(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A beta dispatch stops answering once its window closes, so a client on a build newer
|
||||||
|
// than anything cached can never be looked up. Packaged clients ship their own resources
|
||||||
|
// and only need the urls to be present, so lend them the closest build's.
|
||||||
|
public VersionHotfix? Nearest(string version)
|
||||||
|
{
|
||||||
|
var usable = _versions.Where(v => v.Value.IsUsable).Select(v => v.Key);
|
||||||
|
|
||||||
|
if (!GameVersion.TryFindNearest(usable, version, out var fallback))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_borrowed.Add(version))
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"no hotfix for {Version} and upstream will not serve it; lending {Fallback}'s urls. " +
|
||||||
|
"add the real ones to {Path}, or keep Hotfix:EnableDesignDataUpdate off so the " +
|
||||||
|
"client boots on the resources it shipped with",
|
||||||
|
version, fallback, _path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _versions[fallback];
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<VersionHotfix?> FetchAsync(string version, string dispatchSeed)
|
private async Task<VersionHotfix?> FetchAsync(string version, string dispatchSeed)
|
||||||
{
|
{
|
||||||
var host = SelectHost(version);
|
var host = SelectHost(version);
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ if (-not $clientDir -or -not (Test-Path $clientDir)) {
|
|||||||
|
|
||||||
$launcher = Join-Path $clientDir 'launcher.exe'
|
$launcher = Join-Path $clientDir 'launcher.exe'
|
||||||
$dll = Join-Path $clientDir 'hkrpg.dll'
|
$dll = Join-Path $clientDir 'hkrpg.dll'
|
||||||
$patch = Join-Path $root 'pearl-sr\launcher'
|
$patch = Join-Path $root 'stuff\pearl-sr\launcher'
|
||||||
|
|
||||||
# keep the injector in step with the copy in pearl-sr
|
# keep the injector in step with the copy in pearl-sr
|
||||||
foreach ($file in 'launcher.exe', 'hkrpg.dll') {
|
foreach ($file in 'launcher.exe', 'hkrpg.dll') {
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using CapySR.Common;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CapySR.Tests;
|
||||||
|
|
||||||
|
// A beta dispatch stops answering once its window closes, so a client that updated past
|
||||||
|
// everything in config/versions.json has to be served from the closest build on hand.
|
||||||
|
public class GameVersionTests
|
||||||
|
{
|
||||||
|
private static readonly string[] Cached =
|
||||||
|
[
|
||||||
|
"OSBETAWin4.4.51",
|
||||||
|
"CNBETAWin4.4.53",
|
||||||
|
"CNBETAWin4.5.51",
|
||||||
|
"CNBETAWin4.5.52",
|
||||||
|
];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionsParseIntoAChannelAndNumbers()
|
||||||
|
{
|
||||||
|
Assert.True(GameVersion.TryParse("CNBETAWin4.5.53", out var version));
|
||||||
|
Assert.Equal("CNBETAWin", version.Channel);
|
||||||
|
Assert.Equal([4, 5, 53], version.Parts);
|
||||||
|
|
||||||
|
Assert.False(GameVersion.TryParse("nonsense", out _));
|
||||||
|
Assert.False(GameVersion.TryParse("CNBETAWin4.5.x", out _));
|
||||||
|
Assert.False(GameVersion.TryParse("", out _));
|
||||||
|
Assert.False(GameVersion.TryParse(null, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("CNBETAWin4.5.52", "CNBETAWin4.5.53", -1)]
|
||||||
|
[InlineData("CNBETAWin4.5.53", "CNBETAWin4.5.52", 1)]
|
||||||
|
[InlineData("CNBETAWin4.5.53", "CNBETAWin4.5.53", 0)]
|
||||||
|
[InlineData("CNBETAWin4.9.0", "CNBETAWin4.10.0", -1)]
|
||||||
|
// a missing trailing part counts as zero, so 4.5 and 4.5.0 are the same build
|
||||||
|
[InlineData("CNBETAWin4.5", "CNBETAWin4.5.0", 0)]
|
||||||
|
public void VersionsCompareNumerically(string left, string right, int expected)
|
||||||
|
{
|
||||||
|
Assert.True(GameVersion.TryParse(left, out var a));
|
||||||
|
Assert.True(GameVersion.TryParse(right, out var b));
|
||||||
|
Assert.Equal(expected, Math.Sign(a.CompareTo(b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// the case that matters: the client updated past anything the dispatch will still serve
|
||||||
|
[Fact]
|
||||||
|
public void ANewerClientBorrowsTheClosestOlderBuild()
|
||||||
|
{
|
||||||
|
Assert.True(GameVersion.TryFindNearest(Cached, "CNBETAWin4.5.53", out var nearest));
|
||||||
|
Assert.Equal("CNBETAWin4.5.52", nearest);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChannelsNeverBorrowFromEachOther()
|
||||||
|
{
|
||||||
|
Assert.True(GameVersion.TryFindNearest(Cached, "OSBETAWin4.9.99", out var os));
|
||||||
|
Assert.Equal("OSBETAWin4.4.51", os);
|
||||||
|
|
||||||
|
Assert.False(GameVersion.TryFindNearest(Cached, "CNPRODWin4.5.53", out _));
|
||||||
|
Assert.False(GameVersion.TryFindNearest(Cached, "nonsense", out _));
|
||||||
|
Assert.False(GameVersion.TryFindNearest([], "CNBETAWin4.5.53", out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing older to fall back on, so the newest known build beats having no urls at all
|
||||||
|
[Fact]
|
||||||
|
public void AnOlderClientStillGetsUrls()
|
||||||
|
{
|
||||||
|
Assert.True(GameVersion.TryFindNearest(Cached, "CNBETAWin3.0.0", out var nearest));
|
||||||
|
Assert.Equal("CNBETAWin4.5.52", nearest);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnExactMatchIsPreferredOverEverythingElse()
|
||||||
|
{
|
||||||
|
Assert.True(GameVersion.TryFindNearest(Cached, "CNBETAWin4.5.51", out var nearest));
|
||||||
|
Assert.Equal("CNBETAWin4.5.51", nearest);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ if (repoRoot is null)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sourceProto = Path.Combine(repoRoot, "TurnBaseGameProto", "Raw", "StarRail.proto");
|
var sourceProto = Path.Combine(repoRoot, "stuff", "TurnBaseGameProto", "Raw", "StarRail.proto");
|
||||||
var packetIds = Path.Combine(repoRoot, "TurnBaseGameProto", "Raw", "packetIds.txt");
|
var packetIds = Path.Combine(repoRoot, "stuff", "TurnBaseGameProto", "Raw", "packetIds.txt");
|
||||||
var protocolProject = Path.Combine(repoRoot, "src", "CapySR.Protocol");
|
var protocolProject = Path.Combine(repoRoot, "src", "CapySR.Protocol");
|
||||||
var cleanedProtoDir = Path.Combine(protocolProject, "Proto");
|
var cleanedProtoDir = Path.Combine(protocolProject, "Proto");
|
||||||
var cleanedProto = Path.Combine(cleanedProtoDir, "StarRail.proto");
|
var cleanedProto = Path.Combine(cleanedProtoDir, "StarRail.proto");
|
||||||
|
|||||||
Reference in new issue
Block a user