Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,6 @@ src/Update.exe

# XLCore build directory
src/XIVLauncher.Core/build

# ApkalluCaller (Rust) build output staged into XL Resources
src/XIVLauncher/Resources/version.dll
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "src/FfxivArgLauncher"]
path = src/FfxivArgLauncher
url = https://github.com/ottercorp/FfxivArgLauncher
[submodule "src/ApkalluCaller"]
path = src/ApkalluCaller
url = https://github.com/ottercorp/ApkalluCaller
1 change: 1 addition & 0 deletions src/ApkalluCaller
Submodule ApkalluCaller added at 665a9b
3 changes: 2 additions & 1 deletion src/XIVLauncher.Common/Game/SdoLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static List<GuiLoginType> Get(bool showWeGameToken = false)
new GuiLoginType { LoginType = LoginType.SdoStatic, DisplayName = "密码登录" },
new GuiLoginType { LoginType = LoginType.WeGameSid, DisplayName = "WeGame SID"}
};
if (showWeGameToken)
if (true)
{
types.Add(new GuiLoginType { LoginType = LoginType.WeGameToken, DisplayName = "WeGame抓包" });
}
Expand All @@ -54,6 +54,7 @@ public enum LoginType
SdoQrCode,
WeGameToken,
WeGameSid,
// 仅在内部使用,不出现在GUI上
AutoLoginSession
}

Expand Down
183 changes: 183 additions & 0 deletions src/XIVLauncher.Common/Game/WeGame/WeGameLoginCapturer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using StreamJsonRpc;

namespace XIVLauncher.Common.Game.WeGame
{
public sealed class WeGameLoginCapturer
{
private const string PipeName = "ApkalluCaller";
private const int FfxivWeGameGameId = 2000340;

public async Task<(string userid, string token)> CaptureAsync(
string sdologinDir,
CancellationToken ct,
IProgress<string> progress)
{
progress?.Report("正在部署 version.dll...");
DeployVersionDll(sdologinDir);

progress?.Report("正在等待 WeGame 登录...");

var tcs = new TaskCompletionSource<(string, string)>(
TaskCreationOptions.RunContinuationsAsynchronously);

NamedPipeServerStream pipe;
try
{
pipe = new NamedPipeServerStream(
PipeName,
PipeDirection.InOut,
maxNumberOfServerInstances: 1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);
}
catch (IOException ex)
{
throw new WeGameCapturePipeBusyException(ex);
}

await using (pipe.ConfigureAwait(false))
await using (ct.Register(() =>
{
tcs.TrySetCanceled(ct);
try { pipe.Dispose(); } catch { /* swallow on disposal race */ }
}).ConfigureAwait(false))
{
TryLaunchWeGame(progress);

await pipe.WaitForConnectionAsync(ct).ConfigureAwait(false);

var formatter = new SystemTextJsonFormatter();
var handler = new NewLineDelimitedMessageHandler(pipe, pipe, formatter);
using var rpc = new JsonRpc(handler, new RpcHandler(tcs));
rpc.StartListening();

var result = await tcs.Task.ConfigureAwait(false);
progress?.Report("已捕获登录信息, 正在登录盛趣...");

// Wait for the client to receive the response and close its end of
// the pipe before we dispose anything; otherwise rpc.Dispose() races
// the in-flight response write and the Rust side reads 0 bytes
// ("empty response (peer closed)"). 2s is generous — sdologin closes
// its IpcStream immediately after reading the response.
try
{
await rpc.Completion.WaitAsync(TimeSpan.FromSeconds(2), ct).ConfigureAwait(false);
}
catch
{
// Timeout, cancellation, or rpc fault: response was almost
// certainly flushed by now; fall through to dispose.
}

return result;
}
}

private static void TryLaunchWeGame(IProgress<string> progress)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = $"wegame://StartFor={FfxivWeGameGameId}",
UseShellExecute = true,
});
progress?.Report("已请求启动 WeGame, 请在 WeGame 中登录最终幻想 14...");
}
catch (Exception ex)
{
Log.Warning(ex, "wegame:// 唤起失败");
progress?.Report("自动唤起 WeGame 失败, 请手动启动 WeGame 并登录最终幻想 14...");
}
}

public static void DeployVersionDll(string sdologinDir)
{
var src = Path.Combine(Paths.ResourcesPath, "version.dll");

if (!File.Exists(src))
throw new FileNotFoundException("找不到 version.dll 资源", src);

var dst = Path.Combine(sdologinDir, "version.dll");

if (File.Exists(dst) && HashEquals(src, dst))
{
Log.Information("version.dll 已最新, 跳过部署");
return;
}

try
{
File.Copy(src, dst, overwrite: true);
Log.Information("version.dll 已部署: {Dst}", dst);
}
catch (UnauthorizedAccessException ex)
{
throw new VersionDllPermissionDeniedException(src, dst, ex);
}
catch (IOException ex) when (IsAccessDenied(ex))
{
throw new VersionDllPermissionDeniedException(src, dst, ex);
}
}

public static bool HashEquals(string a, string b)
{
using var sha = SHA256.Create();
using var fa = File.OpenRead(a);
using var fb = File.OpenRead(b);
return sha.ComputeHash(fa).SequenceEqual(sha.ComputeHash(fb));
}

private static bool IsAccessDenied(IOException ex)
{
const int errAccessDenied = 5;
const int errSharingViolation = 32;
var code = ex.HResult & 0xFFFF;
return code == errAccessDenied || code == errSharingViolation;
}

private sealed class RpcHandler
{
private readonly TaskCompletionSource<(string, string)> _tcs;
public RpcHandler(TaskCompletionSource<(string, string)> tcs) => _tcs = tcs;

[JsonRpcMethod("login.captured")]
public string LoginCaptured(string userid, string token)
{
_tcs.TrySetResult((userid, token));
return "ok";
}
}
}

public sealed class VersionDllPermissionDeniedException : Exception
{
public string Source { get; }
public string Destination { get; }

public VersionDllPermissionDeniedException(string src, string dst, Exception inner)
: base($"无权限写入 {dst}", inner)
{
Source = src;
Destination = dst;
}
}

public sealed class WeGameCapturePipeBusyException : Exception
{
public WeGameCapturePipeBusyException(Exception inner)
: base("命名管道 ApkalluCaller 已被另一个进程占用", inner)
{
}
}
}
46 changes: 46 additions & 0 deletions src/XIVLauncher.Common/Game/WeGame/WeGamePathValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.IO;
using Newtonsoft.Json.Linq;

namespace XIVLauncher.Common.Game.WeGame
{
public static class WeGamePathValidator
{
public const int FfxivWeGameGameId = 2000340;

public static bool IsValidSdologinDir(string sdologinDir)
{
if (string.IsNullOrEmpty(sdologinDir)) return false;
if (!Directory.Exists(sdologinDir)) return false;
if (!File.Exists(Path.Combine(sdologinDir, "sdologin.exe"))) return false;

try
{
var root = Path.GetFullPath(Path.Combine(sdologinDir, "..", ".."));
return IsValidGameRoot(root);
}
catch
{
return false;
}
}

public static bool IsValidGameRoot(string root)
{
if (string.IsNullOrEmpty(root)) return false;
var marker = Path.Combine(root, "rail_files", "rail_game_identify.json");
if (!File.Exists(marker)) return false;
try
{
var json = JObject.Parse(File.ReadAllText(marker));
return (int?)json["game_id"] == FfxivWeGameGameId;
}
catch
{
return false;
}
}

public static string DeriveSdologinDir(string gameRoot)
=> Path.Combine(gameRoot, "sdo", "sdologin");
}
}
1 change: 1 addition & 0 deletions src/XIVLauncher.Common/XIVLauncher.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<PackageReference Include="EmbedIO" Version="3.5.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="StreamJsonRpc" Version="2.24.84" />
<PackageReference Include="Serilog.Enrichers.Sensitive" Version="1.7.2" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
Expand Down
1 change: 1 addition & 0 deletions src/XIVLauncher/Settings/ILauncherSettingsV3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public interface ILauncherSettingsV3
bool? EnableBeta { get; set; }
bool? HasAgreeWeGameUsage { get; set; }
bool? ShowWeGameTokenLogin { get; set; }
string WeGameLauncherPath { get; set; }
CredType? CredType { get; set; }

bool? EnableVerboseLog { get; set; }
Expand Down
32 changes: 28 additions & 4 deletions src/XIVLauncher/Windows/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,19 @@ public void Initialize()
MainWindowViewModel.AfterLoginAction.Start
);
}
else if (savedAccount.AccountType == XivAccountType.WeGame)
{
// WeGameToken 不能复用 AutoLoginSessionKey, 必须重新走 LoginByWeGameToken。
// 传空密码, 让 TryLogin 内部从 savedAccount.Password 解出 token。
Model.TryLogin(
LoginType.WeGameToken,
savedAccount.LoginAccount,
null,
Model.IsFastLogin,
Model.IsReadWegameInfo,
MainWindowViewModel.AfterLoginAction.Start
);
}
else
{
Model.TryLogin(
Expand Down Expand Up @@ -623,7 +636,10 @@ private void SwitchAccount(XivAccount account, bool saveAsCurrent)
break;
case XivAccountType.WeGame:
LoginTypeSelection.SelectedValue = LoginType.WeGameToken;
LoginPassword.Password = MainWindowViewModel.PresudoPassword;
if (account.Password is not null)
{
LoginPassword.Password = MainWindowViewModel.PresudoPassword;
}
break;
case XivAccountType.WeGameSid:
LoginTypeSelection.SelectedValue = LoginType.WeGameSid;
Expand Down Expand Up @@ -731,6 +747,8 @@ private void LoginTypeSelection_OnSelectionChanged(object sender, SelectionChang
FastLoginCheckBox.Visibility = Visibility.Visible;
ReadWeGameInfoCheckBox.Visibility = Visibility.Collapsed;
FastLoginCheckBox.Content = "快速登录";
ReadWeGameInfoCheckBox.Content = "读取登录信息";
ReadWeGameInfoCheckBox.ToolTip = "从WeGame版FFXIV中读取登录信息";
LoginPassword.Password = string.Empty;
HintAssist.SetHint(this.LoginUsername, "盛趣账号");
HintAssist.SetHint(this.LoginPassword, "密码");
Expand All @@ -750,9 +768,15 @@ private void LoginTypeSelection_OnSelectionChanged(object sender, SelectionChang
//FastLoginCheckBox.Visibility = Visibility.Collapsed;
break;
case LoginType.WeGameToken:
LoginPassword.Visibility = Visibility.Visible;
HintAssist.SetHint(this.LoginUsername, "SndaId");
HintAssist.SetHint(this.LoginPassword, "抓包Token");
// WeGameToken 全自动抓包, 不再支持手填 token; 用户名留空时也会从抓包结果回填。
LoginPassword.Visibility = Visibility.Collapsed;
HintAssist.SetHint(this.LoginUsername, "SndaId (可留空, 自动抓取)");
// 跟 SdoStatic 对齐: 勾选才把抓到的 token 持久化, 下次免抓包。
FastLoginCheckBox.Content = "保存密码";
// 复用同一个 checkbox: WeGameSid 用它"读取登录信息", WeGameToken 用它"强制重新抓包"。
ReadWeGameInfoCheckBox.Visibility = Visibility.Visible;
ReadWeGameInfoCheckBox.Content = "强制重新抓包";
ReadWeGameInfoCheckBox.ToolTip = "勾选后忽略已保存的 token, 强制重新启动 sdologin 抓包";
break;
case LoginType.WeGameSid:
FastLoginCheckBox.Visibility = Visibility.Collapsed;
Expand Down
Loading
Loading