游戏保存中的 C# UTC 转换和夏令时
C# UTC conversion and Daylight Saving Time in game save
所以,我做了一个游戏,在保存和加载时需要检查时间。
相关加载代码块:
playerData = save.LoadPlayer();
totalSeconds = playerData.totalSeconds;
System.DateTime stamp = System.DateTime.MinValue;
if (!System.DateTime.TryParse(playerData.timeStamp, out stamp)) {
playerData.timeStamp = System.DateTime.UtcNow.ToString("o");
stamp = System.DateTime.Parse(playerData.timeStamp);
}
stamp = stamp.ToUniversalTime();
loadStamp = System.DateTime.UtcNow;
long elapsedSeconds = (long)(System.DateTime.UtcNow - stamp).TotalSeconds;
if (elapsedSeconds < 0) {
ui.Cheater();
}
显然,这一切所做的只是检查是否可以解析当前保存的时间戳 - 如果可以,我们确保它是 UTC,如果不是,我们将时间戳设置为当前时间并继续。如果加载的时间戳和当前时间之间经过的时间是负数,我们就知道玩家已经弄乱了他们的时钟来利用系统。
夏令时时钟向后拨一个小时时会出现潜在问题。
这是保存函数中的相关代码,如果重要:
if (loadStamp == System.DateTime.MinValue) {
loadStamp = System.DateTime.UtcNow;
}
playerData.timeStamp = loadStamp.AddSeconds(sessionSeconds).ToString("o");
我的问题是:
当前使用的这种方法是否会在时钟倒退并错误地认为玩家作弊时引起任何问题?
提前致谢。
编辑:
忘了补充一点,当时间设置为时钟向后移动时,似乎不会在计算机上造成任何问题,但游戏是移动的。再一次,如果这很重要的话。不太确定。到目前为止,我对基于时间的奖励和游戏中的东西做的不多。
更新 我已经根据@theMayer 的评论显着更新了这个答案,事实上虽然我错了,但它可能突出了一个更大的问题。
我认为这里存在一个问题,因为代码正在读取 UTC 时间,将其转换为本地时间,然后再将其转换回 UTC。
保存例程记录用往返格式说明符 o
表示的 loadStamp
的值,并且由于 loadStamp
始终从 DateTime.UtcNow
设置,因此存储的值文件中的始终是 UTC 时间,尾随 "Z" 表示 UTC 时间。
例如:
2018-02-18T01:30:00.0000000Z ( = 2018-02-17T23:30:00 in UTC-02:00 )
此问题是在 Brazil 时区报告的,UTC 偏移量为 UTC-02:00 (BRST) 直到 2018-02-18T02:00:00Z,UTC 偏移量为 UTC-03 :00 (BRT) 之后。
代码到达这一行:
if (!System.DateTime.TryParse(playerData.timeStamp, out stamp)) {
DateTime.TryParse() (which uses the same rules as DateTime.Parse()) 会遇到这个字符串。然后它将 UTC 时间转换为本地时间,并将 stamp
设置为等于:
2018-02-17T23:30:00 DateTimeKind.Local
代码到达:
stamp = stamp.ToUniversalTime();
在这一点上,stamp
应该 表示一个模糊时间,即作为有效 BRST 和有效 BRT 时间存在的时间,并且 MSDN状态:
If the date and time instance value is an ambiguous time, this method assumes that it is a standard time. (An ambiguous time is one that can map either to a standard time or to a daylight saving time in the local time zone)
这意味着 .NET 可能 正在更改任何不明确的 DateTime 值的 UTC 值,这些值被转换为本地时间并再次转换回来。
尽管文档清楚地说明了这一点,但我无法在巴西时区重现此行为。我还在调查这个。
我对这类问题的处理方法是使用 DateTimeOffset
类型而不是 DateTime
。它表示与本地时间、时区或夏令时无关的 point-in-time。
关闭这个洞的另一种方法是改变:
if (!System.DateTime.TryParse(playerData.timeStamp, out stamp)) {
playerData.timeStamp = System.DateTime.UtcNow.ToString("o");
stamp = System.DateTime.Parse(playerData.timeStamp);
}
stamp = stamp.ToUniversalTime();
到
if (!System.DateTime.TryParse(playerData.timeStamp, null, DateTimeStyles.RoundtripKind, out stamp)) {
stamp = System.DateTime.UtcNow;
playerData.timeStamp = stamp.ToString("o");
}
再次假设保存的 playerData.timeStamp
将始终来自 UTC 日期,因此处于 "Z" 时区,添加 DateTimeStyles.RoundtripKind
应该意味着它被直接解析为 DateTimeKind.Utc
而不是转换为本地时间 DateTimeKind.Local
。它还消除了调用 ToUniversalTime()
将其转换回来的需要。
希望对您有所帮助
从表面上看,此代码似乎没有任何明显错误。
在做DateTime
比较操作时,一定要保证被比较DateTime
的时区一致。该框架只比较实例的值,而不管您认为它们处于哪个时区。确保 DateTime
个实例之间的时区一致取决于您。在这种情况下似乎正在这样做,因为在与其他 UTC 时间进行比较之前,时间已从本地时间转换为 UTC:
stamp = stamp.ToUniversalTime();
elapsedSeconds = (long)(System.DateTime.UtcNow - stamp).TotalSeconds;
一些注意事项
经常让人们感到困惑的一个项目是重复查询时间值(每次调用 DateTime.UtcNow
)- 可能 每次都会产生不同的值。然而,差异将是无穷小的,并且大部分时间为零,因为此代码将比 resolution of the processor clock.
执行得更快
另一个我在评论中提到的事实,用于将 DateTime
写入字符串的 "Round Trip Format Specifier" 旨在保留时区信息 - 在这种情况下,它 应该在时间上加一个"Z"来表示UTC。转换回来后(通过 TryParse
),如果 Z 存在,解析器会将此次时间从 UTC 转换为本地时间。 这可能是一个重要的陷阱,因为它导致实际 DateTime
值与序列化为字符串的值不同,并且在某种程度上与 .NET 框架处理的所有其他方式相反 DateTime
's(忽略时区信息)。如果您遇到 "Z" 不存在于传入字符串中的情况,但该字符串否则为 UTC,那么您就会遇到问题,因为它将比较其值已被第二次调整的 UTC 时间(从而使其成为 UTC +2 的时间)。
还应注意,在 .Net 1.1 中,DateTime.ToUniversalTime()
不是 idempotent function. It will offset the DateTime
instance by the difference in time zone between the local time zone and UTC each time it is called. From the documentation:
This method assumes that the current DateTime holds the local time value, and not a UTC time. Therefore, each time it is run, the current method performs the necessary modifications on the DateTime to derive the UTC time, whether the current DateTime holds the local time or not.
使用更高版本框架的程序可能需要也可能不需要担心这一点,具体取决于使用情况。
所以,我做了一个游戏,在保存和加载时需要检查时间。
相关加载代码块:
playerData = save.LoadPlayer();
totalSeconds = playerData.totalSeconds;
System.DateTime stamp = System.DateTime.MinValue;
if (!System.DateTime.TryParse(playerData.timeStamp, out stamp)) {
playerData.timeStamp = System.DateTime.UtcNow.ToString("o");
stamp = System.DateTime.Parse(playerData.timeStamp);
}
stamp = stamp.ToUniversalTime();
loadStamp = System.DateTime.UtcNow;
long elapsedSeconds = (long)(System.DateTime.UtcNow - stamp).TotalSeconds;
if (elapsedSeconds < 0) {
ui.Cheater();
}
显然,这一切所做的只是检查是否可以解析当前保存的时间戳 - 如果可以,我们确保它是 UTC,如果不是,我们将时间戳设置为当前时间并继续。如果加载的时间戳和当前时间之间经过的时间是负数,我们就知道玩家已经弄乱了他们的时钟来利用系统。
夏令时时钟向后拨一个小时时会出现潜在问题。
这是保存函数中的相关代码,如果重要:
if (loadStamp == System.DateTime.MinValue) {
loadStamp = System.DateTime.UtcNow;
}
playerData.timeStamp = loadStamp.AddSeconds(sessionSeconds).ToString("o");
我的问题是:
当前使用的这种方法是否会在时钟倒退并错误地认为玩家作弊时引起任何问题?
提前致谢。
编辑: 忘了补充一点,当时间设置为时钟向后移动时,似乎不会在计算机上造成任何问题,但游戏是移动的。再一次,如果这很重要的话。不太确定。到目前为止,我对基于时间的奖励和游戏中的东西做的不多。
更新 我已经根据@theMayer 的评论显着更新了这个答案,事实上虽然我错了,但它可能突出了一个更大的问题。
我认为这里存在一个问题,因为代码正在读取 UTC 时间,将其转换为本地时间,然后再将其转换回 UTC。
保存例程记录用往返格式说明符 o
表示的 loadStamp
的值,并且由于 loadStamp
始终从 DateTime.UtcNow
设置,因此存储的值文件中的始终是 UTC 时间,尾随 "Z" 表示 UTC 时间。
例如:
2018-02-18T01:30:00.0000000Z ( = 2018-02-17T23:30:00 in UTC-02:00 )
此问题是在 Brazil 时区报告的,UTC 偏移量为 UTC-02:00 (BRST) 直到 2018-02-18T02:00:00Z,UTC 偏移量为 UTC-03 :00 (BRT) 之后。
代码到达这一行:
if (!System.DateTime.TryParse(playerData.timeStamp, out stamp)) {
DateTime.TryParse() (which uses the same rules as DateTime.Parse()) 会遇到这个字符串。然后它将 UTC 时间转换为本地时间,并将 stamp
设置为等于:
2018-02-17T23:30:00 DateTimeKind.Local
代码到达:
stamp = stamp.ToUniversalTime();
在这一点上,stamp
应该 表示一个模糊时间,即作为有效 BRST 和有效 BRT 时间存在的时间,并且 MSDN状态:
If the date and time instance value is an ambiguous time, this method assumes that it is a standard time. (An ambiguous time is one that can map either to a standard time or to a daylight saving time in the local time zone)
这意味着 .NET 可能 正在更改任何不明确的 DateTime 值的 UTC 值,这些值被转换为本地时间并再次转换回来。
尽管文档清楚地说明了这一点,但我无法在巴西时区重现此行为。我还在调查这个。
我对这类问题的处理方法是使用 DateTimeOffset
类型而不是 DateTime
。它表示与本地时间、时区或夏令时无关的 point-in-time。
关闭这个洞的另一种方法是改变:
if (!System.DateTime.TryParse(playerData.timeStamp, out stamp)) {
playerData.timeStamp = System.DateTime.UtcNow.ToString("o");
stamp = System.DateTime.Parse(playerData.timeStamp);
}
stamp = stamp.ToUniversalTime();
到
if (!System.DateTime.TryParse(playerData.timeStamp, null, DateTimeStyles.RoundtripKind, out stamp)) {
stamp = System.DateTime.UtcNow;
playerData.timeStamp = stamp.ToString("o");
}
再次假设保存的 playerData.timeStamp
将始终来自 UTC 日期,因此处于 "Z" 时区,添加 DateTimeStyles.RoundtripKind
应该意味着它被直接解析为 DateTimeKind.Utc
而不是转换为本地时间 DateTimeKind.Local
。它还消除了调用 ToUniversalTime()
将其转换回来的需要。
希望对您有所帮助
从表面上看,此代码似乎没有任何明显错误。
在做DateTime
比较操作时,一定要保证被比较DateTime
的时区一致。该框架只比较实例的值,而不管您认为它们处于哪个时区。确保 DateTime
个实例之间的时区一致取决于您。在这种情况下似乎正在这样做,因为在与其他 UTC 时间进行比较之前,时间已从本地时间转换为 UTC:
stamp = stamp.ToUniversalTime();
elapsedSeconds = (long)(System.DateTime.UtcNow - stamp).TotalSeconds;
一些注意事项
经常让人们感到困惑的一个项目是重复查询时间值(每次调用 DateTime.UtcNow
)- 可能 每次都会产生不同的值。然而,差异将是无穷小的,并且大部分时间为零,因为此代码将比 resolution of the processor clock.
另一个我在评论中提到的事实,用于将 DateTime
写入字符串的 "Round Trip Format Specifier" 旨在保留时区信息 - 在这种情况下,它 应该在时间上加一个"Z"来表示UTC。转换回来后(通过 TryParse
),如果 Z 存在,解析器会将此次时间从 UTC 转换为本地时间。 这可能是一个重要的陷阱,因为它导致实际 DateTime
值与序列化为字符串的值不同,并且在某种程度上与 .NET 框架处理的所有其他方式相反 DateTime
's(忽略时区信息)。如果您遇到 "Z" 不存在于传入字符串中的情况,但该字符串否则为 UTC,那么您就会遇到问题,因为它将比较其值已被第二次调整的 UTC 时间(从而使其成为 UTC +2 的时间)。
还应注意,在 .Net 1.1 中,DateTime.ToUniversalTime()
不是 idempotent function. It will offset the DateTime
instance by the difference in time zone between the local time zone and UTC each time it is called. From the documentation:
This method assumes that the current DateTime holds the local time value, and not a UTC time. Therefore, each time it is run, the current method performs the necessary modifications on the DateTime to derive the UTC time, whether the current DateTime holds the local time or not.
使用更高版本框架的程序可能需要也可能不需要担心这一点,具体取决于使用情况。