/* * 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; itm_hour * 60 + utc->tm_min; event_index = 0; for (int i=0; i= 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; }