Compare commits
2 Commits
6342e1496d
...
575b8a288e
| Author | SHA1 | Date | |
|---|---|---|---|
| 575b8a288e | |||
| 99473c5679 |
@ -84,7 +84,7 @@ Keep the current single Function Compute service/file unless the user explicitly
|
||||
|
||||
- Backend code changes: edit `Tools/OSS/game-upload-function/index.js`, then run at least `npm install` if dependencies changed and a local smoke server check when env vars are available.
|
||||
- Unity upload/client changes: edit only the relevant `TH1_Logic/Oss` files, then run a Unity compile or targeted editor test when practical.
|
||||
- Player bug report changes: keep Unity-side package shape aligned with `Tools/PlayerBugViewer`; verify `manifest.json`, `description.txt`, and `saves/{single|multi}/...dat` entries.
|
||||
- Player bug report changes: keep Unity-side package shape aligned with `Tools/PlayerBugViewer`; verify `manifest.json`, `description.txt`, device/time fields, `crashSightDeviceId`, and `saves/{single|multi}/...dat` entries.
|
||||
- Developer bug viewer changes: edit `Tools/PlayerBugViewer`; preserve local credential/cache ignores for `config.local.json` and `Data/`.
|
||||
- Collect data download/stat changes: use `OssEditorWindow.cs` and `OssDownloadService.cs`; verify the mapping from `collect/` OSS keys to `Tools/OSS/Data`.
|
||||
- OSS data analysis: use `Tools/OSS/Data` for `.dat` files and `Tools/OSS/Data/JsonExport` for exported JSON; avoid hand-decoding MemoryPack outside Unity unless there is already a project utility for it.
|
||||
@ -97,4 +97,5 @@ Keep the current single Function Compute service/file unless the user explicitly
|
||||
- Run `node -c Tools/OSS/game-upload-function/index.js` after backend edits.
|
||||
- Run `python -m py_compile Tools/PlayerBugViewer/player_bug_viewer.py` after player bug viewer edits.
|
||||
- Confirm no secrets, raw tickets, or full STS credentials were added to files, logs, docs, or reports.
|
||||
- For CrashSight correlation, `CrashSightManager.GetCrashSightDeviceId()` is the shared source: prefer `SystemInfo.deviceUniqueIdentifier`; if unsupported, use a PlayerPrefs-persisted `th1-{guid}` fallback; if PlayerPrefs is unavailable, use a process-local `th1-device-id-unavailable-{guid}` fallback. Set the same value into CrashSight and bug-report manifests.
|
||||
- Report which side was changed: Function Compute code, Unity client upload code, editor OSS tooling, local OSS data, or documentation.
|
||||
|
||||
@ -149,6 +149,15 @@ Bug report package shape:
|
||||
- `saves/{single|multi}/map_archive_begin[_multi]_{mapId}.dat`
|
||||
- `saves/{single|multi}/map_archive_continue[_multi]_{mapId}.dat` or `map_archive_end[_multi]_{mapId}.dat`
|
||||
|
||||
Bug report manifest includes:
|
||||
|
||||
- Identity/time: `reportId`, `createdAtUtc`, `createdAtLocal`, `timezone`, `steamId`.
|
||||
- Version/runtime: `version`, `unityVersion`, `platform`.
|
||||
- CrashSight correlation: `crashSightDeviceId`. `CrashSightManager.Initialize` sets the same value on CrashSight via `CrashSightAgent.SetDeviceId`.
|
||||
- Device ID source: `CrashSightManager.GetCrashSightDeviceId()` prefers `SystemInfo.deviceUniqueIdentifier`; if Unity returns an empty/unsupported identifier, it generates and persists a `th1-{guid}` fallback in PlayerPrefs. If even PlayerPrefs is unavailable, it uses a process-local `th1-device-id-unavailable-{guid}` fallback. Treat it as a stable correlation ID, not a cryptographic global uniqueness guarantee.
|
||||
- Device snapshot: `deviceName`, `deviceModel`, `operatingSystem`, `processorType`, `processorCount`, `systemMemorySizeMb`, `graphicsDeviceName`, `graphicsMemorySizeMb`.
|
||||
- Content/archive: `description`, `archiveCount`, `archives[]`.
|
||||
|
||||
Local collect download mapping:
|
||||
|
||||
- OSS `collect/0.7.0/{steamId}/{ts}.dat`
|
||||
|
||||
@ -356,6 +356,14 @@ saves/multi/map_archive_begin_multi_{mapId}.dat
|
||||
saves/multi/map_archive_continue_multi_{mapId}.dat 或 map_archive_end_multi_{mapId}.dat
|
||||
```
|
||||
|
||||
`manifest.json` 目前包含:
|
||||
|
||||
- 汇报标识与时间:`reportId`、`createdAtUtc`、`createdAtLocal`、`timezone`。
|
||||
- 玩家与版本:`steamId`、`version`、`unityVersion`、`platform`。
|
||||
- CrashSight 对齐字段:`crashSightDeviceId`。项目初始化 CrashSight 时也会显式设置同一个设备 ID,便于后台检索对应玩家。来源优先使用 `SystemInfo.deviceUniqueIdentifier`;如果 Unity 返回空值或 unsupported,则生成并持久化一个 `th1-{guid}` 作为本机安装级 fallback;极端情况下 PlayerPrefs 不可用时,使用进程内 `th1-device-id-unavailable-{guid}` fallback。
|
||||
- 设备信息:`deviceName`、`deviceModel`、`operatingSystem`、`processorType`、`processorCount`、`systemMemorySizeMb`、`graphicsDeviceName`、`graphicsMemorySizeMb`。
|
||||
- 玩家自述与存档:`description`、`archiveCount`、`archives[]`。每个存档条目包含模式、MapID、begin/continue/end 类型、源文件名、zip 路径、文件大小和最后写入时间。
|
||||
|
||||
### 12.2 开发者查看器
|
||||
|
||||
工具目录:`Tools/PlayerBugViewer/`
|
||||
@ -364,6 +372,6 @@ saves/multi/map_archive_continue_multi_{mapId}.dat 或 map_archive_end_multi_{ma
|
||||
|
||||
- 使用 OSS AccessKey 更新 `bugreport/` 内容到本地缓存。
|
||||
- 按版本和 SteamID 筛选条目。
|
||||
- 预览玩家自述、上传时间、版本、SteamID、附带存档。
|
||||
- 预览玩家自述、上传时间、版本、SteamID、CrashSight 设备 ID、设备信息、附带存档。
|
||||
- 一键清理本地 `map_archive_*.dat/.bak` 并替换为该汇报内的存档。
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
1. 运行 `启动玩家Bug查看器.bat`。
|
||||
2. 填写 OSS `AccessKey ID` / `AccessKey Secret`,确认 `Endpoint` 和 `Bucket`。
|
||||
3. 点击「更新内容」拉取 `bugreport/` 下的 zip。
|
||||
4. 选择条目查看玩家自述、版本、SteamID、附带存档。
|
||||
4. 选择条目查看玩家自述、版本、SteamID、CrashSight 设备 ID、设备信息、附带存档。
|
||||
5. 点击「一键替换到本地存档」会删除目标目录下所有 `map_archive_*.dat` / `.bak`,再写入该汇报中的 `start + continue/end` 存档。
|
||||
|
||||
配置会保存到 `config.local.json`,该文件应只保留在本机。
|
||||
|
||||
@ -455,7 +455,17 @@ class PlayerBugViewer(Tk):
|
||||
("ReportID", report.report_id),
|
||||
("版本", report.version),
|
||||
("SteamID", report.steam_id),
|
||||
("时间", parse_oss_time(report.created_at_utc)),
|
||||
("时间(UTC)", parse_oss_time(report.created_at_utc)),
|
||||
("时间(本地)", report.manifest.get("createdAtLocal", "")),
|
||||
("时区", report.manifest.get("timezone", "")),
|
||||
("CrashSight设备ID", report.manifest.get("crashSightDeviceId", "")),
|
||||
("设备名", report.manifest.get("deviceName", "")),
|
||||
("设备型号", report.manifest.get("deviceModel", "")),
|
||||
("系统", report.manifest.get("operatingSystem", "")),
|
||||
("CPU", report.manifest.get("processorType", "")),
|
||||
("内存", f"{report.manifest.get('systemMemorySizeMb', '')} MB"),
|
||||
("显卡", report.manifest.get("graphicsDeviceName", "")),
|
||||
("显存", f"{report.manifest.get('graphicsMemorySizeMb', '')} MB"),
|
||||
("OSS Key", report.object_key),
|
||||
("本地文件", str(report.local_path)),
|
||||
("存档数", str(report.archive_count)),
|
||||
|
||||
@ -1027,7 +1027,8 @@ namespace RuntimeData
|
||||
{
|
||||
if (UnitToGridDict.TryGetValue(uid, out var oldGid))
|
||||
{
|
||||
_gridToUnitDict.Remove(oldGid);
|
||||
if (_gridToUnitDict.TryGetValue(oldGid, out var oldUid) && oldUid == uid)
|
||||
_gridToUnitDict.Remove(oldGid);
|
||||
}
|
||||
|
||||
UnitToGridDict[uid] = gid;
|
||||
@ -1043,7 +1044,8 @@ namespace RuntimeData
|
||||
// 在清除绑定前获取所属城市id和格子id
|
||||
UnitToCityDict.TryGetValue(unitData.Id, out var cityId);
|
||||
GetGridIdByUnitId(unitData.Id, out var gridId);
|
||||
if (gridId != 0) _gridToUnitDict.Remove(gridId);
|
||||
if (gridId != 0 && _gridToUnitDict.TryGetValue(gridId, out var gridUnitId) && gridUnitId == unitData.Id)
|
||||
_gridToUnitDict.Remove(gridId);
|
||||
|
||||
// 清除数据层绑定
|
||||
RemoveUnitData(unitData.Id);
|
||||
@ -1705,9 +1707,9 @@ namespace RuntimeData
|
||||
UnitMap.RemoveUnitData(uid);
|
||||
if (UnitToGridDict.Remove(uid, out var gid))
|
||||
{
|
||||
_gridToUnitDict.Remove(gid);
|
||||
if (_gridToUnitDict.TryGetValue(gid, out var gridUnitId) && gridUnitId == uid)
|
||||
_gridToUnitDict.Remove(gid);
|
||||
}
|
||||
UnitToGridDict.Remove(uid);
|
||||
UnitToCityDict.Remove(uid);
|
||||
}
|
||||
|
||||
@ -2179,7 +2181,8 @@ namespace RuntimeData
|
||||
|
||||
private static bool ShouldSkipMapArchive(MapData map)
|
||||
{
|
||||
return map?.MapConfig?.MatchSettlement == MatchSettlementType.Story;
|
||||
return map?.Net?.Mode == NetMode.Spectator
|
||||
|| map?.MapConfig?.MatchSettlement == MatchSettlementType.Story;
|
||||
}
|
||||
|
||||
private static void InvalidateMapArchiveAvailabilityCache()
|
||||
|
||||
@ -64,9 +64,14 @@ namespace RuntimeData
|
||||
[MemoryPackOnDeserialized]
|
||||
public void OnAfterMemoryPackDeserialize()
|
||||
{
|
||||
UnitList ??= new List<UnitData>();
|
||||
_unitDict ??= new Dictionary<uint, UnitData>();
|
||||
_unitDict.Clear();
|
||||
foreach (var unit in UnitList) _unitDict[unit.Id] = unit;
|
||||
foreach (var unit in UnitList)
|
||||
{
|
||||
if (unit == null) continue;
|
||||
_unitDict[unit.Id] = unit;
|
||||
}
|
||||
}
|
||||
|
||||
public void DeepCopy(UnitMapData copyData)
|
||||
@ -146,17 +151,44 @@ namespace RuntimeData
|
||||
// 删除小兵
|
||||
public void RemoveUnitData(uint unitId)
|
||||
{
|
||||
if (_unitDict.TryGetValue(unitId, out var unitData))
|
||||
UnitList ??= new List<UnitData>();
|
||||
_unitDict ??= new Dictionary<uint, UnitData>();
|
||||
|
||||
var removed = false;
|
||||
for (var i = UnitList.Count - 1; i >= 0; i--)
|
||||
{
|
||||
UnitList.Remove(unitData);
|
||||
_unitDict.Remove(unitId);
|
||||
var unitData = UnitList[i];
|
||||
if (unitData == null || unitData.Id != unitId) continue;
|
||||
UnitList.RemoveAt(i);
|
||||
removed = true;
|
||||
}
|
||||
|
||||
_unitDict.Remove(unitId);
|
||||
|
||||
if (!removed)
|
||||
LogSystem.LogWarning($"UnitMapData.RemoveUnitData: UnitList missing unitId={unitId}, rebuilt dict may have been stale.");
|
||||
}
|
||||
|
||||
// 通过 uid 找小兵数据 unitData
|
||||
public bool GetUnitDataByUnitId(uint unitId, out UnitData unitData)
|
||||
{
|
||||
return _unitDict.TryGetValue(unitId, out unitData);
|
||||
_unitDict ??= new Dictionary<uint, UnitData>();
|
||||
if (_unitDict.TryGetValue(unitId, out unitData)) return true;
|
||||
|
||||
if (UnitList != null)
|
||||
{
|
||||
foreach (var unit in UnitList)
|
||||
{
|
||||
if (unit == null || unit.Id != unitId) continue;
|
||||
unitData = unit;
|
||||
_unitDict[unitId] = unit;
|
||||
LogSystem.LogWarning($"UnitMapData.GetUnitDataByUnitId: recovered stale dict entry unitId={unitId}.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
unitData = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@ -222,6 +254,24 @@ namespace RuntimeData
|
||||
|
||||
// 血量
|
||||
public int Health;
|
||||
private const float PositiveIntEpsilon = 0.0001f;
|
||||
private const double PositiveIntDoubleEpsilon = 0.000001d;
|
||||
|
||||
public static int CeilPositiveToInt(float value)
|
||||
{
|
||||
if (value <= 0f) return 0;
|
||||
var rounded = Mathf.RoundToInt(value);
|
||||
if (rounded > 0 && Mathf.Abs(value - rounded) <= PositiveIntEpsilon) return rounded;
|
||||
return Mathf.CeilToInt(value);
|
||||
}
|
||||
|
||||
public static int CeilPositiveToInt(double value)
|
||||
{
|
||||
if (value <= 0d) return 0;
|
||||
var rounded = (int)Math.Round(value);
|
||||
if (rounded > 0 && Math.Abs(value - rounded) <= PositiveIntDoubleEpsilon) return rounded;
|
||||
return (int)Math.Ceiling(value);
|
||||
}
|
||||
|
||||
// 攻击范围
|
||||
//英雄经验值
|
||||
@ -1760,6 +1810,11 @@ namespace RuntimeData
|
||||
return offset;
|
||||
}
|
||||
|
||||
public int AddHealth(float hp)
|
||||
{
|
||||
return AddHealth(CeilPositiveToInt(hp));
|
||||
}
|
||||
|
||||
public void SetOfficer()
|
||||
{
|
||||
var isFrozen = IsFrozen();
|
||||
@ -1852,4 +1907,4 @@ namespace RuntimeData
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,7 +247,7 @@ public class Table
|
||||
float defenseForce = defenseB * (1f * B.Health / B.GetMaxHealth());
|
||||
var totalDamage = attackForce + defenseForce;
|
||||
if (totalDamage == 0) return 0;
|
||||
int attackResult = (int)((attackForce / totalDamage) * attackA * 4.5f + 0.5f);
|
||||
int attackResult = UnitData.CeilPositiveToInt((attackForce / totalDamage) * attackA * 4.5f);
|
||||
return attackResult;
|
||||
}
|
||||
|
||||
@ -260,7 +260,7 @@ public class Table
|
||||
float defenseForce = defenseB * (1f * B.Health / B.GetMaxHealth());
|
||||
var totalDamage = attackForce + defenseForce;
|
||||
if (totalDamage == 0) return 0;
|
||||
int defenseResult = (int)((defenseForce / totalDamage) * B.GetBaseDefenseValue(map, A) * 4.5f + 0.5f);
|
||||
int defenseResult = UnitData.CeilPositiveToInt((defenseForce / totalDamage) * B.GetBaseDefenseValue(map, A) * 4.5f);
|
||||
return defenseResult;
|
||||
|
||||
/*
|
||||
@ -270,7 +270,7 @@ public class Table
|
||||
float defenseForce = defenseB * (1f * B.Health / B.GetMaxHealth());
|
||||
var totalDamage = attackForce + defenseForce;
|
||||
if (totalDamage == 0) return 0;
|
||||
int attackResult = (int)((attackForce / totalDamage) * attackA * 4.5f + 0.5f);
|
||||
int attackResult = UnitData.CeilPositiveToInt((attackForce / totalDamage) * attackA * 4.5f);
|
||||
return attackResult;*/
|
||||
}
|
||||
|
||||
@ -283,7 +283,7 @@ public class Table
|
||||
float defenseForce = defenseB * (1f * B.Health / B.GetMaxHealth());
|
||||
float totalDamage = attackForce + defenseForce;
|
||||
if (totalDamage == 0) return 0;
|
||||
int attackResult = (int)((attackForce / totalDamage) * attackA * 4.5f + 0.5f);
|
||||
int attackResult = UnitData.CeilPositiveToInt((attackForce / totalDamage) * attackA * 4.5f);
|
||||
return attackResult;
|
||||
}
|
||||
|
||||
|
||||
@ -81,6 +81,13 @@ namespace Logic
|
||||
// 这里用 CurPlayer 来界定游戏是否开始
|
||||
if (_curState == GameState.Menu && Main.MapData.CurPlayer == null) return;
|
||||
if (_curState == GameState.Finished) return;
|
||||
if (_curState == GameState.Spectate)
|
||||
{
|
||||
if (Main.MapData.CheckIfGameEnd(out _)) return;
|
||||
Main.MapData.RefreshTurn();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Main.MapData.CheckIfGameEnd(out _))
|
||||
{
|
||||
ChangeState(GameState.Finished);
|
||||
@ -492,6 +499,8 @@ namespace Logic
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
if (Main.MapData.Net.Mode == NetMode.Spectator) return;
|
||||
|
||||
if (Main.MapData.CheckIfGameEnd(out var isWin))
|
||||
{
|
||||
AchievementDataManager.Instance.OnGameEnd(Main.MapData, isWin);
|
||||
|
||||
@ -652,13 +652,29 @@ namespace TH1_Logic.Core
|
||||
// 开始观战
|
||||
public void StartSpectate(MapData map, MapData spectateMapData)
|
||||
{
|
||||
//step #1 初始化Audio
|
||||
InitGameAudio();
|
||||
|
||||
//step #2 初始化MapData
|
||||
if (map?.Net == null || spectateMapData?.Net == null)
|
||||
{
|
||||
LogSystem.LogError("StartSpectate failed: map data is invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
//step #1 初始化MapData
|
||||
MapData = map;
|
||||
SpectateMapData = spectateMapData;
|
||||
|
||||
// 回放只复用 action 流程,不参与存档、成就和联机发送。
|
||||
MapData.Net.Mode = NetMode.Spectator;
|
||||
if (SpectateMapData.Net.Actions.Count < MapData.Net.Actions.Count)
|
||||
{
|
||||
LogSystem.LogWarning(
|
||||
$"StartSpectate: 终点 action 数少于起点,mapId={MapData.MapID}, start={MapData.Net.Actions.Count}, target={SpectateMapData.Net.Actions.Count}");
|
||||
}
|
||||
|
||||
MapData.PlayerMap.SelfPlayerId = MapData.PlayerMap.PlayerDataList[0].Id;
|
||||
|
||||
//step #2 初始化Audio
|
||||
InitGameAudio();
|
||||
|
||||
AIActionScoreCalculator.RefreshCalMap(MapData, true);
|
||||
|
||||
//step #2 视觉三兄弟 初始化maprender 初始化UIManager 初始化演出管理器
|
||||
|
||||
@ -15,6 +15,8 @@ namespace Logic.CrashSight
|
||||
public class CrashSightManager
|
||||
{
|
||||
public static CrashSightManager Instance => new CrashSightManager();
|
||||
private const string CrashSightDeviceIdFallbackKey = "TH1_CrashSightDeviceId";
|
||||
private static string _runtimeFallbackDeviceId;
|
||||
private CrashSightManager() { }
|
||||
|
||||
public void Initialize()
|
||||
@ -24,6 +26,34 @@ namespace Logic.CrashSight
|
||||
CrashSightAgent.ConfigCrashServerUrl("pc.crashsight.qq.com");
|
||||
// 设置上报所指向的APP ID, 并进行初始化。APPID可以在管理端更多->产品设置->产品信息中找到。
|
||||
CrashSightAgent.InitWithAppId("01076c49ce");
|
||||
var deviceId = GetCrashSightDeviceId();
|
||||
CrashSightAgent.SetDeviceId(deviceId);
|
||||
CrashSightAgent.SetUserValue("DeviceId", deviceId);
|
||||
}
|
||||
|
||||
public static string GetCrashSightDeviceId()
|
||||
{
|
||||
try
|
||||
{
|
||||
var deviceId = SystemInfo.deviceUniqueIdentifier;
|
||||
if (!string.IsNullOrEmpty(deviceId) && deviceId != SystemInfo.unsupportedIdentifier)
|
||||
return deviceId;
|
||||
|
||||
var cachedId = PlayerPrefs.GetString(CrashSightDeviceIdFallbackKey, "");
|
||||
if (!string.IsNullOrEmpty(cachedId))
|
||||
return cachedId;
|
||||
|
||||
cachedId = $"th1-{System.Guid.NewGuid():N}";
|
||||
PlayerPrefs.SetString(CrashSightDeviceIdFallbackKey, cachedId);
|
||||
PlayerPrefs.Save();
|
||||
return cachedId;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (string.IsNullOrEmpty(_runtimeFallbackDeviceId))
|
||||
_runtimeFallbackDeviceId = $"th1-device-id-unavailable-{System.Guid.NewGuid():N}";
|
||||
return _runtimeFallbackDeviceId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ namespace Logic.Editor
|
||||
EditorGUILayout.Space(10);
|
||||
if (_mapPairs.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("未找到匹配的地图存档对(需要同时存在 begin 和 end 文件)", MessageType.Info);
|
||||
EditorGUILayout.HelpBox("未找到匹配的地图存档对(优先使用 begin + end;没有 end 时可使用 begin + continue)", MessageType.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -78,14 +78,18 @@ namespace Logic.Editor
|
||||
EditorGUILayout.LabelField($"Map ID: {pair.MapId}", EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField($"类型: {(pair.IsMulti ? "多人" : "单人")}");
|
||||
EditorGUILayout.LabelField($"开始存档: {Path.GetFileName(pair.BeginPath)}");
|
||||
EditorGUILayout.LabelField($"结束存档: {Path.GetFileName(pair.EndPath)}");
|
||||
EditorGUILayout.LabelField($"{GetEndKindLabel(pair.EndKind)}: {Path.GetFileName(pair.EndPath)}");
|
||||
EditorGUILayout.LabelField($"修改时间: {pair.LastModifiedTime:yyyy-MM-dd HH:mm:ss}");
|
||||
if (pair.EndKind == ReplayEndKind.Continue)
|
||||
{
|
||||
EditorGUILayout.HelpBox("未找到 end 存档,将从 begin 播放到 continue 当前进度。", MessageType.None);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
if (GUILayout.Button("加载此地图进行观战", GUILayout.Height(25)))
|
||||
{
|
||||
LoadMapForSpectator(pair.MapId, pair.IsMulti);
|
||||
LoadMapForSpectator(pair);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
@ -104,7 +108,10 @@ namespace Logic.Editor
|
||||
}
|
||||
|
||||
// 查找所有 begin 文件
|
||||
var beginFiles = Directory.GetFiles(directory, "map_archive_begin*.dat");
|
||||
var beginFiles = Directory.GetFiles(directory, "map_archive_begin*.dat")
|
||||
.Concat(Directory.GetFiles(directory, "map_archive_begin*.dat.bak"))
|
||||
.OrderByDescending(File.GetLastWriteTime);
|
||||
var visitedMapKeys = new HashSet<string>();
|
||||
|
||||
foreach (var beginFile in beginFiles)
|
||||
{
|
||||
@ -113,12 +120,21 @@ namespace Logic.Editor
|
||||
// 解析文件名获取 MapId 和类型
|
||||
if (!TryParseFileName(fileName, out uint mapId, out bool isMulti))
|
||||
continue;
|
||||
|
||||
var mapKey = $"{isMulti}:{mapId}";
|
||||
if (!visitedMapKeys.Add(mapKey))
|
||||
continue;
|
||||
|
||||
// 查找对应的 end 文件
|
||||
string endPattern = $"map_archive_end{(isMulti ? "_multi" : "")}_{mapId}.dat";
|
||||
string endPath = Path.Combine(directory, endPattern);
|
||||
|
||||
if (!File.Exists(endPath))
|
||||
// 优先查找对应的 end 文件;没有 end 时使用 continue 作为回放终点。
|
||||
var endKind = ReplayEndKind.End;
|
||||
var endPath = FindArchivePath(directory, "end", isMulti, mapId);
|
||||
if (endPath == null)
|
||||
{
|
||||
endKind = ReplayEndKind.Continue;
|
||||
endPath = FindArchivePath(directory, "continue", isMulti, mapId);
|
||||
}
|
||||
|
||||
if (endPath == null)
|
||||
continue;
|
||||
|
||||
// 添加到列表
|
||||
@ -128,7 +144,8 @@ namespace Logic.Editor
|
||||
IsMulti = isMulti,
|
||||
BeginPath = beginFile,
|
||||
EndPath = endPath,
|
||||
LastModifiedTime = File.GetLastWriteTime(beginFile)
|
||||
EndKind = endKind,
|
||||
LastModifiedTime = Max(File.GetLastWriteTime(beginFile), File.GetLastWriteTime(endPath))
|
||||
});
|
||||
}
|
||||
|
||||
@ -144,38 +161,92 @@ namespace Logic.Editor
|
||||
isMulti = false;
|
||||
|
||||
// 文件名格式: map_archive_begin[_multi]_{mapId}.dat
|
||||
if (!fileName.StartsWith("map_archive_begin"))
|
||||
var stem = StripArchiveExtensions(fileName);
|
||||
if (string.IsNullOrEmpty(stem))
|
||||
return false;
|
||||
|
||||
isMulti = fileName.Contains("_multi");
|
||||
|
||||
// 提取 MapId
|
||||
var parts = fileName.Replace(".dat", "").Split('_');
|
||||
if (parts.Length == 0)
|
||||
|
||||
const string singlePrefix = "map_archive_begin_";
|
||||
const string multiPrefix = "map_archive_begin_multi_";
|
||||
string idPart;
|
||||
if (stem.StartsWith(multiPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
isMulti = true;
|
||||
idPart = stem.Substring(multiPrefix.Length);
|
||||
}
|
||||
else if (stem.StartsWith(singlePrefix, StringComparison.Ordinal))
|
||||
{
|
||||
idPart = stem.Substring(singlePrefix.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
|
||||
var lastPart = parts[parts.Length - 1];
|
||||
return uint.TryParse(lastPart, out mapId);
|
||||
}
|
||||
|
||||
return uint.TryParse(idPart, out mapId);
|
||||
}
|
||||
|
||||
private void LoadMapForSpectator(uint mapId, bool isMulti)
|
||||
private void LoadMapForSpectator(MapArchivePair pair)
|
||||
{
|
||||
// 这里添加加载地图的逻辑
|
||||
// 例如: 调用游戏中的地图加载方法
|
||||
// Main.Instance.LoadSpectatorMap(mapId, isMulti);
|
||||
var startMap = MapData.GetMapData(isMulti, true, false, mapId);
|
||||
var endMap = MapData.GetMapData(isMulti, false, true, mapId);
|
||||
if (startMap == null || endMap == null) return;
|
||||
var startMap = MapData.GetMapData(pair.IsMulti, true, false, pair.MapId);
|
||||
var endMap = pair.EndKind == ReplayEndKind.End
|
||||
? MapData.GetMapData(pair.IsMulti, false, true, pair.MapId)
|
||||
: MapData.GetMapData(pair.IsMulti, false, false, pair.MapId);
|
||||
if (startMap == null || endMap == null)
|
||||
{
|
||||
Debug.LogWarning($"加载回放存档失败: mapId={pair.MapId}, isMulti={pair.IsMulti}, endKind={pair.EndKind}");
|
||||
return;
|
||||
}
|
||||
|
||||
Main.Instance.StartSpectate(startMap, endMap);
|
||||
}
|
||||
|
||||
private static string FindArchivePath(string directory, string kind, bool isMulti, uint mapId)
|
||||
{
|
||||
var fileName = $"map_archive_{kind}{(isMulti ? "_multi" : "")}_{mapId}.dat";
|
||||
var path = Path.Combine(directory, fileName);
|
||||
if (File.Exists(path)) return path;
|
||||
|
||||
var backupPath = path + ".bak";
|
||||
return File.Exists(backupPath) ? backupPath : null;
|
||||
}
|
||||
|
||||
private static string StripArchiveExtensions(string fileName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileName)) return string.Empty;
|
||||
if (fileName.EndsWith(".bak", StringComparison.OrdinalIgnoreCase))
|
||||
fileName = fileName.Substring(0, fileName.Length - ".bak".Length);
|
||||
if (!fileName.EndsWith(".dat", StringComparison.OrdinalIgnoreCase))
|
||||
return string.Empty;
|
||||
return fileName.Substring(0, fileName.Length - ".dat".Length);
|
||||
}
|
||||
|
||||
private static DateTime Max(DateTime a, DateTime b)
|
||||
{
|
||||
return a > b ? a : b;
|
||||
}
|
||||
|
||||
private static string GetEndKindLabel(ReplayEndKind endKind)
|
||||
{
|
||||
return endKind == ReplayEndKind.End ? "结束存档(end)" : "继续存档(continue)";
|
||||
}
|
||||
|
||||
private enum ReplayEndKind
|
||||
{
|
||||
End,
|
||||
Continue
|
||||
}
|
||||
|
||||
private class MapArchivePair
|
||||
{
|
||||
public uint MapId;
|
||||
public bool IsMulti;
|
||||
public string BeginPath;
|
||||
public string EndPath;
|
||||
public ReplayEndKind EndKind;
|
||||
public DateTime LastModifiedTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Logic.Config;
|
||||
using Logic.CrashSight;
|
||||
using TH1_Logic.Config;
|
||||
using UnityEngine;
|
||||
|
||||
@ -42,10 +43,21 @@ namespace TH1_Logic.Oss
|
||||
public string schema = "th1.player-bug-report.v1";
|
||||
public string reportId;
|
||||
public string createdAtUtc;
|
||||
public string createdAtLocal;
|
||||
public string timezone;
|
||||
public string steamId;
|
||||
public string version;
|
||||
public string unityVersion;
|
||||
public string platform;
|
||||
public string crashSightDeviceId;
|
||||
public string deviceModel;
|
||||
public string deviceName;
|
||||
public string operatingSystem;
|
||||
public string processorType;
|
||||
public int processorCount;
|
||||
public int systemMemorySizeMb;
|
||||
public string graphicsDeviceName;
|
||||
public int graphicsMemorySizeMb;
|
||||
public string description;
|
||||
public int archiveCount;
|
||||
public PlayerBugReportArchiveManifest[] archives = Array.Empty<PlayerBugReportArchiveManifest>();
|
||||
@ -141,10 +153,21 @@ namespace TH1_Logic.Oss
|
||||
{
|
||||
reportId = reportId,
|
||||
createdAtUtc = DateTime.UtcNow.ToString("O"),
|
||||
createdAtLocal = DateTime.Now.ToString("O"),
|
||||
timezone = GetLocalTimezone(),
|
||||
steamId = steamId ?? "",
|
||||
version = string.IsNullOrWhiteSpace(version) ? GetCurrentVersion() : version.Trim(),
|
||||
unityVersion = Application.unityVersion,
|
||||
platform = Application.platform.ToString(),
|
||||
crashSightDeviceId = CrashSightManager.GetCrashSightDeviceId(),
|
||||
deviceModel = SystemInfo.deviceModel,
|
||||
deviceName = SystemInfo.deviceName,
|
||||
operatingSystem = SystemInfo.operatingSystem,
|
||||
processorType = SystemInfo.processorType,
|
||||
processorCount = SystemInfo.processorCount,
|
||||
systemMemorySizeMb = SystemInfo.systemMemorySize,
|
||||
graphicsDeviceName = SystemInfo.graphicsDeviceName,
|
||||
graphicsMemorySizeMb = SystemInfo.graphicsMemorySize,
|
||||
description = description ?? ""
|
||||
};
|
||||
|
||||
@ -378,5 +401,19 @@ namespace TH1_Logic.Oss
|
||||
return _end.LastWriteTimeUtc >= _continue.LastWriteTimeUtc ? _end : _continue;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetLocalTimezone()
|
||||
{
|
||||
try
|
||||
{
|
||||
var offset = DateTimeOffset.Now.Offset;
|
||||
var sign = offset < TimeSpan.Zero ? "-" : "+";
|
||||
return $"{TimeZoneInfo.Local.Id} (UTC{sign}{offset.Duration():hh\\:mm})";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DateTimeOffset.Now.Offset.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,7 +301,8 @@ namespace Logic
|
||||
action.CompleteExecute(param);
|
||||
//action.ExecuteWithoutFullActionPeriod(param);
|
||||
|
||||
if (map.Net.Mode == NetMode.Multi && map.Net.Players.ContainsValue(playerId))
|
||||
if ((map.Net.Mode == NetMode.Multi || map.Net.Mode == NetMode.Spectator)
|
||||
&& map.Net.Players.ContainsValue(playerId))
|
||||
return;
|
||||
if (map.PlayerMap.SelfPlayerData.Id == playerId) return;
|
||||
AIAddMoney(map, playerId);
|
||||
|
||||
@ -38,7 +38,7 @@ namespace Logic.Skill
|
||||
var damage = info.DamageValue;
|
||||
for (var i = 0; i < _level && damage > 1; i++)
|
||||
{
|
||||
damage = Mathf.FloorToInt(damage * 0.5f);
|
||||
damage = UnitData.CeilPositiveToInt(damage * 0.5f);
|
||||
}
|
||||
|
||||
info.DamageValue = Mathf.Max(1, damage);
|
||||
|
||||
@ -36,7 +36,7 @@ namespace Logic.Skill
|
||||
var bonePileSkill = bonePile as BonePileSkill;
|
||||
bonePileSkill.TargetType = targetUnitFullType;
|
||||
};
|
||||
newUnit.Health = (int)(newUnit.GetMaxHealth() * 0.25f);
|
||||
newUnit.Health = UnitData.CeilPositiveToInt(newUnit.GetMaxHealth() * 0.25f);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ namespace Logic.Skill
|
||||
if(!damaged.Add(unit)) continue;
|
||||
if (mapData.IsLeagueOrJustBreakByUnit(unit.Id, self.Id)) continue;
|
||||
var dmg = Table.Instance.CalcDamage(mapData, self, unit);
|
||||
var realDmg = Mathf.FloorToInt(dmg * 0.5f);
|
||||
var realDmg = UnitData.CeilPositiveToInt(dmg * 0.5f);
|
||||
|
||||
var targetRenderer = unit.Renderer(mapData);
|
||||
var settlement = Main.UnitLogic.DamageSettlement(mapData, self, unit, realDmg, DamageType.Splash);
|
||||
|
||||
@ -50,7 +50,7 @@ namespace Logic.Skill
|
||||
// 改为承伤者重定向:伤害减半由 unit(Sakuya) 承受,target 仍正常触发 OnDamaged,
|
||||
// origin 的 OnDamageOther/Moment/HeroTask 链照常跑(Escape 等触发器能正常生效)。
|
||||
info.DamageBearer = unit;
|
||||
info.DamageValue = info.DamageValue / 2;
|
||||
info.DamageValue = UnitData.CeilPositiveToInt(info.DamageValue / 2f);
|
||||
//提前记录unitRednerer(可能会死亡)
|
||||
var unitRenderer = unit.Renderer(Main.MapData);
|
||||
var guardGridRenderer = unit.Grid(Main.MapData)?.Renderer(Main.MapData);
|
||||
|
||||
@ -42,9 +42,7 @@ namespace Logic.Skill
|
||||
// if (info.DamageType == DamageType.ActiveAttack ||
|
||||
// (info.DamageType == DamageType.CounterAttack && info.DamageOrigin.GetSkill(SkillType.RemiliaEgyptianEmpireKill,out var _)))
|
||||
// {
|
||||
// info.DamageOrigin.Health += (int)Math.Round(info.HealthReduceValue * 0.2f);
|
||||
// if (info.DamageOrigin.Health > info.DamageOrigin.GetMaxHealth())
|
||||
// info.DamageOrigin.Health = info.DamageOrigin.GetMaxHealth();
|
||||
// info.DamageOrigin.AddHealth(info.HealthReduceValue * 0.2f);
|
||||
// if (mapData.GetGridDataByUnitId(info.DamageOrigin.Id, out var grid))
|
||||
// grid.Renderer(mapData)?.PlayVFX(new GridVFXParams(GridVFXType.Heal));
|
||||
//
|
||||
@ -57,4 +55,4 @@ namespace Logic.Skill
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,7 +124,7 @@ namespace Logic.Skill
|
||||
if (targetGrid.Terrain == TerrainType.DeepSea &&
|
||||
!player.TechTree.CheckIfHasTechAtom(TechAtom.UnitSkillOCEANMOVE)) continue;
|
||||
mapData.AddUnitData(targetGrid.Id, city.Id, unitFullType, out newUnit1);
|
||||
newUnit1.Health = newUnit1.GetMaxHealth() / 2;
|
||||
newUnit1.Health = UnitData.CeilPositiveToInt(newUnit1.GetMaxHealth() / 2f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -138,7 +138,7 @@ namespace Logic.Skill
|
||||
if (targetGrid.Terrain == TerrainType.DeepSea &&
|
||||
!player.TechTree.CheckIfHasTechAtom(TechAtom.UnitSkillOCEANMOVE)) continue;
|
||||
mapData.AddUnitData(targetGrid.Id, city.Id, unitFullType, out newUnit2);
|
||||
newUnit2.Health = newUnit2.GetMaxHealth() / 2;
|
||||
newUnit2.Health = UnitData.CeilPositiveToInt(newUnit2.GetMaxHealth() / 2f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ namespace Logic.Skill
|
||||
var bonePile = skill as BonePileSkill;
|
||||
bonePile.TargetType = info.DamageTarget.UnitFullType;
|
||||
bonePile.TargetCityId = info.DamageTargetCity.Id;
|
||||
bone.Health = (int)(bone.GetMaxHealth() * 0.25f);
|
||||
bone.Health = UnitData.CeilPositiveToInt(bone.GetMaxHealth() * 0.25f);
|
||||
}
|
||||
var boneGrid = bone.Grid(mapData);
|
||||
if (boneGrid != null)
|
||||
@ -80,7 +80,7 @@ namespace Logic.Skill
|
||||
var bonePile = skill as BonePileSkill;
|
||||
bonePile.TargetType = info.DamageTarget.UnitFullType;
|
||||
bonePile.TargetCityId = info.DamageTargetCity.Id;
|
||||
bone.Health = (int)(bone.GetMaxHealth() * 0.25f);
|
||||
bone.Health = UnitData.CeilPositiveToInt(bone.GetMaxHealth() * 0.25f);
|
||||
}
|
||||
var boneGrid = bone.Grid(mapData);
|
||||
if (boneGrid != null)
|
||||
|
||||
@ -38,9 +38,7 @@ namespace Logic.Skill
|
||||
if (info.DamageOrigin == null || info.DamageTarget == null) return;
|
||||
if (info.DamageType != DamageType.ActiveAttack && info.DamageType != DamageType.CounterAttack) return;
|
||||
|
||||
info.DamageOrigin.Health += (int)Math.Round(info.HealthReduceValue * 0.5f);
|
||||
if (info.DamageOrigin.Health > info.DamageOrigin.GetMaxHealth())
|
||||
info.DamageOrigin.Health = info.DamageOrigin.GetMaxHealth();
|
||||
info.DamageOrigin.AddHealth(info.HealthReduceValue * 0.5f);
|
||||
if (mapData.GetGridDataByUnitId(info.DamageOrigin.Id, out var grid))
|
||||
grid.Renderer(mapData)?.PlayVFXInSight(new GridVFXParams(GridVFXType.Heal));
|
||||
|
||||
@ -75,4 +73,4 @@ namespace Logic.Skill
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,9 +50,7 @@ namespace Logic.Skill
|
||||
if (info.DamageOrigin == null || info.DamageTarget == null) return;
|
||||
if (info.DamageType != DamageType.ActiveAttack && info.DamageType != DamageType.CounterAttack) return;
|
||||
|
||||
info.DamageOrigin.Health += (int)Math.Round(info.HealthReduceValue * 0.3f);
|
||||
if (info.DamageOrigin.Health > info.DamageOrigin.GetMaxHealth())
|
||||
info.DamageOrigin.Health = info.DamageOrigin.GetMaxHealth();
|
||||
info.DamageOrigin.AddHealth(info.HealthReduceValue * 0.3f);
|
||||
if (mapData.GetGridDataByUnitId(info.DamageOrigin.Id, out var grid))
|
||||
grid.Renderer(mapData)?.PlayVFXInSight(new GridVFXParams(GridVFXType.Heal));
|
||||
|
||||
@ -87,4 +85,4 @@ namespace Logic.Skill
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,8 +139,7 @@ namespace Logic.Skill
|
||||
bonePile.TargetType = target.UnitFullType;
|
||||
bonePile.TargetCityId = targetCity.Id;
|
||||
}
|
||||
bone.Health = bone.GetMaxHealth() / 4;
|
||||
if (bone.Health < 1) bone.Health = 1;
|
||||
bone.Health = Mathf.Max(1, UnitData.CeilPositiveToInt(bone.GetMaxHealth() / 4f));
|
||||
var boneGrid = bone.Grid(map);
|
||||
if (boneGrid != null)
|
||||
{
|
||||
|
||||
@ -758,7 +758,12 @@ namespace Logic
|
||||
public void UnitDie(MapData map, UnitData unit, int dmg)
|
||||
{
|
||||
unit.Health = 0;
|
||||
if (!map.GetGridDataByUnitId(unit.Id, out var targetGrid)) return;
|
||||
if (!map.GetGridDataByUnitId(unit.Id, out var targetGrid))
|
||||
{
|
||||
LogSystem.LogWarning($"UnitDie: unitId={unit.Id} missing grid relation, removing stale unit data.");
|
||||
map.SetUnitDataDie(unit);
|
||||
return;
|
||||
}
|
||||
|
||||
bool notifymoment = false;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user