diff options
Diffstat (limited to 'src/c/event.c')
| -rw-r--r-- | src/c/event.c | 717 |
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; +} |
