Compare commits

...

3 Commits

27 changed files with 1786 additions and 76 deletions

View File

@ -1,6 +1,6 @@
---
name: th1-base
description: TH1 project baseline engineering guide for Unity client changes: assembly boundaries, HybridCLR hotfix rules, YooAsset/AssetBundle resource rules, MemoryPack/AOT serialization safety, build packaging flow, and routing to domain skills. Use before broad TH1 Unity code/resource/build changes, especially when touching Scripts, asmdefs, HybridCLR, hotfix DLLs, StreamingAssets, BundleResources, Resources.Load replacement, YooAsset, MemoryPack, generated config, build panels, or package verification.
description: TH1 project baseline engineering guide for Unity client changes: assembly boundaries, HybridCLR hotfix rules, YooAsset/AssetBundle resource rules, BundleResources DataAssets -> Export packaging sync, MemoryPack/AOT serialization safety, unified PC/iOS build packaging flow, and routing to domain skills. Use before broad TH1 Unity code/resource/build changes, especially when touching Scripts, asmdefs, HybridCLR, hotfix DLLs, StreamingAssets, BundleResources, Resources.Load replacement, YooAsset, MemoryPack, generated config, build panels, or package verification.
---
# TH1 Base Engineering
@ -107,6 +107,7 @@ Rules:
Resources are moving to AB-first loading:
- Use `Assets/BundleResources` as the packaged resource root.
- TH1's default YooAsset collector should collect the whole `Assets/BundleResources` root and use `TH1AddressByBundleResourcesPath`, so an asset like `Assets/BundleResources/Export/VersionConfig.asset` is addressed as `Export/VersionConfig.asset`.
- Prefer stable logical addresses. Do not make gameplay code depend on platform-specific bundle paths.
- Replace `Resources.Load` through `TH1Resource.ResourceLoader`/resource cache wrappers, not scattered direct YooAsset calls.
- Editor may run simulate mode; PC/iOS packages should run actual built-in AB package logic.
@ -120,18 +121,26 @@ Resources are moving to AB-first loading:
## Build And Packaging Flow
For migration-era PC IL2CPP smoke builds, use the project one-click flow where possible:
For PC/iOS packages, prefer `Tools/TH1/一体化出包工具` / `TH1UnifiedBuildWindow` where possible.
Current one-click flow should be visible and stage-based:
1. Configure target platform/build settings.
2. Configure HybridCLR.
3. Run HybridCLR `GenerateAll`.
4. Build hotfix DLL and copy hotfix/AOT metadata to `StreamingAssets`.
5. Build YooAsset built-in `DefaultPackage`.
6. Build Windows IL2CPP player.
7. Launch player and scan the log for startup red errors.
2. Apply selected `VersionConfig` to `Assets/BundleResources/DataAssets/VersionConfig.asset`, `PlayerSettings.bundleVersion`, scripting defines, package type, and OPS obfuscation target.
3. Run multilingual export/import when packaging unless the user explicitly disables it. This copies/transforms `Assets/BundleResources/DataAssets/*` into `Assets/BundleResources/Export/*`, updates `Multilingual.asset`, and generates `MatchLevelData/ExportLevelData.bytes`.
4. Configure HybridCLR.
5. Configure YooAsset collector.
6. Run HybridCLR `GenerateAll`.
7. Build hotfix DLL and copy hotfix/AOT metadata to `StreamingAssets`.
8. Build YooAsset built-in `DefaultPackage`.
9. Check migration/build blockers.
10. Build the PC or iOS player.
The window must show each phase and stop on the first phase failure. Do not rely only on a final Unity popup when the flow spans version config, multilingual export, HybridCLR, AB, and player build.
Manual Unity packaging gotchas:
- If runtime version/localization/config looks stale, compare `Assets/BundleResources/DataAssets/*` against `Assets/BundleResources/Export/*`; the game usually reads `Export/...`, not raw `DataAssets/...`.
- After scene list changes, use full `Build`/`Clean Build`; do not use `Build Scripts Only`.
- If the one-click build panel reports success while AOT metadata is missing, trust the artifact check, not the old status text. Required AOT metadata must exist under `Assets/StreamingAssets/HybridCLR/AOTAssemblies`.
- If you only need a runnable PC package, keep `Create Visual Studio Solution` off unless explicitly debugging the generated solution.

View File

@ -1,6 +1,6 @@
---
name: th1-ios-migration
description: "TH1 project-specific iOS migration guide for same-mainline dual-platform support: HybridCLR hot update foundation, iOS/IL2CPP compile isolation, Steamworks/Steam SDK platform abstraction without Steam regressions, touch input adaptation, YooAsset AssetBundle/resource migration, iOS build settings, and verification that existing online Steam builds and gameplay behavior remain unaffected. Use whenever Codex works on TH1 iOS packaging, mobile porting, HybridCLR, YooAsset AB/resource loading, removing direct Steamworks references for iOS, platform services, touch controls, or build pipeline changes that must preserve the current Steam version."
description: "TH1 project-specific iOS migration guide for same-mainline dual-platform support: HybridCLR hot update foundation, iOS/IL2CPP compile isolation, Steamworks/Steam SDK platform abstraction without Steam regressions, touch input adaptation, YooAsset AssetBundle/resource migration, BundleResources DataAssets -> Export packaging sync, iOS build settings, unified PC/iOS build window flow, and verification that existing online Steam builds and gameplay behavior remain unaffected. Use whenever Codex works on TH1 iOS packaging, mobile porting, HybridCLR, YooAsset AB/resource loading, removing direct Steamworks references for iOS, platform services, touch controls, or build pipeline changes that must preserve the current Steam version."
---
# TH1 iOS Migration
@ -173,6 +173,14 @@ Goal: move toward AB/resource hot update without destabilizing the Steam build.
Use the existing YooAsset package and `AssetBundleCollectorSetting.asset` unless there is a clear reason not to.
Current TH1 built-in resource shape:
- Packaged resource root is `Assets/BundleResources`.
- `TH1YooAssetBuildTools.ConfigureDefaultPackageCollector()` should collect the whole root for the default package.
- `TH1AddressByBundleResourcesPath` strips `Assets/BundleResources/`, so `Assets/BundleResources/Export/Multilingual.asset` becomes address `Export/Multilingual.asset`.
- `TH1Resource.ResourceLoader.Load<T>("Export/VersionConfig")` and similar calls normalize addresses and try extension candidates. Do not change runtime callers to platform-specific paths when the collector/address rule is the real issue.
- Runtime config/localization/table data usually reads `Export/...`; raw `DataAssets/...` is the authoring source copied/transformed by the multilingual export/import flow.
Recommended sequence:
1. Finish a real `ResourceManager` wrapper around YooAsset package initialization.
@ -185,7 +193,9 @@ Recommended sequence:
Resource rules:
- Keep stable logical addresses. Do not make gameplay code depend on platform-specific paths.
- Do not modify export-flow outputs such as `Unity/Assets/Resources/Export/*`, `Tools/Multilingual.xlsx`, or `Tools/MultilingualTxt.txt` unless the user asked for export/import changes.
- Do not modify export-flow outputs such as `Unity/Assets/BundleResources/Export/*`, `Tools/Multilingual.xlsx`, or `Tools/MultilingualTxt.txt` unless the user asked for export/import changes.
- In the current migration, export-flow outputs live under `Assets/BundleResources/Export/*`. If a package shows the wrong version, stale text, or stale table config, first check whether `DataAssets -> Export` was refreshed before AB build.
- Before a release/debug package, run or explicitly skip with confirmation the multilingual export/import flow (`Tools/一键导出导回` or the checkbox in `TH1UnifiedBuildWindow`). It refreshes `Export/*`, `Multilingual.asset`, Excel/TXT intermediate files, and `MatchLevelData/ExportLevelData.bytes`.
- Use iOS-specific texture/audio compression settings and bundle output. Do not reuse PC texture assumptions blindly.
- Treat generated config/DataAsset loading as compatibility-sensitive; preserve existing table and localization behavior.
- For Unity 2021+ / Unity 2022 with YooAsset 2.1.1, prefer SBP (`ScriptableBuildPipeline`) for built-in package builds. YooAsset's BBP path uses Unity's old `BuildPipeline.BuildAssetBundles` call and can trigger the internal assertion `m_InstanceIDToAssetBundleIndex.count(id) > 0`.
@ -209,6 +219,20 @@ Maintain one main branch with separate build profiles:
- iOS build: iOS, IL2CPP, mobile platform services, no Steamworks compile dependency, touch enabled, iOS bundles.
- Hot update build: platform-specific HybridCLR DLL/AOT metadata and YooAsset manifests.
Prefer `Tools/TH1/一体化出包工具` for manual PC/iOS packaging. The one-click path should be staged and stop on first failure:
1. Apply version/platform/package defines and OPS obfuscation target.
2. Optional but recommended multilingual export/import (`DataAssets -> Export`).
3. Configure HybridCLR.
4. Configure YooAsset collector.
5. HybridCLR `GenerateAll`.
6. Build hotfix DLL/AOT metadata.
7. Build YooAsset built-in AB.
8. Check build blockers.
9. Build Player.
The confirmation dialog should show the selected version and whether the multilingual export/import step is enabled.
Release branches may exist for stabilization only:
- `release/steam-*` and `release/ios-*` may freeze and cherry-pick fixes.

View File

@ -1,4 +1,4 @@
interface:
display_name: "TH1 iOS Migration"
short_description: "HybridCLR, iOS isolation, touch, YooAsset"
default_prompt: "Use $th1-ios-migration when making TH1 iOS migration changes that must keep the Steam build and existing gameplay behavior intact."
short_description: "HybridCLR、iOS隔离、YooAsset与统一出包"
default_prompt: "Use $th1-ios-migration when making TH1 iOS migration or PC/iOS packaging changes that must keep the Steam build and existing gameplay behavior intact."

View File

@ -1,6 +1,6 @@
---
name: th1-multilingual
description: TH1 project-specific multilingual/localization guide for Unity editor export/import, Multilingual.asset, Multilingual.xlsx, MultilingualTxt.txt, special-term syntax, ordered embedded references, duplicate ID prevention, active text scanning, Excel round-tripping, and translation data debugging. Use whenever Codex works on TH1 多语言导表/导回, localization Excel issues, duplicated rows/IDs, special-term parsing, ordered marker bugs, MultilingualEditorWindow, MultilingualData, or any bug that may create new localization IDs unexpectedly.
description: TH1 project-specific multilingual/localization guide for Unity editor export/import, BundleResources DataAssets -> Export runtime asset sync, Multilingual.asset, Multilingual.xlsx, MultilingualTxt.txt, special-term syntax, ordered embedded references, duplicate ID prevention, active text scanning, Excel round-tripping, and translation data debugging. Use whenever Codex works on TH1 多语言导表/导回, localization Excel issues, duplicated rows/IDs, special-term parsing, ordered marker bugs, MultilingualEditorWindow, MultilingualData, package/export stale version or text issues, or any bug that may create new localization IDs unexpectedly.
---
# TH1 Multilingual
@ -26,7 +26,9 @@ Translation length is part of the project quality bar. Keep target-language text
- `Tools/ExportStringToExcel.py`
- `Tools/Multilingual.xlsx`
- `Tools/MultilingualTxt.txt`
- `Unity/Assets/Resources/Export/Multilingual.asset`
- `Unity/Assets/BundleResources/Export/Multilingual.asset`
- `Unity/Assets/BundleResources/DataAssets/*`
- `Unity/Assets/BundleResources/Export/*`
For the full data-flow contract, read `references/pipeline.md`.
For duplicate IDs, bad markers, and validation commands, read `references/diagnostics.md`.
@ -62,6 +64,14 @@ Asset internal form:
7. Only allocate `_idIndex` when the canonical string is genuinely new.
8. Write Excel-visible text through `GetMultilingualStrEditor()`, which expands internal IDs back to readable terms.
In the current BundleResources/YooAsset flow, export is also the authoring-to-runtime sync step:
- `Assets/BundleResources/DataAssets/*` is the authoring source for many ScriptableObject configs.
- `AssetExportToExcelInternal()` instantiates each DataAsset, traverses/transforms multilingual strings, and writes the runtime copy to `Assets/BundleResources/Export/{name}.asset`.
- Runtime table/config/localization code usually loads `Export/...` through `TH1Resource.ResourceLoader`, not raw `DataAssets/...`.
- `ExportMatchLevelData()` writes `Assets/BundleResources/MatchLevelData/ExportLevelData.bytes`; runtime match config uses this outside editor play mode.
- Before PC/iOS packaging, run `Tools/一键导出导回` or enable the `TH1UnifiedBuildWindow` checkbox for multilingual export/import before YooAsset AB build. If this is skipped, the built player can show a new title/bundle version while CrashSight/config/localization still report stale `Export` data.
## Import Workflow
1. Convert Excel to `MultilingualTxt.txt` through `Tools/PrintExcelString.py`.

View File

@ -1,4 +1,4 @@
interface:
display_name: "TH1 Multilingual"
short_description: "TH1 Unity 多语言导表导回特殊词和重复ID排查"
default_prompt: "Use TH1 multilingual rules to inspect, fix, or explain localization export/import issues."
short_description: "TH1 多语言导表导回、DataAssets到Export同步、重复ID排查"
default_prompt: "Use TH1 multilingual rules to inspect, fix, or explain localization export/import and DataAssets-to-Export sync issues."

View File

@ -38,7 +38,7 @@ import sys, io
from pathlib import Path
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
ids = {'17228', '19977'}
text = Path('Unity/Assets/Resources/Export/Multilingual.asset').read_text(encoding='utf-8')
text = Path('Unity/Assets/BundleResources/Export/Multilingual.asset').read_text(encoding='utf-8')
lines = text.splitlines()
for idx, line in enumerate(lines):
if line.strip() in [f'- ID: {i}' for i in ids]:
@ -59,13 +59,13 @@ Interpretation:
Search an ID in exported assets to see who currently references it:
```powershell
rg -n "Desc: 19977|ActionName: 19977|TechDesc: 19977| 19977" Unity/Assets/Resources/Export -S
rg -n "Desc: 19977|ActionName: 19977|TechDesc: 19977| 19977" Unity/Assets/BundleResources/Export -S
```
For field-level reference context:
```powershell
rg -n "19977" Unity/Assets/Resources/Export/ActionDataAssets.asset -C 20
rg -n "19977" Unity/Assets/BundleResources/Export/ActionDataAssets.asset -C 20
```
If a new duplicate ID is already written into a DataAssets export, rerunning export after fixing canonicalization should normally reassign the source field back to the preserved ID.

View File

@ -14,8 +14,12 @@
- Embedded string resolve/unresolve.
- Language fallback.
- `MultilingualItem`.
- `Unity/Assets/Resources/Export/Multilingual.asset`
- `Unity/Assets/BundleResources/Export/Multilingual.asset`
- Canonical ScriptableObject data.
- `Unity/Assets/BundleResources/DataAssets/*`
- Authoring ScriptableObject configs scanned during export.
- `Unity/Assets/BundleResources/Export/*`
- Runtime ScriptableObject copies produced by export.
- `Tools/MultilingualTxt.txt`
- Intermediate delimiter file.
- `Tools/Multilingual.xlsx`
@ -85,8 +89,8 @@ The same row can therefore have different representations at different phases. A
- Deduplicate by canonical raw `item.ZH`.
2. Initialize `_zhStrDict` from canonical `item.ZH`.
3. Scan active `TextMeshProUGUI` from scene `UICanvas`.
4. Scan prefabs under `Assets/Resources/Prefab/`.
5. Scan ScriptableObjects under `Assets/Resources/DataAssets/`.
4. Scan prefabs under `Assets/BundleResources/Prefab/`.
5. Scan ScriptableObjects under `Assets/BundleResources/DataAssets/`.
6. For each source string:
- Trim and normalize newlines.
- Optionally `TransformString`.
@ -98,6 +102,17 @@ The same row can therefore have different representations at different phases. A
8. Write `MultilingualTxt.txt` with `GetMultilingualStrEditor`, which expands IDs for Excel.
9. Run `ExportStringToExcel.py`.
## BundleResources / Packaging Notes
The multilingual export flow also refreshes runtime config assets for AB builds:
- `DataAssets/*` is authoring data.
- `SaveExportAsset(name, newAsset)` writes transformed runtime assets to `Assets/BundleResources/Export/{name}.asset`.
- `ConfigManager`, `Table`, `MultilingualManager`, achievements, AI config, and similar runtime paths generally load `Export/...` through `TH1Resource.ResourceLoader`.
- `ExportMatchLevelData()` writes `Assets/BundleResources/MatchLevelData/ExportLevelData.bytes`; runtime match config uses `MatchLevelData/ExportLevelData` outside editor play mode.
- `TH1YooAssetBuildTools` collects the whole `Assets/BundleResources` root, and `TH1AddressByBundleResourcesPath` strips that prefix. `Assets/BundleResources/Export/VersionConfig.asset` is therefore addressed as `Export/VersionConfig.asset`.
- Before building PC/iOS AB/player packages, run `Tools/一键导出导回` or the `TH1UnifiedBuildWindow` multilingual export/import checkbox. Otherwise a player can contain stale `Export` assets even when `DataAssets` and `PlayerSettings.bundleVersion` were changed.
## Import Phases
1. Run `PrintExcelString.py`.

View File

@ -15,7 +15,6 @@ using Logic.Audio;
using Logic.CrashSight;
using Logic.Skill;
using RuntimeData;
using Steamworks;
using TH1_Core.Events;
using TH1_Core.Managers;
using TH1_Logic.Core;

View File

@ -92,28 +92,15 @@ namespace Logic.Editor
[MenuItem("Tools/一键导出导回")]
public static void OneClickExportAndImport()
{
EditorUtility.DisplayProgressBar("一键导出导回", "正在导出到 Excel...", 0.3f);
try
{
var window = new MultilingualEditorWindow();
window.InitializeAsset();
// 步骤1: 执行导出
window.AssetExportToExcelInternal();
Debug.Log("[一键导出导回] 导出 Excel 成功");
EditorUtility.DisplayProgressBar("一键导出导回", "正在从 Excel 导回...", 0.7f);
// 步骤2: 执行导回
window.ExcelExportToAssetInternal();
Debug.Log("[一键导出导回] Excel 导回成功");
RunOneClickExportAndImportForBuild((message, progress) =>
EditorUtility.DisplayProgressBar("一键导出导回", message, progress));
EditorUtility.DisplayDialog("成功", "一键导出导回完成!", "确定");
}
catch (Exception ex)
{
Debug.LogError($"[一键导出导回] 失败: {ex.Message}");
Debug.LogError($"[一键导出导回] 失败: {ex}");
EditorUtility.DisplayDialog("错误", $"操作失败: {ex.Message}", "确定");
}
finally
@ -122,6 +109,24 @@ namespace Logic.Editor
}
}
public static void RunOneClickExportAndImportForBuild(System.Action<string, float> progress = null)
{
var window = new MultilingualEditorWindow();
progress?.Invoke("正在初始化多语言资源...", 0.05f);
window.InitializeAsset();
progress?.Invoke("正在导出 DataAssets/UI/Prefab 到 Export 并生成 Excel...", 0.35f);
window.AssetExportToExcelInternal();
Debug.Log("[一键导出导回] 导出 Excel 成功");
progress?.Invoke("正在从 Excel 导回 Multilingual.asset...", 0.75f);
window.ExcelExportToAssetInternal();
Debug.Log("[一键导出导回] Excel 导回成功");
progress?.Invoke("多语言导出导回完成。", 1f);
}
private void InitializeAsset()
{
var path = "Assets/BundleResources/Export/Multilingual.asset";

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b7ee1d9bbf743e898610ee7468260ff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -34,6 +34,12 @@ namespace TH1_Logic.Net
// 获取在线好友列表
public Dictionary<ulong, MemberInfo> GetOnlineFriendsDict();
// 获取可展示房间列表
public List<LobbyListInfo> GetLobbyListInfos();
// 刷新房间列表缓存
public void RefreshLobbyListInfo();
// 是否初始化完毕
public bool IsInitialized();
@ -43,6 +49,9 @@ namespace TH1_Logic.Net
// 举报房间
public bool ReportLobby(LobbyListInfo lobbyInfo);
// 处理收到的房间举报消息
public void OnReceivedLobbyReport(LobbyReportMessage message);
// 踢出成员
public void KickMember(ulong memberId);
@ -68,6 +77,9 @@ namespace TH1_Logic.Net
// 获取房间所有成员信息
public MemberInfo GetMemberInfo(ulong steamID);
// 获取平台展示名
public string GetSelfDisplayName();
// 获取成员数量
public int GetMemberCount();
@ -101,14 +113,24 @@ namespace TH1_Logic.Net
// 发送P2P消息
public bool SendMessageToPeer(ulong member, byte[] data, bool reliable = true);
// 不要求已建立房间内 P2P 连接的直发消息,用于邀请/举报等房间外消息
public bool SendMessageToPeerWithoutConnection(ulong member, byte[] data, bool reliable = true);
// 广播P2P消息
public bool BroadcastMessage(byte[] data, bool reliable = true);
// 检查和某成员的 P2P 连接是否已建立
public bool IsPeerConnected(ulong memberId);
// 获取当前房间ID用于分享
public ulong GetShareableLobbyId();
// 获取自己是否为隐私状态
public bool IsSelfStatusInvisibleOrOffline();
// 平台房间名工具
public string GetDefaultRoomName(string ownerName);
public string FilterRoomName(string roomName);
}
@ -193,7 +215,16 @@ namespace TH1_Logic.Net
public Dictionary<ulong, MemberInfo> GetOnlineFriendsDict()
{
return null;
return new Dictionary<ulong, MemberInfo>();
}
public List<LobbyListInfo> GetLobbyListInfos()
{
return new List<LobbyListInfo>();
}
public void RefreshLobbyListInfo()
{
}
public void CreateLobby(int maxMembers = 4,bool isPublic = true, string password = "", string roomName = "")
@ -266,6 +297,16 @@ namespace TH1_Logic.Net
return false;
}
public bool SendMessageToPeerWithoutConnection(ulong member, byte[] data, bool reliable = true)
{
return false;
}
public bool IsPeerConnected(ulong memberId)
{
return false;
}
public ulong GetShareableLobbyId()
{
return 0;
@ -275,5 +316,24 @@ namespace TH1_Logic.Net
{
return false;
}
public void OnReceivedLobbyReport(LobbyReportMessage message)
{
}
public string GetSelfDisplayName()
{
return string.Empty;
}
public string GetDefaultRoomName(string ownerName)
{
return string.IsNullOrWhiteSpace(ownerName) ? "Default" : ownerName;
}
public string FilterRoomName(string roomName)
{
return roomName ?? string.Empty;
}
}
}

View File

@ -6,7 +6,7 @@
*/
using TH1_Logic.Steam;
using System;
namespace TH1_Logic.Net
{
@ -18,12 +18,23 @@ namespace TH1_Logic.Net
public void Init()
{
#if UNITY_EDITOR || STEAM_CHANNEL
Lobby = new SteamLobbyManager();
#if STEAM_CHANNEL || STEAMWORKS_NET
Lobby = CreateSteamLobby() ?? new LobbyBase();
#else
Lobby = new LobbyBase();
#endif
Lobby.Init();
}
#if STEAM_CHANNEL || STEAMWORKS_NET
private static ILobby CreateSteamLobby()
{
var lobbyType = Type.GetType("TH1_Logic.Steam.SteamLobbyManager")
?? Type.GetType("TH1_Logic.Steam.SteamLobbyManager, TH1.Hotfix")
?? Type.GetType("TH1_Logic.Steam.SteamLobbyManager, TH1.Steam.Runtime");
if (lobbyType == null) return null;
return Activator.CreateInstance(lobbyType) as ILobby;
}
#endif
}
}
}

View File

@ -3,7 +3,9 @@ using System.Threading.Tasks;
using Logic.CrashSight;
using MemoryPack;
using RuntimeData;
#if STEAM_CHANNEL || STEAMWORKS_NET
using Steamworks;
#endif
using TH1_Logic.Collect;
using TH1_Logic.Config;
using TH1_Logic.Core;
@ -245,6 +247,7 @@ namespace TH1_Logic.Oss
private static bool TryGetSteamId(out string steamId)
{
steamId = "";
#if STEAM_CHANNEL || STEAMWORKS_NET
try
{
if (!SteamUser.BLoggedOn()) return false;
@ -259,11 +262,15 @@ namespace TH1_Logic.Oss
{
return false;
}
#else
return false;
#endif
}
private static bool TryGetAuthTicket(out string authTicket)
{
authTicket = null;
#if STEAM_CHANNEL || STEAMWORKS_NET
try
{
var ticket = new byte[1024];
@ -283,6 +290,9 @@ namespace TH1_Logic.Oss
LogSystem.LogWarning($"Steam auth ticket unavailable: {ex.Message}");
return false;
}
#else
return false;
#endif
}
}
}

View File

@ -2,7 +2,9 @@ using System;
using System.Text;
using System.Threading.Tasks;
using Logic.CrashSight;
#if STEAM_CHANNEL || STEAMWORKS_NET
using Steamworks;
#endif
using TH1_Logic.Config;
using UnityEngine;
using UnityEngine.Networking;
@ -144,6 +146,7 @@ namespace TH1_Logic.Oss
private static string GetSteamAppId()
{
#if STEAM_CHANNEL || STEAMWORKS_NET
try
{
var appId = SteamUtils.GetAppID().m_AppId;
@ -153,6 +156,9 @@ namespace TH1_Logic.Oss
{
return string.Empty;
}
#else
return string.Empty;
#endif
}
}
}

View File

@ -762,8 +762,7 @@ namespace TH1_Logic.Steam
return;
}
if (LobbyManager.Instance.Lobby is SteamLobbyManager steamLobby)
steamLobby.OnReceivedLobbyReport(message);
LobbyManager.Instance.Lobby?.OnReceivedLobbyReport(message);
}
private void OnReceivedInviteVersionMismatch(InviteVersionMismatchMessage message)

View File

@ -12,7 +12,6 @@ using Logic.AI;
using Logic.CrashSight;
using MemoryPack;
using RuntimeData;
using Steamworks;
using TH1_Logic.Chat;
using TH1_Logic.Config;
using TH1_Logic.Core;
@ -57,9 +56,6 @@ namespace TH1_Logic.Steam
{
if (lobbyInfo == null || lobbyInfo.OwnerId == 0 || lobbyInfo.LobbyId == 0) return false;
var targetId = new CSteamID(lobbyInfo.OwnerId);
if (!targetId.IsValid()) return false;
var data = new InviteVersionMismatchMessage
{
LobbyId = lobbyInfo.LobbyId,
@ -68,8 +64,8 @@ namespace TH1_Logic.Steam
LobbyVersion = lobbyInfo.Version,
};
byte[] messageBytes = NetworkPayloadCodec.Encode(TH1Serialization.Serialize<BaseMessage>(data));
if (SimpleP2P.Instance.SendToWithOutConnect(targetId, messageBytes)) return true;
byte[] messageBytes = SerializeForNetwork(data);
if (LobbyManager.Instance.Lobby.SendMessageToPeerWithoutConnection(lobbyInfo.OwnerId, messageBytes)) return true;
LogSystem.LogError($"InviteVersionMismatchMessage: 发送给房主失败 owner={lobbyInfo.OwnerId}, lobby={lobbyInfo.LobbyId}");
return false;
@ -313,8 +309,7 @@ namespace TH1_Logic.Steam
{
if (memberId == selfId) continue;
var target = new CSteamID(memberId);
if (!SimpleP2P.Instance.IsConnectedTo(target))
if (!LobbyManager.Instance.Lobby.IsPeerConnected(memberId))
{
LogSystem.LogInfo($"UpdateLobbyData deferred until P2P connected: memberId={memberId}");
continue;
@ -330,7 +325,7 @@ namespace TH1_Logic.Steam
if (LobbyManager.Instance.Lobby.IsLobbyOwner()) return false;
if (!LobbyManager.Instance.Lobby.IsInLobby()) return false;
var hostId = LobbyManager.Instance.Lobby.GetLobbyOwnerId();
if (hostId == 0 || !SimpleP2P.Instance.IsConnectedTo(new CSteamID(hostId)))
if (hostId == 0 || !LobbyManager.Instance.Lobby.IsPeerConnected(hostId))
{
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyDataRequestFailed);
return false;

View File

@ -0,0 +1,21 @@
using MemoryPack;
namespace TH1_Logic.Steam
{
// 房间信息结构。保持命名空间和字段不变,避免影响已有网络消息和 UI 引用。
[MemoryPackable]
public partial class LobbyListInfo
{
public ulong LobbyId;
public ulong OwnerId;
public string OwnerName;
public string RoomName;
public string Version;
public int CurrentPlayers;
public int MaxPlayers;
public int GameState;
public bool HasPassword;
public int ReportCount;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 93f23d875f97403ba1d4cf2afdc2f8c3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,4 +1,5 @@
using System;
#if STEAM_CHANNEL || STEAMWORKS_NET
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Logic.CrashSight;
@ -1738,3 +1739,4 @@ namespace TH1_Logic.Steam
}
}
}
#endif

View File

@ -5,7 +5,7 @@
* @Modify:
*/
#if STEAM_CHANNEL || STEAMWORKS_NET
using TH1_Logic.Core;
using TH1_Logic.Net;
using UnityEngine;
@ -138,4 +138,5 @@ namespace TH1_Logic.Steam
#endif
}
}
}
#endif

View File

@ -1,4 +1,4 @@
#if STEAM_CHANNEL || STEAMWORKS_NET
using System;
using System.Collections.Generic;
using System.IO;
@ -108,6 +108,7 @@ namespace TH1_Logic.Steam
// 房间列表
private List<LobbyListInfo> _lobbyListInfos;
public List<LobbyListInfo> LobbyListInfos => _lobbyListInfos;
public List<LobbyListInfo> GetLobbyListInfos() => _lobbyListInfos;
private readonly HashSet<ulong> _lobbyReporters = new HashSet<ulong>();
// 事件委托
@ -1274,6 +1275,21 @@ namespace TH1_Logic.Steam
if (string.IsNullOrEmpty(filteredRoomName)) return "Default";
return BannedWordFilter.Filter(filteredRoomName, BannedTextContext.Name);
}
string ILobby.GetDefaultRoomName(string ownerName)
{
return GetDefaultRoomName(ownerName);
}
string ILobby.FilterRoomName(string roomName)
{
return FilterRoomName(roomName);
}
public string GetSelfDisplayName()
{
return SelfName;
}
public bool IsInitialized()
{
@ -1797,6 +1813,23 @@ namespace TH1_Logic.Steam
return SimpleP2P.Instance.SendTo(cSteamId, data, reliable);
}
public bool SendMessageToPeerWithoutConnection(ulong member, byte[] data, bool reliable = true)
{
if (data == null || data.Length == 0)
return ReportP2PSendPrecheckFailed(member, "Trying to send null or empty data");
var targetId = new CSteamID(member);
if (!targetId.IsValid())
return ReportP2PSendPrecheckFailed(member, $"Invalid target member: {member}");
return SimpleP2P.Instance.SendToWithOutConnect(targetId, data, reliable);
}
public bool IsPeerConnected(ulong memberId)
{
if (memberId == 0) return false;
var targetId = new CSteamID(memberId);
return targetId.IsValid() && SimpleP2P.Instance.IsConnectedTo(targetId);
}
// 广播P2P消息
public bool BroadcastMessage(byte[] data, bool reliable = true)
{
@ -2129,22 +2162,5 @@ namespace TH1_Logic.Steam
LogSystem.LogInfo("SteamLobbyManager cleaned up");
}
}
// 房间信息结构
[MemoryPackable]
public partial class LobbyListInfo
{
public ulong LobbyId;
public ulong OwnerId;
public string OwnerName;
public string RoomName;
public string Version;
public int CurrentPlayers;
public int MaxPlayers;
public int GameState;
public bool HasPassword;
public int ReportCount;
}
}
#endif

View File

@ -0,0 +1,145 @@
#if !(STEAM_CHANNEL || STEAMWORKS_NET)
using System;
using System.Collections.Generic;
using Steamworks;
using TH1_Logic.Chat;
using TH1_Logic.Net;
namespace TH1_Logic.Steam
{
public class SteamLobbyManager : LobbyBase
{
public const string LobbyPasswordWrongError = "LobbyPasswordWrong";
public bool IsSteamInitialized => false;
public bool IsloggedIn => false;
public bool IsLobbyInitialized => false;
public string SelfName => GetSelfDisplayName();
public CSteamID SelfID => CSteamID.Nil;
public List<LobbyListInfo> LobbyListInfos => GetLobbyListInfos();
public event Action<CSteamID> OnLobbyCreatedEvent;
public event Action<CSteamID> OnLobbyEnteredEvent;
public event Action<List<CSteamID>> OnLobbyLeftEvent;
public event Action<CSteamID, CSteamID> OnHostChangedEvent;
public event Action<List<CSteamID>> OnMembersChangedEvent;
public event Action<string> OnLobbyErrorEvent;
public event Action<CSteamID> OnMemberJoinedEvent;
public event Action<CSteamID> OnMemberLeftEvent;
public new static string GetDefaultRoomName(string selfName)
{
return string.IsNullOrWhiteSpace(selfName) ? "Default" : $"{selfName} Room";
}
public static string FilterRoomName(string input, int maxLength = 20)
{
if (string.IsNullOrWhiteSpace(input)) return "Default";
var result = input.Trim();
if (result.Length > maxLength) result = result.Substring(0, maxLength);
return BannedWordFilter.Filter(result, BannedTextContext.Name);
}
public string GetRoomName()
{
return "Default";
}
public bool SetRoomName(string roomName)
{
return false;
}
public bool CanCreateLobbyNow(out string reason)
{
reason = "Steam is unavailable on this platform.";
return false;
}
public void SearchPublicLobbies()
{
}
public List<(CSteamID id, string name)> GetOnlineFriends()
{
return new List<(CSteamID, string)>();
}
public CSteamID GetCSteamID(ulong memberId)
{
return CSteamID.Nil;
}
public EPersonaState GetSelfPersonaState()
{
return EPersonaState.k_EPersonaStateOffline;
}
public void RaiseNoSteamError()
{
OnLobbyErrorEvent?.Invoke("Steam is unavailable on this platform.");
}
public void SuppressEventWarnings()
{
_ = OnLobbyCreatedEvent;
_ = OnLobbyEnteredEvent;
_ = OnLobbyLeftEvent;
_ = OnHostChangedEvent;
_ = OnMembersChangedEvent;
_ = OnMemberJoinedEvent;
_ = OnMemberLeftEvent;
}
}
public class SimpleP2P
{
public static SimpleP2P Instance { get; } = new SimpleP2P();
public bool IsInitialized => false;
public event Action<CSteamID> OnPeerConnectedEvent;
public event Action<CSteamID> OnPeerDisconnectedEvent;
public event Action<CSteamID, byte[]> OnMessageReceivedEvent;
public event Action<CSteamID, string> OnMessageSendFailedEvent;
public event Action<string> OnConnectionErrorEvent;
public void Initialize() { }
public void Update() { }
public void PollMessages() { }
public void Cleanup() { }
public void DisconnectAll() { }
public void DisconnectFromPeer(CSteamID steamID) { }
public void MarkExpectedDisconnect(CSteamID steamID) { }
public int GetConnectionCount() => 0;
public bool ConnectToPeer(CSteamID steamID) => false;
public bool IsConnectedTo(CSteamID steamID) => false;
public IEnumerable<CSteamID> GetConnectedPeers() => Array.Empty<CSteamID>();
public bool HasRecentConnectionFailure(float seconds, out string reason)
{
reason = string.Empty;
return false;
}
public bool SendTo(CSteamID target, byte[] data, bool reliable = true, bool ordered = true) => false;
public bool SendToWithOutConnect(CSteamID target, byte[] data, bool reliable = true, bool ordered = true) => false;
public bool CanQueueMessages(IReadOnlyList<CSteamID> targets, int dataLength, out CSteamID failedTarget, out string reason)
{
failedTarget = CSteamID.Nil;
reason = string.Empty;
return targets == null || targets.Count == 0;
}
public void LogDetailedConnectionInfo(CSteamID steamID) { }
public void SuppressEventWarnings()
{
_ = OnPeerConnectedEvent;
_ = OnPeerDisconnectedEvent;
_ = OnMessageReceivedEvent;
_ = OnMessageSendFailedEvent;
_ = OnConnectionErrorEvent;
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d9854f7e4ff4700b11fc15f41159a9b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,291 @@
#if !(STEAM_CHANNEL || STEAMWORKS_NET)
using System;
namespace Steamworks
{
public struct CSteamID : IEquatable<CSteamID>
{
public static readonly CSteamID Nil = new CSteamID(0);
public ulong m_SteamID;
public CSteamID(ulong value)
{
m_SteamID = value;
}
public bool IsValid() => m_SteamID != 0;
public uint GetAccountID() => (uint)(m_SteamID & 0xffffffff);
public bool Equals(CSteamID other) => m_SteamID == other.m_SteamID;
public override bool Equals(object obj) => obj is CSteamID other && Equals(other);
public override int GetHashCode() => m_SteamID.GetHashCode();
public override string ToString() => m_SteamID.ToString();
public static bool operator ==(CSteamID left, CSteamID right) => left.Equals(right);
public static bool operator !=(CSteamID left, CSteamID right) => !left.Equals(right);
}
public struct PublishedFileId_t : IEquatable<PublishedFileId_t>
{
public ulong m_PublishedFileId;
public PublishedFileId_t(ulong value)
{
m_PublishedFileId = value;
}
public bool Equals(PublishedFileId_t other) => m_PublishedFileId == other.m_PublishedFileId;
public override bool Equals(object obj) => obj is PublishedFileId_t other && Equals(other);
public override int GetHashCode() => m_PublishedFileId.GetHashCode();
public override string ToString() => m_PublishedFileId.ToString();
public static bool operator ==(PublishedFileId_t left, PublishedFileId_t right) => left.Equals(right);
public static bool operator !=(PublishedFileId_t left, PublishedFileId_t right) => !left.Equals(right);
}
public struct AppId_t
{
public uint m_AppId;
public AppId_t(uint value)
{
m_AppId = value;
}
}
public struct SteamAPICall_t
{
public ulong m_SteamAPICall;
}
public struct UGCQueryHandle_t
{
public ulong m_UGCQueryHandle;
}
public struct UGCUpdateHandle_t
{
public ulong m_UGCUpdateHandle;
}
public struct SteamNetworkingIdentity
{
public CSteamID SteamID;
public void SetSteamID(CSteamID steamId)
{
SteamID = steamId;
}
}
public enum EResult
{
k_EResultOK = 1,
k_EResultFail = 2,
k_EResultNoConnection = 3,
k_EResultInvalidPassword = 5,
k_EResultLimitExceeded = 25,
k_EResultIgnored = 43,
k_EResultAccessDenied = 15
}
public enum EPersonaState
{
k_EPersonaStateOffline = 0,
k_EPersonaStateOnline = 1,
k_EPersonaStateBusy = 2,
k_EPersonaStateAway = 3,
k_EPersonaStateSnooze = 4,
k_EPersonaStateInvisible = 7
}
public enum ETextFilteringContext
{
k_ETextFilteringContextUnknown = 0,
k_ETextFilteringContextGameContent = 1,
k_ETextFilteringContextChat = 2,
k_ETextFilteringContextName = 3
}
[Flags]
public enum EItemState
{
k_EItemStateNone = 0,
k_EItemStateSubscribed = 1,
k_EItemStateInstalled = 4
}
public enum EItemUpdateStatus
{
k_EItemUpdateStatusInvalid = 0
}
public enum EWorkshopFileType
{
k_EWorkshopFileTypeCommunity = 0
}
public enum ERemoteStoragePublishedFileVisibility
{
k_ERemoteStoragePublishedFileVisibilityPublic = 0
}
public enum EUGCQuery
{
k_EUGCQuery_RankedByPublicationDate = 0
}
public enum EUGCMatchingUGCType
{
k_EUGCMatchingUGCType_Items = 0,
k_EUGCMatchingUGCType_Items_ReadyToUse = 1
}
public enum EUserUGCList
{
k_EUserUGCList_Published = 0
}
public enum EUserUGCListSortOrder
{
k_EUserUGCListSortOrder_CreationOrderDesc = 0
}
public struct SteamUGCDetails_t
{
public PublishedFileId_t m_nPublishedFileId;
public string m_rgchTitle;
public string m_rgchDescription;
public string m_rgchTags;
public uint m_unVotesUp;
public uint m_unVotesDown;
public int m_nFileSize;
public uint m_rtimeCreated;
public uint m_rtimeUpdated;
public ulong m_ulSteamIDOwner;
}
public struct SteamUGCQueryCompleted_t
{
public UGCQueryHandle_t m_handle;
public EResult m_eResult;
public uint m_unNumResultsReturned;
public uint m_unTotalMatchingResults;
}
public struct CreateItemResult_t
{
public EResult m_eResult;
public PublishedFileId_t m_nPublishedFileId;
public bool m_bUserNeedsToAcceptWorkshopLegalAgreement;
}
public struct SubmitItemUpdateResult_t
{
public EResult m_eResult;
public bool m_bUserNeedsToAcceptWorkshopLegalAgreement;
}
public struct RemoteStorageSubscribePublishedFileResult_t
{
public EResult m_eResult;
public PublishedFileId_t m_nPublishedFileId;
}
public struct RemoteStorageUnsubscribePublishedFileResult_t
{
public EResult m_eResult;
public PublishedFileId_t m_nPublishedFileId;
}
public sealed class CallResult<T>
{
public static CallResult<T> Create(Action<T, bool> callback) => new CallResult<T>();
public void Set(SteamAPICall_t apiCall) { }
public void Set(SteamAPICall_t apiCall, Action<T, bool> callback) { }
}
public static class CallbackDispatcher
{
public static bool IsInitialized => false;
}
public static class SteamAPI
{
public static bool IsSteamRunning() => false;
public static void RunCallbacks() { }
}
public static class SteamUtils
{
public static AppId_t GetAppID() => new AppId_t(0);
public static bool InitFilterText(uint filterOptions = 0) => false;
public static int FilterText(ETextFilteringContext context, CSteamID sourceSteamId, string input, out string filtered, uint filteredSize)
{
filtered = input;
return -1;
}
}
public static class SteamUser
{
public static bool BLoggedOn() => false;
public static CSteamID GetSteamID() => CSteamID.Nil;
public static uint GetAuthSessionTicket(byte[] ticket, int maxTicket, out uint ticketSize, ref SteamNetworkingIdentity identity)
{
ticketSize = 0;
return 0;
}
}
public static class SteamFriends
{
public static string GetPersonaName() => string.Empty;
public static bool RequestUserInformation(CSteamID steamId, bool requireNameOnly) => false;
public static string GetFriendPersonaName(CSteamID steamId) => string.Empty;
}
public static class SteamUGC
{
public static UGCQueryHandle_t CreateQueryAllUGCRequest(EUGCQuery queryType, EUGCMatchingUGCType matchingType, AppId_t creatorAppId, AppId_t consumerAppId, uint page) => default;
public static UGCQueryHandle_t CreateQueryUserUGCRequest(uint accountId, EUserUGCList listType, EUGCMatchingUGCType matchingType, EUserUGCListSortOrder sortOrder, AppId_t creatorAppId, AppId_t consumerAppId, uint page) => default;
public static bool SetReturnLongDescription(UGCQueryHandle_t handle, bool value) => false;
public static bool SetReturnTotalOnly(UGCQueryHandle_t handle, bool value) => false;
public static SteamAPICall_t SendQueryUGCRequest(UGCQueryHandle_t handle) => default;
public static bool ReleaseQueryUGCRequest(UGCQueryHandle_t handle) => true;
public static bool GetQueryUGCResult(UGCQueryHandle_t handle, uint index, out SteamUGCDetails_t details)
{
details = default;
return false;
}
public static uint GetItemState(PublishedFileId_t fileId) => 0;
public static bool GetItemInstallInfo(PublishedFileId_t fileId, out ulong sizeOnDisk, out string folder, uint folderSize, out uint timestamp)
{
sizeOnDisk = 0;
folder = string.Empty;
timestamp = 0;
return false;
}
public static uint GetNumSubscribedItems() => 0;
public static uint GetSubscribedItems(PublishedFileId_t[] publishedFileIds, uint maxEntries) => 0;
public static SteamAPICall_t SubscribeItem(PublishedFileId_t fileId) => default;
public static SteamAPICall_t UnsubscribeItem(PublishedFileId_t fileId) => default;
public static SteamAPICall_t CreateItem(AppId_t consumerAppId, EWorkshopFileType fileType) => default;
public static UGCUpdateHandle_t StartItemUpdate(AppId_t consumerAppId, PublishedFileId_t fileId) => default;
public static bool SetItemTitle(UGCUpdateHandle_t handle, string title) => false;
public static bool SetItemDescription(UGCUpdateHandle_t handle, string description) => false;
public static bool SetItemContent(UGCUpdateHandle_t handle, string contentFolder) => false;
public static bool SetItemPreview(UGCUpdateHandle_t handle, string previewFile) => false;
public static bool SetItemVisibility(UGCUpdateHandle_t handle, ERemoteStoragePublishedFileVisibility visibility) => false;
public static bool SetItemTags(UGCUpdateHandle_t handle, System.Collections.Generic.IList<string> tags) => false;
public static SteamAPICall_t SubmitItemUpdate(UGCUpdateHandle_t handle, string changeNote) => default;
public static EItemUpdateStatus GetItemUpdateProgress(UGCUpdateHandle_t handle, out ulong bytesProcessed, out ulong bytesTotal)
{
bytesProcessed = 0;
bytesTotal = 0;
return EItemUpdateStatus.k_EItemUpdateStatusInvalid;
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: da2c27e88ef14b3d9b65f2b1222f1f5d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -2,7 +2,9 @@ using TH1_Core.Events;
using Logic.CrashSight;
using TH1_Logic.Config;
using TH1_Logic.Oss;
#if STEAM_CHANNEL || STEAMWORKS_NET
using Steamworks;
#endif
using TH1_UI.Controller.Base;
using TH1_UI.View.Global;
@ -117,6 +119,7 @@ namespace TH1_UI.Controller.Global
private static bool TryGetSteamId(out string steamId)
{
steamId = "";
#if STEAM_CHANNEL || STEAMWORKS_NET
try
{
if (!SteamUser.BLoggedOn())
@ -134,6 +137,9 @@ namespace TH1_UI.Controller.Global
LogSystem.LogWarning($"PlayerBugReport SteamID unavailable: {ex.Message}");
return false;
}
#else
return false;
#endif
}
}
}