Compare commits

...

2 Commits

Author SHA1 Message Date
575b8a288e 增加单位删除新增的格子保护 2026-05-21 18:41:43 +08:00
99473c5679 增加一些数据保护 2026-05-21 18:40:33 +08:00
25 changed files with 320 additions and 72 deletions

View File

@ -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.

View File

@ -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`

View File

@ -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` 并替换为该汇报内的存档。

View File

@ -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`,该文件应只保留在本机。

View File

@ -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)),

View File

@ -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()

View File

@ -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
}
}
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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 初始化演出管理器

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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
}
}
}
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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)
{

View File

@ -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;