Skip to content

Time and Calendar

Allofich edited this page May 1, 2020 · 16 revisions

The game tracks time by counting minutes. The starting minutes value is set as ‭346320‬, which will give a date & time of 12:00 noon, Tirdas, 1st of Hearthfire in the year 3E 389.

The date format string is located @44224. It takes the weekday, day, ordinal suffix, month and year. To get the short date string, the part starting with in the is removed.

The values for the date format string are gotten as follows:

string GetDateString(unsigned int gameMinutes)
{
  // Get the year
  int year = gameMinutes / 518,400; // 518,400 minutes are in an in-game year
  year += 389; // Starting year

  // Get how many minutes into the year have passed
  int minutesPassed = gameMinutes % 518,400;

  // Use the number of minutes into the year to get the month
  int i = 0;
  while (minutesPassed >= NumbersOfMinutesPerMonth[i])
  {
    minutesPassed -= NumbersOfMinutesPerMonth[i];
    i++;
  }

  // Get the day. At this point "minutesPassed" refers to how many minutes into the month have passed.
  int day = (minutesPassed / 1440) + 1; // 1440 minutes in a day
  string daySuffix = GetDaySuffix(day);

  while (day >= 7)
  {
    day -= 7; // Day must be 0 through 6
  }
  
  // From here, use the numerical values obtained for month and day to get the corresponding strings from the
  // string arrays, and combine with year, day suffix and the date format string.

  return dateString;
}

NumbersOfMinutesPerMonth is a 12-element DWORD array @442BA.

string GetDaySuffix(int day)
{
  if (day == 11 || day == 12 || day == 13)
  {
    return DaySuffixes[3]; // "th"
  }
  
  day /= 10;
  // Return DaySuffixes entry pointed to by DaySuffixAddresses[day]
}

DaySuffixes is a 4-element string array @443E6. DaySuffixAddresses is a 10-element WORD array @443D2 that points to the entries of DaySuffixes. (To translate its values to the addresses being used here, add 0x3F527 to them.)

The time format is %d:02%d %s, where the hour is 12-based, and the last string is taken from the strings located @44259, 44267, 44276, 4427B, 4428C, 4429B, 442A4. The mapping is as follows:

00      midnight
01..02  night
03..05  early morning
06..11  morning
12      noon
13..17  afternoon
18..20  evening
21..23  night

Moon phases

Those are controlled by phase <- currentDay mod 32. Moon1 uses phase, Moon2 phase + 14. The phase determines the frame in moon0x.dfa which is shown.

Events

  • Each minute:
    • Spell timers are advanced, and if any PC attribute reaches 0, PC dies.
    • If there is no enemies around, and not resting, the levelup is checked.
    • Stamina decreases and is checked
    • Some other things happen
    • Quest and random encounters are fired
  • Each hour:
    • Quests are checked for expiring
    • If resting or in wilderness, a random encounter is fired
  • Each day:
    • Quests are checked for expiring (v2)
    • The weather changes
    • Knights repair their equipment

Knight equipment repair

void KnightItemRepair(void)
{
  int maxRepairAmount = (PlayerLevel + 1) * 10;
  int i = 0;
  INVENTORYITEM* item = &PlayerInventoryItems[i];
  int numberOfUsedInventorySlots = GetNumberOfInventorySlotsInUse();
  while (numberOfUsedInventorySlots != 0)
  {
    byte flags = ((item->Flags) & 0xc) >> 2;
    if ((flags != 2 && flags != 3)) // Not a potion or trinket
    {
      int amountToRepair = item->MaxHealth - item->Health; // Only repair as much as is damaged
      if (amountToRepair != 0 && amountToRepair > maxRepairAmount)
      {
        amountToRepair = maxRepairAmount; // Max repair amount depends on level
      }
      item->Health += amountToRepair;
    }
    i++;
    item = &PlayerInventoryItems[i];
    numberOfUsedInventorySlots--;
  }
  return;
}

Holidays

There are 15 holidays. Their names are stored in szlist @4268D. The info popups for them are resource strings starting with #47, and their descriptions are resource strings starting with #169.

The holiday data itself is stored @4277F. It has the following structure:

BYTE venueType
BYTE services[5]

venueType is the interior index, and the value of services is individual for each venue type. Values 1..4 are used as (cost/4)*n.

0  Is opened on this day
1  Drinks, Heals, Weapons, Spells
2  Rooms, Cures, Armor, Identifies
3  Magical items
4  Blessings, Potions/Spells

Checking whether a service provider is affected by a holiday

bool DoesHolidayServiceAdjustmentApply(void)
{
  int holidayIndex;
  bool isHoliday = IsHoliday(GameMinutes, holidayIndex);
  if (isHoliday && (HolidayData[holidayIndex].venueType == CurrentInteriorType))
  {
    return true;
  }
  return false;
}

Displaying holiday text

// Within an update loop function. "UpdateCount" is a guess but this seems to be a global that is used for timing when
// certain updates happen. In this case it might be waiting until 3 updates have happened so the holiday text doesn't show
// immediately on loading a game.
if (UpdateCount > 3 && IsHoliday(GameMinutes) && (PlayerFlags3 & HaveShownHolidayText) == 0
    && (PlayerEnvFlags & InWildernessOrDungeon) == 0 && (PlayerEnvFlags2 & Outdoors) != 0)
{
  PlayerFlags3 = PlayerFlags3 | HaveShownHolidayText; // This flag will be cleared when the date changes.
  TEMPLATE_DATIndex = holidayIndex + 47;
  ShowTEMPLATE_DATMessage();
}

bool IsHoliday(int gameMinutes, int& holidayIndex)
{  
  int dayOfYear = (gameMinutes % 518400‬) / 1440; // 518400 minutes in a game year, 1440 minutes in a day
  if (dayOfYear < 345) // The last holiday in the game data is on the 344th day.
  {
    holidayIndex = 0;
    while( true )
    {
      bool isHoliday = (HolidayDates[i] == dayOfYear);
      if (isHoliday)
      {
        return true;
      }
      if (!isHoliday && dayOfYear <= HolidayDates[i])
      {
        break;
      }
      holidayIndex++;
    }
  }
  return false;
}

HolidayDates (day number in the year) is the WORD array @427D9.

Clone this wiki locally