aboutsummaryrefslogtreecommitdiff
path: root/src/c/event.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/c/event.c')
-rw-r--r--src/c/event.c717
1 files changed, 717 insertions, 0 deletions
diff --git a/src/c/event.c b/src/c/event.c
new file mode 100644
index 0000000..779be43
--- /dev/null
+++ b/src/c/event.c
@@ -0,0 +1,717 @@
+/*
+ * Copyright (C) 2026 Reiner Herrmann
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "event.h"
+
+#define TIME2MIN(hh,mm) (hh * 60 + mm)
+
+#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
+
+static const struct Gw2Event event_list[] = {
+ /* Core Tyria */
+ {
+ .name = "Admiral Taidha Covington",
+ .utc_offset = TIME2MIN(00,00),
+ .category = CORE,
+ .location = "Bloodtide Coast",
+ .schedule_hours = 3,
+ },
+ {
+ .name = "Svanir Shaman Chief",
+ .utc_offset = TIME2MIN(00,15),
+ .category = CORE,
+ .location = "Wayfarer Foothills",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Megadestroyer",
+ .utc_offset = TIME2MIN(00,30),
+ .category = CORE,
+ .location = "Mount Maelstrom",
+ .schedule_hours = 3,
+ },
+ {
+ .name = "Fire Elemental",
+ .utc_offset = TIME2MIN(00,45),
+ .category = CORE,
+ .location = "Metrica Province",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "The Shatterer",
+ .utc_offset = TIME2MIN(01,00),
+ .category = CORE,
+ .location = "Blazeridge Steppes",
+ .schedule_hours = 3,
+ },
+ {
+ .name = "Great Jungle Worm",
+ .utc_offset = TIME2MIN(01,15),
+ .category = CORE,
+ .location = "Caledon Forest",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Modnir Ulgoth",
+ .utc_offset = TIME2MIN(01,30),
+ .category = CORE,
+ .location = "Harathi Hinterlands",
+ .schedule_hours = 3,
+ },
+ {
+ .name = "Shadow Behemoth",
+ .utc_offset = TIME2MIN(01,45),
+ .category = CORE,
+ .location = "Queensdale",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Golem Mark II",
+ .utc_offset = TIME2MIN(02,00),
+ .category = CORE,
+ .location = "Mount Maelstrom",
+ .schedule_hours = 3,
+ },
+ {
+ .name = "Claw of Jormag",
+ .utc_offset = TIME2MIN(02,30),
+ .category = CORE,
+ .location = "Frostgorge Sound",
+ .schedule_hours = 3,
+ },
+ {
+ .name = "Ley-Line Anomaly",
+ .utc_offset = TIME2MIN(00,20),
+ .category = CORE,
+ .location = "Timberline Falls",
+ .schedule_hours = 6,
+ },
+ {
+ .name = "Ley-Line Anomaly",
+ .utc_offset = TIME2MIN(02,20),
+ .category = CORE,
+ .location = "Iron Marches",
+ .schedule_hours = 6,
+ },
+ {
+ .name = "Ley-Line Anomaly",
+ .utc_offset = TIME2MIN(04,20),
+ .category = CORE,
+ .location = "Gendarran Fields",
+ .schedule_hours = 6,
+ },
+ {
+ .name = "Fractal Incursion",
+ .utc_offset = TIME2MIN(00,00),
+ .category = CORE,
+ .location = "Kessex Hills",
+ .schedule_hours = 4,
+ },
+ {
+ .name = "Fractal Incursion",
+ .utc_offset = TIME2MIN(01,00),
+ .category = CORE,
+ .location = "Diessa Plateau",
+ .schedule_hours = 4,
+ },
+ {
+ .name = "Fractal Incursion",
+ .utc_offset = TIME2MIN(02,00),
+ .category = CORE,
+ .location = "Brisban Wildlands",
+ .schedule_hours = 4,
+ },
+ {
+ .name = "Fractal Incursion",
+ .utc_offset = TIME2MIN(03,00),
+ .category = CORE,
+ .location = "Snowden Drifts",
+ .schedule_hours = 4,
+ },
+
+ /* Living World Season 1 */
+ {
+ .name = "Twisted Marionette",
+ .utc_offset = TIME2MIN(00,00),
+ .category = LWS1,
+ .location = "Eye of the North",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Battle for Lion's Arch",
+ .utc_offset = TIME2MIN(00,30),
+ .category = LWS1,
+ .location = "Eye of the North",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Tower of Nightmares",
+ .utc_offset = TIME2MIN(01,30),
+ .category = LWS1,
+ .location = "Eye of the North",
+ .schedule_hours = 2,
+ },
+
+ /* Living World Season 2 */
+ {
+ .name = "Sandstorm",
+ .utc_offset = TIME2MIN(00,40),
+ .category = LWS2,
+ .location = "Dry Top",
+ .schedule_hours = 1,
+ },
+
+ /* Living World Season 3 */
+ {
+ .name = "New Loamhurst",
+ .utc_offset = TIME2MIN(01,45),
+ .category = LWS3,
+ .location = "Lake Doric",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Noran's Homestead",
+ .utc_offset = TIME2MIN(00,30),
+ .category = LWS3,
+ .location = "Lake Doric",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Saidra's Haven",
+ .utc_offset = TIME2MIN(01,00),
+ .category = LWS3,
+ .location = "Lake Doric",
+ .schedule_hours = 2,
+ },
+
+ /* Living World Season 4 */
+ {
+ .name = "Palawadan",
+ .utc_offset = TIME2MIN(01,45),
+ .category = LWS4,
+ .location = "Domain of Istan",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Death-Branded Shatterer",
+ .utc_offset = TIME2MIN(01,15),
+ .category = LWS4,
+ .location = "Jahai Bluffs",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "The Oil Floes",
+ .utc_offset = TIME2MIN(00,45),
+ .category = LWS4,
+ .location = "Thunderhead Peaks",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Thunderhead Keep",
+ .utc_offset = TIME2MIN(01,45),
+ .category = LWS4,
+ .location = "Thunderhead Peaks",
+ .schedule_hours = 2,
+ },
+
+ /* Ice Brood Saga */
+ {
+ .name = "Effigy",
+ .utc_offset = TIME2MIN(00,10),
+ .category = IBS,
+ .location = "Grothmar Valley",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Doomlore Shrine",
+ .utc_offset = TIME2MIN(00,38),
+ .category = IBS,
+ .location = "Grothmar Valley",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Ooze Pits",
+ .utc_offset = TIME2MIN(01,05),
+ .category = IBS,
+ .location = "Grothmar Valley",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Metal Concert",
+ .utc_offset = TIME2MIN(01,40),
+ .category = IBS,
+ .location = "Grothmar Valley",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Storms of Winter",
+ .utc_offset = TIME2MIN(01,45),
+ .category = IBS,
+ .location = "Bjora Marches",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Drakkar",
+ .utc_offset = TIME2MIN(01,05),
+ .category = IBS,
+ .location = "Bjora Marches",
+ .schedule_hours = 2,
+ },
+
+ /* Heart of Thorns */
+ {
+ .name = "Chak Gerent",
+ .utc_offset = TIME2MIN(00,30),
+ .category = HOT,
+ .location = "Tangled Depths",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Octovine",
+ .utc_offset = TIME2MIN(01,00),
+ .category = HOT,
+ .location = "Auric Basin",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Dragon's Stand",
+ .utc_offset = TIME2MIN(01,30),
+ .category = HOT,
+ .location = "Dragon's Stand",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Night and the Enemy",
+ .utc_offset = TIME2MIN(01,45),
+ .category = HOT,
+ .location = "Verdant Brink",
+ .schedule_hours = 2,
+ },
+
+ /* Path of Fire */
+ {
+ .name = "Casino Blitz",
+ .utc_offset = TIME2MIN(00,05),
+ .category = POF,
+ .location = "Crystal Oasis",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Buried Treasure",
+ .utc_offset = TIME2MIN(01,00),
+ .category = POF,
+ .location = "Desert Highlands",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "The Path to Ascension",
+ .utc_offset = TIME2MIN(01,30),
+ .category = POF,
+ .location = "Elon Riverlands",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Maws of Torment",
+ .utc_offset = TIME2MIN(01,00),
+ .category = POF,
+ .location = "The Desolation",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Junundu Rising",
+ .utc_offset = TIME2MIN(00,30),
+ .category = POF,
+ .location = "The Desolation",
+ .schedule_hours = 1,
+ },
+ {
+ .name = "Forged with Fire",
+ .utc_offset = TIME2MIN(00,00),
+ .category = POF,
+ .location = "Domain of Vabbi",
+ .schedule_hours = 1,
+ },
+ {
+ .name = "Serpents' Ire",
+ .utc_offset = TIME2MIN(00,30),
+ .category = POF,
+ .location = "Domain of Vabbi",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Dragonstorm",
+ .utc_offset = TIME2MIN(01,00),
+ .category = POF,
+ .location = "Eye of the North",
+ .schedule_hours = 2,
+ },
+
+ /* End of Dragons */
+ {
+ .name = "Aetherblade Assault",
+ .utc_offset = TIME2MIN(01,30),
+ .category = EOD,
+ .location = "Seitung Province",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Kaineng Blackout",
+ .utc_offset = TIME2MIN(00,00),
+ .category = EOD,
+ .location = "New Kaineng City",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Gang War",
+ .utc_offset = TIME2MIN(00,30),
+ .category = EOD,
+ .location = "The Echovald Wilds",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Aspenwood",
+ .utc_offset = TIME2MIN(01,40),
+ .category = EOD,
+ .location = "The Echovald Wilds",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Battle for the Jade Sea",
+ .utc_offset = TIME2MIN(01,00),
+ .category = EOD,
+ .location = "Dragon's End",
+ .schedule_hours = 2,
+ },
+
+ /* Secrets of the Obscure */
+ {
+ .name = "Unlocking the Wizard's Tower",
+ .utc_offset = TIME2MIN(01,00),
+ .category = SOTO,
+ .location = "Skywatch Archipelago",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Defense of Amnytas",
+ .utc_offset = TIME2MIN(00,00),
+ .category = SOTO,
+ .location = "Amnytas",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Outer Nayos",
+ .utc_offset = TIME2MIN(01,30),
+ .category = SOTO,
+ .location = "The Wizard's Tower",
+ .schedule_hours = 3,
+ },
+
+ /* Janthir Wilds */
+ {
+ .name = "Of Mists and Monsters",
+ .utc_offset = TIME2MIN(00,30),
+ .category = JW,
+ .location = "Janthir Syntri",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "A Titanic Voyage",
+ .utc_offset = TIME2MIN(01,20),
+ .category = JW,
+ .location = "Bava Nisos",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Mount Balrior",
+ .utc_offset = TIME2MIN(00,00),
+ .category = JW,
+ .location = "Lowland Shore",
+ .schedule_hours = 3,
+ },
+
+ /* Visions of Eternity */
+ {
+ .name = "Hammerhart Rumble!",
+ .utc_offset = TIME2MIN(00,40),
+ .category = VOE,
+ .location = "Shipwreck Strand",
+ .schedule_hours = 2,
+ },
+ {
+ .name = "Secrets of the Weald",
+ .utc_offset = TIME2MIN(01,40),
+ .category = VOE,
+ .location = "Starlit Weald",
+ .schedule_hours = 2,
+ },
+};
+
+/* these events have no regular schedule */
+static const struct Gw2Event event_list_irregular[] = {
+ {
+ .name = "Tequatl the Sunless",
+ .utc_offset = TIME2MIN(00,00),
+ .category = CORE,
+ .location = "Sparkfly Fen",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Tequatl the Sunless",
+ .utc_offset = TIME2MIN(03,00),
+ .category = CORE,
+ .location = "Sparkfly Fen",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Tequatl the Sunless",
+ .utc_offset = TIME2MIN(07,00),
+ .category = CORE,
+ .location = "Sparkfly Fen",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Tequatl the Sunless",
+ .utc_offset = TIME2MIN(11,30),
+ .category = CORE,
+ .location = "Sparkfly Fen",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Tequatl the Sunless",
+ .utc_offset = TIME2MIN(16,00),
+ .category = CORE,
+ .location = "Sparkfly Fen",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Tequatl the Sunless",
+ .utc_offset = TIME2MIN(19,00),
+ .category = CORE,
+ .location = "Sparkfly Fen",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Karka Queen",
+ .utc_offset = TIME2MIN(02,00),
+ .category = CORE,
+ .location = "Southsun Cove",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Karka Queen",
+ .utc_offset = TIME2MIN(06,00),
+ .category = CORE,
+ .location = "Southsun Cove",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Karka Queen",
+ .utc_offset = TIME2MIN(10,30),
+ .category = CORE,
+ .location = "Southsun Cove",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Karka Queen",
+ .utc_offset = TIME2MIN(15,00),
+ .category = CORE,
+ .location = "Southsun Cove",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Karka Queen",
+ .utc_offset = TIME2MIN(19,00),
+ .category = CORE,
+ .location = "Southsun Cove",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Karka Queen",
+ .utc_offset = TIME2MIN(23,00),
+ .category = CORE,
+ .location = "Southsun Cove",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Triple Trouble",
+ .utc_offset = TIME2MIN(01,00),
+ .category = CORE,
+ .location = "Bloodtide Coast",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Triple Trouble",
+ .utc_offset = TIME2MIN(04,00),
+ .category = CORE,
+ .location = "Bloodtide Coast",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Triple Trouble",
+ .utc_offset = TIME2MIN(8,00),
+ .category = CORE,
+ .location = "Bloodtide Coast",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Triple Trouble",
+ .utc_offset = TIME2MIN(12,30),
+ .category = CORE,
+ .location = "Bloodtide Coast",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Triple Trouble",
+ .utc_offset = TIME2MIN(17,00),
+ .category = CORE,
+ .location = "Bloodtide Coast",
+ .schedule_hours = 24,
+ },
+ {
+ .name = "Triple Trouble",
+ .utc_offset = TIME2MIN(20,00),
+ .category = CORE,
+ .location = "Bloodtide Coast",
+ .schedule_hours = 24,
+ },
+};
+
+static struct Gw2Event *all_events = NULL;
+static int all_events_count = 0;
+static int event_index = 0;
+
+static inline uint16_t enabled_categories() {
+ /* TODO: make this mask configurable */
+ return 0xffff;
+}
+
+static inline bool is_category_enabled(enum Category category) {
+ return enabled_categories() & (1 << category);
+}
+
+static int event_time_comp(const void *p1, const void *p2) {
+ const struct Gw2Event *event1 = p1;
+ const struct Gw2Event *event2 = p2;
+
+ if (event1->utc_offset == event2->utc_offset) {
+ /* if they start at the same time, sort by category */
+ return event1->category - event2->category;
+ }
+
+ return (event1->utc_offset < event2->utc_offset) ? -1 : 1;
+}
+
+void cleanup_events() {
+ if (all_events) {
+ free(all_events);
+ all_events = NULL;
+ }
+ all_events_count = 0;
+ event_index = 0;
+}
+
+void init_events() {
+ cleanup_events();
+
+ /* count all events in a day */
+ all_events_count = 0;
+ for (size_t i=0; i<ARRAY_SIZE(event_list); i++) {
+ if (!is_category_enabled(event_list[i].category)) {
+ continue;
+ }
+ all_events_count += 24 / event_list[i].schedule_hours;
+ }
+ for (size_t i=0; i<ARRAY_SIZE(event_list_irregular); i++) {
+ if (!is_category_enabled(event_list_irregular[i].category)) {
+ continue;
+ }
+ all_events_count++;
+ }
+
+ all_events = calloc(all_events_count, sizeof(struct Gw2Event));
+
+ /* create all individual events */
+ int pos = 0;
+ for (size_t i=0; i<ARRAY_SIZE(event_list); i++) {
+ if (!is_category_enabled(event_list[i].category)) {
+ continue;
+ }
+ const int daily_count = 24 / event_list[i].schedule_hours;
+ for (int j=0; j<daily_count; j++) {
+ memcpy(&all_events[pos], &event_list[i], sizeof(struct Gw2Event));
+ all_events[pos].utc_offset += all_events[pos].schedule_hours * 60 * j;
+ pos++;
+ }
+ }
+ for (size_t i=0; i<ARRAY_SIZE(event_list_irregular); i++) {
+ if (!is_category_enabled(event_list_irregular[i].category)) {
+ continue;
+ }
+ memcpy(&all_events[pos], &event_list_irregular[i], sizeof(struct Gw2Event));
+ pos++;
+ }
+
+ /* sort list by time */
+ qsort(all_events, all_events_count, sizeof(struct Gw2Event), event_time_comp);
+
+ update_event_index();
+}
+
+const struct Gw2Event *get_next_event(int offset) {
+ offset = (offset + event_index) % all_events_count;
+ return &all_events[offset];
+}
+
+/* moves the index to the first event that is not yet running */
+void update_event_index() {
+ const time_t now = time(NULL);
+ const struct tm *utc = gmtime(&now);
+ const int utc_offset = utc->tm_hour * 60 + utc->tm_min;
+
+ event_index = 0;
+ for (int i=0; i<all_events_count; i++) {
+ if (all_events[i].utc_offset >= utc_offset) {
+ event_index = i;
+ break;
+ }
+ }
+}
+
+int minutes_to_event(const struct Gw2Event *event) {
+ const time_t now = time(NULL);
+ const struct tm *utc = gmtime(&now);
+ const int utc_offset = utc->tm_hour * 60 + utc->tm_min;
+
+ return event->utc_offset - utc_offset;
+}
+
+GColor8 color_for_category(enum Category category) {
+ switch(category) {
+ case CORE:
+ case LWS1:
+ return GColorDarkCandyAppleRed;
+ case LWS2:
+ return GColorLimerick;
+ case LWS3:
+ return GColorMayGreen;
+ case LWS4:
+ return GColorLavenderIndigo;
+ case IBS:
+ return GColorVividCerulean;
+ case HOT:
+ return GColorSpringBud;
+ case POF:
+ return GColorPurpureus;
+ case EOD:
+ return GColorTiffanyBlue;
+ case SOTO:
+ return GColorChromeYellow;
+ case JW:
+ return GColorBabyBlueEyes;
+ case VOE:
+ return GColorRajah;
+ }
+ return GColorFashionMagenta;
+}