简体   繁体   中英

C# UTC conversion and Daylight Saving Time in game save

So, I've made a game in which it is required to check the time when saving and loading.

The relevant chunk of loading code:

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

Obviously, all this does is check to see if the currently saved timestamp can be parsed - if so, we make sure it's UTC, if not, we set the stamp to the current time and continue. If the elapsed time between the loaded timestamp and current time is negative, we know the player has messed with their clock to exploit the system.

The potential problem arises when the clocks move an hour back for DST.

This is the relevant code in the save function, if it matters:

if (loadStamp == System.DateTime.MinValue) {
    loadStamp = System.DateTime.UtcNow;
}

playerData.timeStamp = loadStamp.AddSeconds(sessionSeconds).ToString("o");

My question is:

Will this currently used method potentially cause any problems when the clocks move back and falsely deem players cheaters?

Thank you in advance.

EDIT: Forgot to add that it seems to not cause any problems on the computer when the time is set to when the clocks move back, but the game is mobile. Again, if that matters at all. Not quite sure. I've not done much with time-based rewards and stuff in games thus far.

Update I have significantly updated this answer in respect of comments made by @theMayer and the fact that while I was wrong, it may have highlighted a bigger issue.


I believe there is an issue here in the fact that the code is reading the UTC time in, converting it to local time, then converting it back to UTC.

The save routine records the value of loadStamp expressed with the Round Trip format specifier o , and as loadStamp is always set from DateTime.UtcNow , the value stored in the file will always be a UTC time with a trailing "Z" indicating UTC time.

For example:

2018-02-18T01:30:00.0000000Z ( = 2018-02-17T23:30:00 in UTC-02:00 )

The issue was reported in the Brazil time zone, with a UTC offset of UTC-02:00 (BRST) until 2018-02-18T02:00:00Z and a UTC offset of UTC-03:00 (BRT) after.

The code reaches this line:

if (!System.DateTime.TryParse(playerData.timeStamp, out stamp)) {

DateTime.TryParse() (which uses the same rules as DateTime.Parse() ) will encounter this string. It will then convert the UTC time into a local time, and set stamp to equal:

2018-02-17T23:30:00 DateTimeKind.Local

The code then reaches:

stamp = stamp.ToUniversalTime();

At this point, stamp should represent an Ambiguous time, ie one that exists as a valid BRST and a valid BRT time, and MSDN states:

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)

This means that .NET could be changing the UTC value of any ambiguous DateTime values that are converted to Local time and back again.

Although the documentation states this clearly, I have been unable to reproduce this behaviour in the Brazilian time zone. I am still investigating this.


My approach to this type of issue is to use the DateTimeOffset type instead of DateTime . It represents a point-in-time that is irrelevant of local time, time zones, or Daylight Savings.

An alternative approach to closing this hole would be to change:

if (!System.DateTime.TryParse(playerData.timeStamp, out stamp)) {
    playerData.timeStamp = System.DateTime.UtcNow.ToString("o");
    stamp = System.DateTime.Parse(playerData.timeStamp);
}
stamp = stamp.ToUniversalTime();

to

if (!System.DateTime.TryParse(playerData.timeStamp, null, DateTimeStyles.RoundtripKind, out stamp)) {
    stamp = System.DateTime.UtcNow;
    playerData.timeStamp = stamp.ToString("o");
}

Again assuming that the saved playerData.timeStamp will always be from a UTC date and therefore be in "Z" timezone, adding the DateTimeStyles.RoundtripKind should mean it gets parsed straight into DateTimeKind.Utc and not converted into Local time as DateTimeKind.Local . It also eliminates the need to call ToUniversalTime() on it to convert it back.

Hope this helps

On the surface, there does not appear to be anything obviously wrong with this code.

When doing DateTime comparison operations, it is important to ensure that the time zone of the compared DateTime 's are consistent. The framework only compares the values of the instances, irrespective of whatever time zone you think they are in. It is up to you to ensure consistent time zones between DateTime instances. It looks like that is being done in this case, as times are converted from local to UTC prior to being compared with other UTC times:

stamp = stamp.ToUniversalTime();
elapsedSeconds = (long)(System.DateTime.UtcNow - stamp).TotalSeconds;

Some Caveats

One item that often trips people up is that the time value is repeatedly queried (each call to DateTime.UtcNow ) - which could result in different values each time. However, the difference would be infinitesimal, and most of the time zero as this code will execute faster than the resolution of the processor clock .

Another fact, which I brought up in the comments, the "Round Trip Format Specifier" used to write the DateTime to string is intended to preserve time zone information - in this case, it should add a "Z" to the time to denote UTC. Upon conversion back (via TryParse ), the parser will convert this time from the UTC to a local time if the Z is present. This can be a significant gotcha as it results in an actual DateTime value that is different from the one serialized to the string, and in a way is contrary to every other way that the .NET framework handles DateTime 's (which is to ignore time zone info). If you have a case where the "Z" is not present in the incoming string, but the string is otherwise UTC, then you have a problem there because it will be comparing a UTC time whose value has been adjusted a second time (thus making it the time of UTC +2).

It should also be noted that in .Net 1.1, DateTime.ToUniversalTime() is NOT an 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.

Programs using a later version of the framework may or may not have to worry about this, depending on usage.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM