Author Topic: Issues with creating an in-game local Date/Time system  (Read 246 times)

Hi all!

I am tinkering with this idea of creating some sort of space trading sim in AGS and, after managing to scatter some "VIPs" in the universe, I wondered if I could have them run their own errands\missions.
These errands\missions, along with other features, are all going to have a start date and a due date. This led me to implement an internal clock and also a "calendar", in order to make date differences calculations.

I treated dates as managed structs as defined here:
Code: Adventure Game Studio
  1. // Time.ash
  2.  
  3. managed struct Date
  4. {
  5.    int Hour;
  6.    int Minute;
  7.    int Second;
  8.    
  9.    int Year;
  10.    int Month;
  11.    int Day;
  12.    
  13.    import bool IsLeap();
  14.    import function Add(int days);
  15. };
  16.  
  17. import int DateToRaw(int y, int m, int d); // converts years, months, days to days
  18. import Date* RawToDate(int rawDate); // converts days to years, months, days
  19.  
  20. import function Clock(int speed); // speed = 40*x, where 40 is 1 second in game loops
  21.  
  22. import Date* StartDate; // set to 01-01-3200
  23. import Date* CurrentDate; // updated by the clock

Code: Adventure Game Studio
  1. // Time.asc
  2.  
  3. Date* StartDate;
  4. Date* CurrentDate;
  5.  
  6. int tick; // Game loops
  7.  
  8. // I know it is kind of a duplicate but I was testing different solutions
  9. bool leapyear(int year)
  10. {
  11.    if (year %   4 != 0) return false;
  12.    if (year % 100 != 0) return true;
  13.    if (year % 400 != 0) return false;
  14.    return true;
  15.    
  16.    //Alternative one-liner
  17.    /* return year%4 == 0 && (year%100 != 0 || year%400 == 0); */
  18. }
  19.  
  20. // I know it is kind of a duplicate but I was testing different solutions
  21. bool Date::IsLeap()
  22. {
  23.    return leapyear(this.Year);
  24. }
  25.  
  26. // This increases Date of x number of days, and keeps track of months and years
  27. // It is not used in this example here but it works perfectly fine
  28. function Date::Add(int days)
  29. {
  30.    for (int i = 0; i < days; i++)
  31.    {
  32.       this.Day++;
  33.      
  34.       if (this.Day > 28 && this.Month == 2)
  35.       {
  36.          if (!this.IsLeap())
  37.          {
  38.             this.Day = 1;
  39.             this.Month++;
  40.          }
  41.          else
  42.          {
  43.             if (this.Day > 29)
  44.             {
  45.                this.Day = 1;
  46.                this.Month++;
  47.             }
  48.          }
  49.       }
  50.       else if (this.Day > 30 && (this.Month == 11 || this.Month == 4 || this.Month == 6 || this.Month == 9))
  51.       {
  52.          this.Day = 1;
  53.          this.Month++;
  54.       }
  55.       else if (this.Day > 31 && (this.Month == 1 || this.Month == 3 || this.Month == 5 || this.Month == 7 || this.Month == 8 || this.Month == 10 || this.Month == 12))
  56.       {
  57.          this.Day = 1;
  58.          this.Month++;
  59.          if (this.Month == 13)
  60.          {
  61.             this.Month = 1;
  62.             this.Year++;
  63.          }
  64.       }
  65.    }
  66. }
  67.  
  68. // Convert YYYY-MM-DD to days
  69. // Found on stackoverflow, here https://stackoverflow.com/a/14224238
  70. int DateToRaw(int y, int m, int d)
  71. {
  72.    /* Rata Die day one is 0001-01-01 */
  73.    if (m < 3)
  74.    {
  75.       y--;
  76.       m += 12;
  77.    }
  78.    return 365*y + y/4 - y/100 + y/400 + (153*m - 457)/5 + d - 306;
  79. }
  80.  
  81. // Convert days to YYYY-MM-DD
  82. // includes code provided by vga256 and most of the code from the Date.Add() function
  83. Date* RawToDate(int rawDate)
  84. {
  85.    // Initialize a new Date object
  86.    Date* d = new Date;
  87.    d.Year = 1;
  88.    d.Month = 1;
  89.    d.Day = 1;
  90.    d.Second = 0;
  91.    d.Minute = 0;
  92.    d.Hour = 0;
  93.    
  94.    int rawdays = rawDate;
  95.    int howmanyleapyears;
  96.    
  97.    // To make it start at 3200 subtract 1
  98.    d.Year = rawdays / 365;
  99.    d.Year--; // *
  100.    
  101.    // How many leap years from year 0001 to rawDate?
  102.    //
  103.    // PLEASE NOTE: I do this to keep the number of iterations low
  104.    // in order to prevent the loop from hanging
  105.    for (int i = 1; i < d.Year; i++)
  106.    {
  107.       if (leapyear(i)) howmanyleapyears++;
  108.    }
  109.    
  110.    // If the current year is a leap year then calculate the
  111.    // remainder out of 366 days
  112.    if (d.IsLeap()) rawdays = (rawdays - howmanyleapyears) % 366;
  113.    
  114.    // Otherwise calculate the remainder out of 365 days
  115.    else rawdays = (rawdays - howmanyleapyears) % 365;
  116.  
  117.    // Same as the one in the Clock() function
  118.    for (int i = 0; i < rawdays; i++)
  119.    {
  120.       d.Day++;
  121.      
  122.       if (d.Day > 28 && d.Month == 2)
  123.       {
  124.          if (!d.IsLeap())
  125.          {
  126.             d.Day = 1;
  127.             d.Month++;
  128.          }
  129.          else
  130.          {
  131.             if (d.Day > 29)
  132.             {
  133.                d.Day = 1;
  134.                d.Month++;
  135.             }
  136.          }
  137.       }
  138.       else if (d.Day > 30 && (d.Month == 11 || d.Month == 4 || d.Month == 6 || d.Month == 9))
  139.       {
  140.          d.Day = 1;
  141.          d.Month++;
  142.       }
  143.       else if (d.Day > 31 && (d.Month == 1 || d.Month == 3 || d.Month == 5 || d.Month == 7 || d.Month == 8 || d.Month == 10 || d.Month == 12))
  144.       {
  145.          d.Day = 1;
  146.          d.Month++;
  147.          if (d.Month == 13)
  148.          {
  149.             d.Month = 1;
  150.             d.Year++;
  151.          }
  152.       }
  153.    }
  154.    return d;
  155. }
  156.  
  157. // Tick tock!
  158. // The clock works perfectly fine
  159. function Clock(int speed)
  160. {
  161.    tick -= 1*speed;
  162.    if (tick <= 0)
  163.    {
  164.       CurrentDate.Second++;
  165.       if (CurrentDate.Second == 60)
  166.       {
  167.          CurrentDate.Second = 0;
  168.          CurrentDate.Minute++;
  169.          if (CurrentDate.Minute == 60)
  170.          {
  171.             CurrentDate.Minute = 0;
  172.             CurrentDate.Hour++;
  173.             if (CurrentDate.Hour == 24)
  174.             {
  175.                CurrentDate.Hour = 0;
  176.                CurrentDate.Day++;
  177.                if (CurrentDate.Day > 28 && CurrentDate.Month == 2)
  178.                {
  179.                   if (!CurrentDate.IsLeap())
  180.                   {
  181.                      CurrentDate.Day = 1;
  182.                      CurrentDate.Month++;
  183.                   }
  184.                   else
  185.                   {
  186.                      if (CurrentDate.Day > 29)
  187.                      {
  188.                         CurrentDate.Day = 1;
  189.                         CurrentDate.Month++;
  190.                      }
  191.                   }
  192.                }
  193.                if (CurrentDate.Day > 30 && (CurrentDate.Month == 11 || CurrentDate.Month == 4 || CurrentDate.Month == 6 || CurrentDate.Month == 9))
  194.                {
  195.                   CurrentDate.Day = 1;
  196.                   CurrentDate.Month++;
  197.                }
  198.                else if (CurrentDate.Day > 31 && (CurrentDate.Month == 1 || CurrentDate.Month == 3 || CurrentDate.Month == 5 || CurrentDate.Month == 7 || CurrentDate.Month == 8 || CurrentDate.Month == 10 || CurrentDate.Month == 12))
  199.                {
  200.                   CurrentDate.Day = 1;
  201.                   CurrentDate.Month++;
  202.                   if (CurrentDate.Month == 13)
  203.                   {
  204.                      CurrentDate.Month = 1;
  205.                      CurrentDate.Year++;
  206.                   }
  207.                }                      
  208.             }
  209.          }
  210.       }
  211.       tick = 40;
  212.    }  
  213. }
  214.  
  215. function repeatedly_execute()
  216. {
  217.    // Runs and displays the clock on screen in a certain room
  218.    if (player.Room == 2)
  219.    {
  220.       Clock(1);
  221.       String s = String.Format("%.2d-%.2d-%.2d \\ %.2d:%.2d:%.2d", CurrentDate.Day, CurrentDate.Month, CurrentDate.Year, CurrentDate.Hour, CurrentDate.Minute, CurrentDate.Second);
  222.       lblCurrentTime.Text = s;
  223.    }
  224. }
  225.  
  226. function game_start()
  227. {
  228.    StartDate = new Date;
  229.    CurrentDate = new Date;
  230.    
  231.    tick = 40;
  232.    
  233.    StartDate.Year = 3200;
  234.    CurrentDate.Year = StartDate.Year;
  235.    
  236.    StartDate.Month = 1;
  237.    CurrentDate.Month = StartDate.Month;
  238.    
  239.    StartDate.Day = 31;
  240.    CurrentDate.Day = StartDate.Day;
  241.    
  242.    StartDate.Second = 50;
  243.    CurrentDate.Second = StartDate.Second;
  244.    
  245.    StartDate.Minute = 59;
  246.    CurrentDate.Minute = StartDate.Minute;
  247.    
  248.    StartDate.Hour = 23;
  249.    CurrentDate.Hour = StartDate.Hour;
  250.    String s = String.Format("%.2d-%.2d-%.2d \\ %.2d:%.2d:%.2d", CurrentDate.Day, CurrentDate.Month, CurrentDate.Year, CurrentDate.Hour, CurrentDate.Minute, CurrentDate.Second);
  251.    lblCurrentTime.Text = s;  
  252. }
  253.  
  254. export CurrentDate;
  255. export StartDate;

Last but not least, I have bound to a key this block of code, which calls the above function and displays the returned string:
Code: Adventure Game Studio
  1. String s;
  2. int a = DateToRaw(CurrentDate.Year, CurrentDate.Month, CurrentDate.Day); // = 1168441
  3. Date* b;
  4. b = RawToDate(a);
  5. s = String.Format("%.2d-%.2d-%.2d", b.Day, b.Month, b.Year);
  6. return s;

Now, given all of this, the problem I'm having (actually me and vga256 are having, since he is helping me a great deal to crack this) is that, when I call this function:

1) If I don't subtract 1 from d.Year (check line 99 of the Time.asc): Months and Days show correctly, but the year is off.


2) If I subtract 1 from d.Year: the year is fine but Months and Days are quite off.


Does anyone have an idea why this is happening?

To give you an idea of what my ideal outcome would be, at the end of the day, here is a mockup:


Thanks a million in advance!
« Last Edit: 13 Jun 2018, 20:09 by LostTrainDude »
"We do not stop playing because we grow old, we grow old because we stop playing."

Snarky

  • Global Moderator
  • Mittens Earl
  • Private Insultant
    • I can help with proof reading
    •  
    • I can help with translating
    •  
Re: Issues with creating an in-game local Date/Time system
« Reply #1 on: 13 Jun 2018, 20:20 »
Without delving into the code (and I see that you've got the calculation from Stackoverflow, which would require further study), I'm guessing it's very likely to do with starting the month and day counts at 1 instead of 0. It messes with any calculations that use division or modulo.

tzachs

  • AGS Baker
  • Mittens Vassal
  • Parking Goat- games that goats like!
    • I can help with translating
    •  
    • tzachs worked on a game that was nominated for an AGS Award!
Re: Issues with creating an in-game local Date/Time system
« Reply #2 on: 14 Jun 2018, 00:38 »
Well, the hacky solution (without understanding the code, just based on the behavior you're describing) would be to move the "reduce one year" line to the end of the function, just before the return.

Note, though, that dealing with time is incredibly complex with like a billion of edge cases, so no matter what, test a lot.
Some reading material to appreciate the complexity:
https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time
https://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time

Re: Issues with creating an in-game local Date/Time system
« Reply #3 on: 14 Jun 2018, 08:24 »
Without delving into the code (and I see that you've got the calculation from Stackoverflow, which would require further study), I'm guessing it's very likely to do with starting the month and day counts at 1 instead of 0. It messes with any calculations that use division or modulo.

I guess that part of the issue is that, starting from 0001-01-01 generates an incredibly high (and likely useless) number (1168441 days from 0001-01-01 to 3200-01-31). Because of this I can't run just the iteration because of the iteration limit for the for loop.

Well, the hacky solution (without understanding the code, just based on the behavior you're describing) would be to move the "reduce one year" line to the end of the function, just before the return.

Note, though, that dealing with time is incredibly complex with like a billion of edge cases, so no matter what, test a lot.
Some reading material to appreciate the complexity:
https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time
https://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time
The hacky solution works momentarily, but 3201 is not a leap year, while 3200 is, so you end up with this issue :( :



(OT: That website is amazing and not only for the falsehoods about time! Thanks for sharing!)
"We do not stop playing because we grow old, we grow old because we stop playing."

Snarky

  • Global Moderator
  • Mittens Earl
  • Private Insultant
    • I can help with proof reading
    •  
    • I can help with translating
    •  
Re: Issues with creating an in-game local Date/Time system
« Reply #4 on: 14 Jun 2018, 13:30 »
What do you need the int representation for, anyway? If you just want to store it, you could trivially encode it as

Code: Adventure Game Studio
  1.  int dateCode = this.Day + this.Month * 100 + this.Year * 10000; // Gives YYYYMMDD

And decode it as:

Code: Adventure Game Studio
  1.   int day = dateCode % 100;
  2.   int month = (datecode / 100) % 100;
  3.   int year = datecode / 10000;

selmiak

  • ǝsıɔɹǝxǝ ʞɔǝu puɐ uıɐɹq
    • I can help with play testing
    •  
    • I can help with proof reading
    •  
    • I can help with translating
    •  
    • I can help with web design
    •  
    • selmiak worked on a game that was nominated for an AGS Award!
Re: Issues with creating an in-game local Date/Time system
« Reply #5 on: 14 Jun 2018, 18:11 »
I guess that part of the issue is that, starting from 0001-01-01 generates an incredibly high (and likely useless) number (1168441 days from 0001-01-01 to 3200-01-31). Because of this I can't run just the iteration because of the iteration limit for the for loop.

then start at 3000-01-01 or whatever is closest to the ingame start date and add the numbers, or rather start at 0001-01-01 for easier calculating and calculate this internal and just add 3000 or so years every time you put out a date for the player to see.

Re: Issues with creating an in-game local Date/Time system
« Reply #6 on: 14 Jun 2018, 18:25 »
then start at 3000-01-01 or whatever is closest to the ingame start date and add the numbers, or rather start at 0001-01-01 for easier calculating and calculate this internal and just add 3000 or so years every time you put out a date for the player to see.

That was one of my ideas as well but I honestly don't know how to twist the DateToRaw() function in order to have it start from year 3000. And, continuing on to Snarky's suggestion...

What do you need the int representation for, anyway?

The reason I think I need these is that I would like to do date calculations "at leisure".
I would like to add dateB to dateA, as well as be able to know how many days\months\years of distance happen between the two.

For instance let's say that I want to set up a random Mission constructor that sets a due date that is 2 months from whatever the game's current date is, and stores both (e.g. startDate, dueDate).
Another example could be: to finish my current mission I have to travel from Planet X to Planet Y and to do so I need Z amount of days. Will I manage to be there before time is up?

Anyway, I like how your compact your code is :) I am not sure I can use it for all of the above purposes, though!

Another solution vga256 and me tried was to use Julian/Gregorian conversions and that seemed to be quite a good solution but unfortunately AGS doesn't really work well with float :(

Code: Adventure Game Studio
  1. float GtoJ(int year, int month, int day)
  2. {
  3.    float I = IntToFloat(year);
  4.    float J = IntToFloat(month);
  5.    float K = IntToFloat(day);
  6.    
  7.    return K-32075.0+1461.0*(I+4800.0+(J-14.0)/12.0)/4.0+367.0*(J-2.0-(J-14.0)/12.0*12.0)/12.0-3.0*((I+4900.0+(J-14.0)/12.0)/100.0)/4.0;
  8. }
  9.  

3200-01-31 in Julian date should be 2889865.500000, but what I get is this:



"We do not stop playing because we grow old, we grow old because we stop playing."

tzachs

  • AGS Baker
  • Mittens Vassal
  • Parking Goat- games that goats like!
    • I can help with translating
    •  
    • tzachs worked on a game that was nominated for an AGS Award!
Re: Issues with creating an in-game local Date/Time system
« Reply #7 on: 14 Jun 2018, 19:28 »
You can try the answer here if you haven't already (it has 2 way conversions- days_from_civil and civil_from_days): https://stackoverflow.com/questions/7960318/math-to-convert-seconds-since-1970-into-date-and-vice-versa?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

EDIT: Oh, and about this:

Quote
Another solution vga256 and me tried was to use Julian/Gregorian conversions and that seemed to be quite a good solution but unfortunately AGS doesn't really work well with float :(
I seriously doubt AGS doesn't "work well with floats", but rather floats as a data format have limitations which means you can't do accurate calculations with them. It's not AGS specific, all languages suffer from that, see here: http://floating-point-gui.de/
This is why some languages offer another data type which can do more accurate calculations, like decimals in c#, at the expense of performance.
« Last Edit: 14 Jun 2018, 23:28 by tzachs »

Snarky

  • Global Moderator
  • Mittens Earl
  • Private Insultant
    • I can help with proof reading
    •  
    • I can help with translating
    •  
Re: Issues with creating an in-game local Date/Time system
« Reply #8 on: 14 Jun 2018, 21:58 »
As for what's wrong with the code provided, one mistake is at line 98 in Time.asc:

Code: Adventure Game Studio
  1.    d.Year = rawdays / 365;

By the year 3200 there have been 800 - 32 + 8 = 776 leap days, more than two years (so I'm surprised that subtracting 1 is enough to give the right year at all).

I'm also highly dubious of the logic in lines 112 and 115:

Quote
   if (d.IsLeap()) rawdays = (rawdays - howmanyleapyears) % 366;
   else rawdays = (rawdays - howmanyleapyears) % 365;

I think you always have to do mod 365 here, because the question isn't how many days are in this year, but how many days are in the years that have gone by. I think doing mod 365 on the raw day count and then subtracting the number of leap days (or vice versa, as in the code) is convoluted but correct, but the other variation is straight up wrong. You would then have to do another check to make sure you're not on day 366 of a non-leapyear.

Re: Issues with creating an in-game local Date/Time system
« Reply #9 on: 15 Jun 2018, 01:54 »
Working further with the code you provided I may have found what I am looking for (or at least what I am ok with!)

This is how I changed my script:
Code: Adventure Game Studio
  1. // Time.asc
  2.  
  3. int DateToRaw(int y, int m, int d)
  4. {
  5.    return d + m * 100 + y * 10000; // Gives YYYYMMDD
  6. }
  7.  
  8. Date* RawToDate(int rawDate)
  9. {
  10.    Date* d = new Date;
  11.    d.Day = rawDate % 100;
  12.    d.Month = (rawDate / 100) % 100;
  13.    d.Year = rawDate / 10000;
  14.    
  15.    return d;
  16. }
  17.  
  18. int Date::RawDate()
  19. {
  20.    return DateToRaw(this.Year, this.Month, this.Day);
  21. }
  22.  
  23. bool Date::IsLeap()
  24. {
  25.    return this.Year%4 == 0 && (this.Year%100 != 0 || this.Year%400 == 0);
  26. }
  27.  
  28. function Date::Add(int days)
  29. {
  30.    int newRawDate = this.RawDate() + days;
  31.    
  32.    for (int i = this.RawDate(); i <= newRawDate; i++)
  33.    {
  34.       this.Day++;
  35.      
  36.       if (this.Day > 28 && this.Month == 2)
  37.       {
  38.          if (!this.IsLeap())
  39.          {
  40.             this.Day = 1;
  41.             this.Month++;
  42.          }
  43.          else
  44.          {
  45.             if (this.Day > 29)
  46.             {
  47.                this.Day = 1;
  48.                this.Month++;
  49.             }
  50.          }
  51.       }
  52.       else if (this.Day > 30 && (this.Month == 11 || this.Month == 4 || this.Month == 6 || this.Month == 9))
  53.       {
  54.          this.Day = 1;
  55.          this.Month++;
  56.       }
  57.       else if (this.Day > 31 && (this.Month == 1 || this.Month == 3 || this.Month == 5 || this.Month == 7 || this.Month == 8 || this.Month == 10 || this.Month == 12))
  58.       {
  59.          this.Day = 1;
  60.          this.Month++;
  61.          if (this.Month == 13)
  62.          {
  63.             this.Month = 1;
  64.             this.Year++;
  65.          }
  66.       }
  67.    }
  68. }

And just to provide the context:
Code: Adventure Game Studio
  1. // Missions.asc
  2.  
  3. // some other code
  4.  
  5. function CreateMission(int ID, MissionType type)
  6. {
  7.    // some other code
  8.    
  9.    Missions[ID].RawStartDate = CurrentDate.RawDate();
  10.    Date* start = RawToDate(Missions[ID].RawStartDate);
  11.    Date* due = start;
  12.    due.Add(30);
  13.    
  14.    Missions[ID].RawDueDate = DateToRaw(due.Year, due.Month, due.Day);
  15.    
  16.    // some other code
  17. }
  18.  
  19. String MissionText(int ID)
  20. {
  21.    String missionType;
  22.    
  23.    // some other code
  24.    String p = planetarySystem[Missions[ID].TargetPlanet].name;
  25.    
  26.    Date* start = RawToDate(Missions[ID].RawStartDate);
  27.    Date* due = RawToDate(Missions[ID].RawDueDate);
  28.    
  29.    String s;
  30.    s = String.Format("%s is sent over to %s for %s, commissioned by The %s. Start: %.2d-%.2d-%.2d. Due: %.2d-%.2d-%.2d", People[Missions[ID].AssignedTo].name, p, missionType, Missions[ID].IssuingFaction, start.Day, start.Month, start.Year, due.Day, due.Month, due.Year);
  31.    return s;
  32. }

The output being this, where the due date is basically the start date + 30 days:


EDIT: the result is 31 days actually, but hmm, yeah it probably is something I messed up in the for loop in the Date::Add() function.
EDIT 2: Yeah I tweaked the for loop to be
Code: Adventure Game Studio
  1. for (int i = this.RawDate(); i < newRawDate; i++) // < instead of <=
and it works :)

It seems to be working, but I will test it more and see if I can use it as I intended to before confirming!

In the meantime thank you all! :) And feel free to ask for details or provide further feedback, if you want!
« Last Edit: 15 Jun 2018, 02:02 by LostTrainDude »
"We do not stop playing because we grow old, we grow old because we stop playing."