promotional bannermobile promotional banner

Ultimate Calendar

Forking the Classic Calendar addon for WoW Classic and making it available in more game versions
Back to Files

UltimateCalendar-v1.9.1

File nameClassicCalendar-UltimateCalendar-v1.9.1.zip
Uploader
PmptastyPmptasty
Uploaded
Oct 21, 2026
Downloads
658
Size
492.5 KB
Flavors
Classic TBCClassicForever
File ID
9280878
Type
R
Release
Supported game versions
  • 2.5.6
  • 1.60.1
  • 1.15.9

What's new

Ultimate Calendar

Formerly Classic Calendar - Revived. Entries before v1.9.1 use the old name.

[v1.9.1] (2026-10-21)

Renamed to Ultimate Calendar

Files: ClassicCalendar.toc, ClassicCalendar_TBC.toc, ClassicCalendar_Camelot.toc, ClassicCalendar.lua, ClassicCalendar_Options.lua, GameTime_Wrath.lua, Patches.lua, HolidayData.lua, Forever.lua, WorldBuff.lua, WorldBuffDB.lua, Tests/env_cc.lua, README.md, API.md, docs/Curseforge_Description.{md,html}, CLAUDE.md.

Operator, 2026-10-21: "i want to rebrand the addon as Ultimate Calendar". The CurseForge project is now "Ultimate Calendar", slug ultimatecalendar; the project ID (1339939) is unchanged, so X-Curse-Project-ID stays.

  • Display name only. Every TOC ## Title is now |cffFF8000Ultimate Calendar|r (Forever keeps its grey "Forever" suffix). The minimap tooltip, LDB tooltip, the Settings category and its OpenToCategory fallbacks say "Ultimate Calendar", and chat lines that started ClassicCalendar: now start Ultimate Calendar:.
  • Kept on purpose: the ClassicCalendar folder and TOC file names, every SavedVariable, global, frame name and ClassicCalendar.API, the LibDBIcon key (it holds the minimap button position) and the AceComm namespace. Renaming any of those would lose saved data, break other addons' OptionalDeps, or stop old and new versions talking.
  • Release tags are now UltimateCalendar-vX.Y.Z, starting with this one (operator: "change the tag to UltimateCalendar-v1.9.1"). The packager reads only the version from the tag, so nothing in the addon depends on the prefix.
  • VersionCheck name stays "Classic Calendar - Revived". VersionCheck-1.0 keys every peer's version by that string on the wire (VC.hosts[name], the REQ batch), so a new name would mean older installs never hear that a newer version exists. KNOWN COST: the version window's tab still shows the old name.

[v1.9.0] (2026-10-20)

Classic Era: the Edge of Madness boss is on the calendar

Files: HolidayData.lua, Locales/localization-world.lua, Tests/edgeofmadness_spec.lua.

crimsonmane on Discord, 2026-10-20 (inbox edea3a57): "is it possible for the calendar addon to see which Madness boss is active?" My first answer said no, after one search that missed the trackers; the operator: "there are sites dedicated to the schedule we can pull in". The answer on Discord was edited.

  • EdgeOfMadnessScheduleFor(year) lists every two-week boss window that starts in that year: Gri'lek, Hazza'rah, Renataki, Wushoolay, 14 days each, counted from Gri'lek on Tuesday 22 September 2026. edgeofmadnesscalendar.com and zockify's Classic calendar both put Renataki on from 20 October to 3 November 2026; sources and doubts are in docs/Event_Date_Sources.md. The client has no API for it.
  • Days are counted from noon with addDaysToDate, not by the holiday frequency recurrence: that steps whole seconds, so after a daylight-saving change a midnight start lands on the day before.
  • Shown Tuesday 00:00 to Monday 23:59 in realm time (keepTimes), under the Weekly Holidays filter, with no banner art. The change happens at midnight Pacific; the whole-day display does not show the hour.
  • Classic Era and Hardcore only. Season of Discovery, Burning Crusade and WoW Forever get none, since their rotation is not sourced.
  • Names and descriptions are new enUS entries in L.HolidayLocalization (EdgeOfMadnessGrilek and so on); other locales fall back to them through HolidayText.
  • Specs: Tests/edgeofmadness_spec.lua pins both trackers' autumn 2026 dates, checks six years of windows run back to back Tuesday to Monday in order across daylight-saving changes, draws Renataki on a grid day and opens its description, and checks SoD, TBC and Forever show none.
  • Seen in game (operator, 2026-10-20, Classic Era): "edge of madness workign in classic".
  • Problems panel: three duplicate-set-field warnings in Tests/patches_spec.lua, where each example installs its own C_Calendar.GetEventIndex, are disabled on those lines only. Suite 999 passed / 0 failed. Dev-only.

Release docs and lint config

Files: docs/Curseforge_Description.*, .markdownlint.json, .pkgmeta, every Markdown file in the repo.

Peer Review on the v1.8.1 release-prep self-audit (thread a626e480). This work landed after v1.8.1 was tagged, so it ships here. No Lua changed.

  • The v1.7.2 Recent Updates block listed Fixed before Changed; it is now New, Changed, Fixed like the others, in both CurseForge files.
  • The v1.8.0 popup bullet said the new look "arrives with the next LibAceGUIWidgets update". That update is out (v0.4.0, MINOR 42, carries DIALOG_STYLES.window; the library is at v0.6.0), so the line now says an up-to-date library gives the new look.
  • .markdownlint.json disabled ten rules and both changelogs disabled MD024 inline. All are back on, MD013 (line length, 80) included: a disabled rule is a finding, not a setting to respect (Peer Review directive 6c26b41b). MD024 is now siblings_only in the config instead of off. README.md and docs/Curseforge_Description.md were fixed to pass: the title is the first line, list markers take one space, the New/Changed/Fixed labels and patch lines are real headings, and stray double blank lines and a trailing space are gone. docs/BUG_TRACKER.md and the other docs/ files got the same kind of fixes: bold pseudo-headings became real headings, italic placeholder lines became plain sentences, and bare code fences name a language.
  • Every Markdown file is wrapped to 80 columns. Only line breaks changed, apart from these edits MD013 cannot be met without: section headings longer than 80 moved their file list onto a Files: line below, long code lines in examples were split across lines, and over-long tables were turned into lists (one bullet per row, the first column in bold). One table was kept instead: the coverage table in Tests/README.md has shortened Notes cells, with the full notes in a paragraph below it. In docs/BUG_TRACKER.md the moved heading tails were made into sentences: a capital letter, a closing period, and "Also:" where the heading had used "+".
  • CHANGELOG_ARCHIVE.md was shipping in the zip. It is now in .pkgmeta's ignore list.
  • Test harness pinned at 21d2f0e. Its .pkgmeta checker now matches the CalendarHelper dependency to the slug calendar-helper, so the desk's lint_all is clean on every checker instead of showing three known-false failures (WoWAPITesting thread 35f402e7). Suite 994 passed / 0 failed on the new pin. Dev-only; nothing ships.
  • README.md (which ships) pointed players at the upstream fork. Its CI badge and "report any issues" link went to github.com/birkholz, and its Discord badge to an old invite. The CI badge is gone, and both now point at the Discord the CurseForge page uses (Peer Review thread ae59aee6).
  • CurseForge page: "Revinging" is now "Reviving", and the .md gains the html's "only the last five patches" line, both now saying "the addon's full changelog" rather than "on GitHub".
  • CHANGELOG_ARCHIVE.md carried the live file's "Older releases" pointer, moved along with the sections it described; removed.

[v1.8.1] (2026-10-20)

WoW Forever: dungeons and raids can be picked in Create Event again

Files: Forever.lua.

N3zr0k on Discord, 2026-10-18 (inbox 6927d964): "Dungeons and Raids can't be selected in the Events".

  • Forever's client has no global GetDungeonNameWithDifficulty. Era and TBC define it in Blizzard_UIParent/Shared/UIParent.lua; Forever keeps only private local copies in its own Blizzard_Calendar/Mainline/Blizzard_Calendar.lua:62 and Blizzard_StaticPopup_Game/GameDialogDefs.lua:5. The ported calendar calls it for every dungeon or raid name it draws -- each texture-picker row (CalendarTexturePicker_InitButton), the chosen dungeon in Create and View Event, raid lockout and reset titles in the grid, day tooltips, the event picker and the Today view -- so on Forever the picker raised "attempt to call a nil value" on its first row (the customer's error file, below). Forever.lua now defines Era's function unchanged, only when the client has none. Era and TBC never load that file.
  • .luarc.json declares NORMAL_FONT_COLOR_CODE, FONT_COLOR_CODE_CLOSE and DUNGEON_NAME_WITH_DIFFICULTY for the language server.
  • Specs: Tests/forever_spec.lua checks the function is supplied without the client global, formats with and without a difficulty, and leaves a client's own copy alone; it also draws real Create Event picker rows (CalendarTexturePicker_InitButton on a CalendarTexturePickerButtonTemplate button) for dungeons and raids, using Era's positional DUNGEON_NAME_WITH_DIFFICULTY string "%1$s (%2$s)". Those globals are reset by the harness from pin 1d8ae40 (next bullet), which replaced this spec's own save/restore (Peer Review B-N1 on 420602ab) and its GetDifficultyInfo stub.
  • Test harness pinned at 1d8ae40, which installs GetDifficultyInfo, GetDungeonNameWithDifficulty (non-Mainline, non-Forever) and DUNGEON_NAME_WITH_DIFFICULTY on every reset (WoWAPITesting inbox 60f1ea2b). Tests/forever_spec.lua dropped its own save/restore of the two globals, its DUNGEON_NAME_WITH_DIFFICULTY stand-in and its GetDifficultyInfo stub; it now seeds wow.difficulties[0], the id every picker texture carries (Patches.lua), and still clears the harness's Era GetDungeonNameWithDifficulty so the examples test Forever.lua's copy. Suite 994 passed / 0 failed on the new pin. Dev-only; nothing ships.
  • Tests/todayview_spec.lua depended on spec-file order. Five of its examples draw a holiday with art, which calls _CalendarFrame_GetTextureCoords from ClassicCalendar.lua, a file the spec never loaded; they passed only because an earlier file had left the global behind, and --order reverse failed all five. The spec now loads ClassicCalendar.lua after CalendarTodayView.lua, as every TOC does. 994 passed / 0 failed forward, reversed and shuffled (seed 1792423524). Dev-only.
  • The customer's report was this error. Their attached LUA_Error.txt (inbox 0102257e), read on 2026-10-20, is GetDungeonNameWithDifficulty being nil at ClassicCalendar.lua:5810, inside CalendarTexturePicker_InitButton.
  • Seen in game (operator, 2026-10-20, WoW Forever): Create Event with type Dungeon and type Raid both open the picker, a raid can be chosen, and BugSack shows no errors. Raids show without "(Normal)", which is right: Patches.lua gives every raid texture difficultyId=0, and the name comes from GetDifficultyInfo(0) (ClassicCalendar.lua:1024), which on Forever gave none, as seen in game.

One shipped API reference, with its limits in bytes

Files: API.md, README.md, GameEvents.lua.

Peer Review on self-audit 32e60fef, after v1.8.0 was tagged.

  • docs/Addon_API.md is now API.md at the addon root. .pkgmeta ignores docs, so the reference never reached the zip; v1.8.0 worked around that with a second, condensed copy in README.md, and nothing asserted the two agreed. The README section is gone, replaced by one line pointing addon authors at API.md in the addon folder, which also keeps the README non-technical as its own rule asks. The v1.8.0 entry below still names the old path; that is what shipped.
  • The text limits are bytes, not letters. AE.Clean cuts at n bytes and backs off to a UTF-8 boundary (AddonEvents.lua), so the 64 and 1000 for added events and the 31 and 255 for personal and guild events are byte counts: a German, Russian or Korean title keeps fewer letters than the event window would take by hand. Safe, never longer. API.md and the header comment in GameEvents.lua now say bytes. No code change.
  • Test harness pinned at bd5ba2a, and runTimers re-checks each timer before calling it (Tests/env_cc.lua). Since harness 79e230c a cancelled or spent C_Timer entry has fn = nil, so a callback that cancelled a later timer in the same batch would have raised "attempt to call a nil value" (WoWAPITesting inbox bb4a6020). Tests/syncstartup_spec.lua carried its own copy of runTimers and now uses the shared one, so the next timer change is made once (Peer Review on self-audit 3f5eaedc). Suite 990 passed / 0 failed on the new pin. Dev-only; nothing ships.
  • Correction to v1.8.0: the windowed popups need LibAceGUIWidgets v0.4.0 (MINOR 42), not "v0.3.2" as the v1.8.0 entry says. The library's earlier replies named v0.3.2 in error (Peer Review a87bb068). The code checks MINOR 42 / DIALOG_STYLES, not the version string, so only the text was wrong.

[v1.8.0] (2026-10-13)

Other addons can add events to the calendar and read every event on a day

Files: AddonEvents.lua, Patches.lua.

Asked for on Discord by the author of the "Can't Play Forever" clock addon (inbox b23c8ec5 / 8627351b): alarms shown in the calendar, and "is there an event today" shown on the clock. Operator, 2026-10-13: "build an API that allows people to add events of the proper type with the proper tie in to their character, as well as read events".

  • ClassicCalendar.API is version 2. New: AddEvent(source, info), RemoveEvent(source, key), ClearEvents(source), GetEvents(source), GetDayEvents(year, month, day), GetTodayEvents(), the EVENT_TYPES table and an EventsChanged(source) callback. The version-1 raid read API (UpcomingRaids, Signups, LootLink, RaidsChanged, SignupsChanged) is unchanged.
  • Events have a real type and belong to the character. eventType is RAID, DUNGEON, PVP, MEETING or OTHER (the client's Enum.CalendarEventType). Events are saved in a new per-character saved variable, ClassicCalendarCharDB, so they show on that character's calendar only and are never synced to the guild. Times are calendar (realm) time, the same clock the calendar shows. Adding the same key again replaces the event; each addon may keep up to 1,000.
  • On the calendar they use the calendar's existing SYSTEM kind, which the ported code already formats, colours and draws, and which it does not treat as player-made, so the day menu never offers to copy or delete one through the server. Clicking one opens the information panel the holidays use, showing the event's text and "Added by" the addon's name. An event is listed on the day it starts.
  • Wiring in Patches.lua: the uncached day count and getter append ClassicCalendar_AddonEventsOn(year, month, day), and the month cache's signature carries ClassicCalendar_AddonEventsVersion, so an added event shows on the next redraw without /calrefresh. stubbedOpenEvent and newGetHolidayInfo gained the added-event branch. New locale string ADDON_EVENT_ADDED_BY (enUS; other locales fall back to it).
  • Reading a day goes through the same stubbedGetNumDayEvents / stubbedGetDayEvent the grid uses, so holidays, resets, guild events and added events all come back, filters applied, without moving the client's calendar month. The client's own events exist only for months the server has sent.
  • TOCs: AddonEvents.lua after RaidSignupsSync.lua and ClassicCalendarCharDB in SavedVariablesPerCharacter, in all three.
  • Bulk adds: AddEvents(source, list) adds a whole list with one redraw and one EventsChanged at the end, and returns keys, reasons per entry. Each entry runs under pcall, so one that raises is reported as refused instead of leaving the batch open. Without it, AddEvent redraws the open calendar on every call, so an addon adding hundreds of events in a loop froze the client (self-audit ba3d385a, confirmed by Peer Review). Not a timer: directive b15a1e31 keeps timers off anything that draws, and the writ check refused a C_Timer.After(0) redraw.
  • Saved events show at login. An index or month built before the per-character saved variable loaded kept the empty store all session; ADDON_LOADED for ClassicCalendar now drops the index and bumps the version.
  • Text from another addon: title and description are cut on a UTF-8 character boundary (a byte cut left broken Cyrillic or Korean) and trimmed again after the cut, and a text that comes out empty is refused (title) or dropped (description). | escapes are removed whole -- colours and textures go, a link keeps its display text, a |n line break becomes a space, any stray | goes -- so another addon cannot draw textures, links or colours in the grid (Peer Review a9fbedb3 F1-F3). source is trimmed, so "Clock" and "Clock " are one owner (Peer Review, LOW).
  • Specs: new Tests/addonevents_spec.lua (37 examples), 100% line coverage of AddonEvents.lua. Tests/env_cc.lua resets the new globals. Tests/clientmodel_spec.lua pins API.EVENT_TYPES and Patches.lua's CalendarEventType to the client's Enum.CalendarEventType.
  • README carries the API reference. docs/ is in .pkgmeta's ignore list, so docs/Addon_API.md never reaches the zip; README.md (which ships) now has a "For addon authors" section covering versions 2 and 3. Keep the two in step when the API changes.
  • Added events do not raise the calendar's event alarms, which read the client's own events only. Documented in docs/Addon_API.md.
  • Seen in game (operator, 2026-10-13, Classic Era): AddEvent, GetTodayEvents and ClearEvents worked from /run. Not checked: how the information panel lays out an added event's text, and TBC or WoW Forever.

Other addons can ask for personal and guild events, and edit or delete them

Files: GameEvents.lua, RaidTemplates.lua, AddonEvents.lua, ClassicCalendar.lua.

Operator, 2026-10-13: "we need the API to allow the both the guild event and the personal event type read/write, for the alarm one it probably doesn't need it, but other addons may".

  • ClassicCalendar.API is version 3. New: CreateGameEvent(source, info) (kind PLAYER or GUILD_EVENT), EditGameEvent(year, month, day, index, changes, eventID) and RemoveGameEvent(year, month, day, index, eventID).
  • The player's press saves it. C_Calendar.AddEvent and C_Calendar.UpdateEvent carry HasRestrictions in the Classic Era client's CalendarDocumentation.lua, and the documentation does not say what the restriction is. So no call saves anything: create opens the calendar at that month and runs the day menu's own CalendarDayContextMenu_CreateEvent / _CreateGuildEvent on the real day cell, then fills the window; edit opens the event with C_Calendar.OpenEvent and fills the edit window once, after the calendar's own CALENDAR_OPEN_EVENT handler has shown and loaded it (a post-hook on CalendarFrame_OnEvent), and only when the opened event is the one asked for; remove selects the event and asks the calendar's delete question. Create, Update or OK is the player's.
  • An index is checked against its eventID (Peer Review eec90a18, F1). An index is a place in a list the server can change between the caller's read and its request. When the event at that index has an eventID, edit and remove now require the caller to pass the same one, and refuse otherwise ("a different event is at that index now: read the day again"); an event without one is still found by its place. Before this, a stale index could put the player's "delete this event?" question on a different event.
  • The API's delete question names the event: its title and short date above the usual wording, through the new ClassicCalendar_ConfirmDeleteSelectedEvent(heading). The day menu's question is unchanged; it shares the wording with CalendarDayContextMenu_DeleteEvent through one local.
  • A redraw no longer drops a requested edit (Peer Review eec90a18, F2). The first version consumed the request on the edit window's first redraw, and a roster or event update redraws an open window before the server answers, so the API returned true for an edit that never appeared. The request now waits for the server's open answer, for up to GE.PENDING_SECONDS (30): a request the server never answered is dropped when it is that old, so it cannot fill the event when the player opens it by hand later. Checked when the answer arrives, not on a timer.
  • One edit at a time (Peer Review 36642e8e, C2). A second EditGameEvent before the first one's open answer replaced it, and the first event's answer then ended the second unfilled while both calls had returned true. A new edit is now refused with "another edit is waiting for the server: try again shortly" while one younger than GE.PENDING_SECONDS waits. docs/Addon_API.md states this and the 30-second drop.
  • CalendarHelper's raid-event path opens the right month (found in self-audit, plus Peer Review 728b6b30 N1). Its stand-in day cell kept the offset from before it moved the calendar, so the create window named the wrong month and weekday whenever the calendar had been showing another month; it is now 0, the shown month, as in the World Buff path. And its move to the target month started from the current month but stepped the year from the displayed one, so it missed by twelve months whenever the calendar showed another year.
  • One month step, not three (Peer Review 36642e8e, C3). CalendarHelper's reset-then-step block and the World Buff announcement's own step are gone; both call GE.ShowMonth(year, month), which measures from the month the calendar shows, so the copies cannot drift apart again. Changed: when the calendar cannot be opened or reports no month, both now print "Cannot open calendar window" and stop; before, CalendarHelper went on over a hidden calendar and the World Buff path fell back to the game date.
  • One month-offset sum. "How many months from the shown one" was written four times (GE.ShowMonth, the API's event lookup, API.GetDayEvents, Today View's click-time offset). It is now GE.MonthOffset(year, month), nil when the calendar reports no month; the API's lookup also stops raising on that nil and refuses with "no such day on the calendar" (one example in Tests/raidtemplates_spec.lua). Today View reads GameEvents unguarded in both its offset and its day-cell ID, one policy instead of two. Tests/todayview_spec.lua loads GameEvents.lua, as every TOC does.
  • Today View's day menus use the month as it is at the click (Peer Review 36642e8e, C1). The right-click and open handlers read the month offset the last redraw stored, and the player can page the calendar without the Today View redrawing, so the day menu could act on another month. They now compute it from the view's date when clicked.
  • Stand-in day cells name the right weekday. The calendar names the weekday from a day cell's ID. The World Buff announcement and the three Today View day-menu and open paths built stand-in cells whose ID was always 1, so the create window's date line named the first column's weekday whatever the date. CalendarHelper's stand-in ignored the week's starting day, so with the week starting on Monday it named the day after. All now take the position from ClassicCalendarGameEvents.CellIndex(day, monthOffset), which GE.DayButton also uses.
  • Editor warnings: a zero-argument GetGuildSetting stub in Tests/raidtemplates_spec.lua made the language server type every real call as taking none (eight warnings across ClassicCalendar.lua, WorldBuff.lua and ClassicCalendar_Options.lua); the stub now has the real signature. Two nil-assignment warnings in CalendarHelper:IsPacificDST are casts, since every March has a second Sunday and every November a first.
  • Refusals match the calendar's own rules: today or later and no later than C_Calendar.GetMaxCreateDate; a guild event needs a guild and passes the officers-only setting; edit and remove need ContextMenuEventCanEdit / CanRemove; guild announcements are not created, as the day menu does not offer them. Title cut to 31 letters and description to 255, the window's own limits, with the same text cleaning as added events.
  • Reading: GetDayEvents gives the game's own events index, eventID, invitedBy, inviteStatus, modStatus, isLocked, canEdit and canRemove.
  • One permission check: the day menu's guild-event gate is now ClassicCalendar_CanCreateGuildEvent(), used by the menu, the API and WorldBuffs:CreateGuildEvent (Peer Review P1 on self-audit ff59da84). The World Buff announce kept an inline copy of the officers-only rule with no IsInGuild test, so a guildless player reached the date picker for a guild event; it now says "You must be in a guild to create a guild event." and stops. Its officer test was always the same WorldBuffDB:IsOfficer(), so only the guild test is new. Three examples in Tests/calendarhelper_spec.lua.
  • One form filler: RaidTemplates.Form.Fill fills the create or edit window from any subset of title, type (with picture), time, description and lock; Form.Apply (templates) now goes through it.
  • Minimap button spec under the coverage run: the button's PLAYER_LOGIN frame was a local nothing referenced, which the offline harness collects (the client never does); under the coverage run that happened before login and five minimap specs failed. The frame is now kept on ClassicCalendar.
  • Specs: Tests/raidtemplates_spec.lua gains 13 GameEvents examples on the real windows (the server's open answer delivered through the calendar's own handler; the clock pinned in October, the events' month) and 7 for the template menu, prompts, loot setter, menu button and picture path. Tests/deleteconfirm_spec.lua checks the named question; Tests/addonevents_spec.lua checks PLAYER_MADE against the calendar's own _CalendarFrame_IsPlayerCreatedEvent through the source. 100% line coverage of GameEvents.lua, AddonEvents.lua and RaidTemplates.lua. Tests/todayview_spec.lua checks the right-click month after the calendar pages; Tests/calendarhelper_spec.lua loads GameEvents.lua, which both create paths now use. Suite 979.
  • Seen in game (operator, 2026-10-13, Classic Era): create, edit and delete each worked from /run, as did AddEvent, GetTodayEvents and ClearEvents. The filled-in edit survived the window's redraws, the Create press in a window the API opened saved the event, and the create window's date line named the right weekday.

Calendar prompts open beside the calendar, not over it

Files: Patches.lua, ClassicCalendar.lua, RaidTemplates.lua.

Operator, 2026-10-13, after testing the API's delete question in game: "it should windowsnap to the next top/right open area".

  • New ClassicCalendar_PlaceBesideOpen(popup). It puts the popup's top-left corner against the top-right corner of the rightmost calendar window that is showing (the calendar, Today View, the event, holiday and raid-reset windows, the class-count strip beside them, the World Buff windows, Auto-Moderators), with the same 16-pixel drop as the docked side windows when that window is the calendar itself. Edges are compared in screen units, since those windows do not share a parent. Beside any other window the popup is level with that window's top, so beside the class-count strip it starts 30 pixels below the event window's top. When the space would run off the right or the bottom of the screen (the bottom check from peer review 0df90615, C1), or none of them is open, the popup stays where LibAceGUIWidgets centred it.
  • Used by all three W:ShowDialog prompts: the event delete question and the raid template save and delete prompts. The library re-anchors its one shared dialog on every call, so moving it after the call leaves nothing behind for the next addon's prompt.
  • The same look as the addon's other windows. Operator, 2026-10-13: "you need to use the same border/look/feel for the popup as you do the others." All three prompts pass style = "window" and a title ("Delete Event" from the client's CALENDAR_DELETE_EVENT, "Save Raid Template", "Delete Raid Template"), so LibAceGUIWidgets draws them with BasicFrameTemplateWithInset, the title bar, close X and inset the World Buff and Auto-Moderators windows use. Its close X takes the Cancel path. This needs LibAceGUIWidgets MINOR 42 (v0.3.2, inbox 82cae97e). An older library ignores the two fields and draws the plain dialog.
  • Specs: six placement examples in Tests/deleteconfirm_spec.lua: beside the calendar, past a window already open, past the class-count strip (peer review C2), stays put off the right, stays put off the bottom, nothing open moves nothing. One look example checks the window style, the title, and that the close X deletes nothing and leaves no modal. Tests/raidtemplates_spec.lua asserts the style and title on both template prompts. The "closes only its own prompt" example now covers another addon's prompt in each look. The examples hide the calendar again afterwards with its scripts removed, since a calendar left shown kept answering later spec files' calendar events. Suite 990.

[v1.7.4] (2026-10-11)

Menus and pickers moved off UIDropDownMenu

Files: ClassicCalendar.lua, CalendarTodayView.lua, ClassicCalendar_Options.lua, GuildRankConfirm.lua, WorldBuff.lua.

FastGuildInvite finding, inbox thread 0c878e13, from the operator's WoW Forever taint.log (2026-10-05). Any addon call to UIDropDownMenu_Initialize, ToggleDropDownMenu or CloseDropDownMenus writes the shared UIDROPDOWNMENU_* globals from insecure code. Three of ours ran from XML OnLoad (the hidden CalendarFilterDropDown, CalendarAddonDropDown and CalendarTodayViewFilterDropDown) and three more at ADDON_LOADED (the options pickers), so the addon tainted those globals at every login.

  • Calendar buttons: the calendar's Filter and Add Events arrows and the Today View's Filter open Blizzard_Menu menus through CalendarToggleButtonMenu (first click opens, second closes, the client closing it by itself is noticed). Both Filter buttons share one generator, CalendarFilterMenu_Generate, where there were two copies. The three hidden dropdown frames are gone from ClassicCalendar.xml.
  • Our own pickers: the options panel's Start Day and Event Alarm pickers, the Rank Auto-Confirm control and the World Buff "Dropped" box are LibAceGUIWidgets dropdown boxes. The options pickers refresh their text when the panel shows and after a pick, instead of every frame.
  • Filter defaults at load: the removed Filter dropdown's OnLoad was what wrote "1" into every filter setting nobody had chosen, and HolidayIsEnabled lists a holiday only when its setting is exactly "1". Without it every holiday stayed hidden until the player opened Filter once. ClassicCalendar.lua now applies those defaults at load, World Buffs excepted (it lives in CCConfig). Caught by forever_spec going red; diagnosis confirmed by Peer Review reply afe4e100. HolidayIsEnabled (HolidayData.lua) also now hides a holiday only on an explicit "0", the rule the raid-reset filter already used: a setting the client never registered reads nil and SetCVar cannot create it, so the default alone would leave holidays hidden there. Specced in Tests/holidaydata_spec.lua, red first. That rule is now one function, CalendarFilterShown (HolidayData.lua), asked by the holiday list, both raid-reset checks in Patches.lua and the Filter checkboxes: the checkboxes read GetCVarBool, so on an unregistered setting the box showed unticked while the calendar listed the holidays, and ticking it could never stick. The checkbox also registers a setting the client lacks (C_CVar.RegisterCVar, falling back to the RegisterCVar global) before defaulting it. Peer Review reply 65931f41 (G1); Tests/buttonmenus_spec.lua models a client that refuses SetCVar on an unregistered name, red first. Whether an addon-registered CVar keeps its value across sessions is not verified; the "anything but 0" rule shows holidays either way.
  • World Buff events stayed hidden after creating one: WorldBuffs:CreateGuildEventWithDate (WorldBuff.lua) meant to switch the World Buffs filter back on, but tested and set a calendarShowWorldBuffs CVar, while that filter lives in CCConfig.calendarShowWorldBuffs. So a player who had unticked World Buffs created an event they could not see. It now switches the CCConfig value on and redraws. Found by this session's self-audit; Tests/calendarhelper_spec.lua, red first.
  • Behaviour changes: the open sound plays only when a menu actually opened; the alarm number picker shows an empty list instead of raising when no alarm unit is saved; Raid Events' officer refusal no longer calls CloseDropDownMenus, since a menu button returning nothing closes the menu.
  • Rank Auto-Confirm closed other addons' dropdowns: closing an event window called CloseDropDownMenus(), which shuts whatever dropdown any addon had open. It now closes only its own menu.
  • A new World Buff entry opened as "Dropped: Yes" straight after editing an entry marked Yes: add mode reset the name and date but not Dropped. WorldBuffs:SetDroppedValue resets it.
  • Known cost: the ported Create Event and Mass Invite forms still initialise UIDropDownMenu from their OnLoad; the calendar itself is left alone by standing directive, so login taint is reduced, not removed.
  • Specs: new Tests/buttonmenus_spec.lua (filter entries and ticks, defaults at load with a holiday listed and no menu opened, toggle open/close, modal close, Add Events entries and officer refusal, the Dropped reset, and a scan that no shipped file toggles or closes the shared dropdown and only the two ported forms still initialise one). Tests/rankconfig_spec.lua reads the library's item list. Tests/calendarhelper_spec.lua's create examples now clear the Create Event form functions an earlier spec may have loaded, instead of depending on file order.
  • Not seen in game: the boxes' positions were derived from the old dropdown padding, not by looking.

/ch create printed a developer dump into chat (CalendarHelper.lua)

CalendarHelper peer-review thread 06a03a90 (2026-10-11): the "Event Form State Before Create" block in CreateGuildEventWithDate, a C_Timer.After(0.1, ...) inside the time-setting callback, printed about eleven diagnostic lines on every event import. Every other diagnostic in that function is behind DEBUG_MODE; this one was not.

  • Fix: the block returns early unless DEBUG_MODE is on.
  • Specs: Tests/calendarhelper_spec.lua "event creation chat output" drives the whole create path through its nested timers and asserts the dump is absent with debug off and present with debug on, so the first example cannot pass by never reaching the block. Proven by a red run: with the guard removed, the debug-off example fails and lists the dump. Its time stand-in is gone; the spec loads the real RaidTemplates.lua, whose To12 gets midnight right (Peer Review reply d4073f0f).

The World Buff announce printed three debug lines too (WorldBuff.lua)

Found by reading WorldBuffs:CreateGuildEventWithDate, the function CalendarHelper's create was copied from; a text search for the CalendarHelper dump's wording had missed it. Every announce printed "WorldBuffs Debug: Setting event title", "Title field contains" (or "CalendarCreateEventTitleEdit not found!") to every player.

  • Fix: all three are behind DEBUG_MODE. The player-facing "Guild event pre-filled" and "Opening guild event creation" lines stay.
  • Specs: Tests/calendarhelper_spec.lua "World Buff event creation chat output", debug off (no debug lines, the confirmation still prints) and debug on (both lines print).
  • And at every login: WorldBuffs:Initialize, run from ADDON_LOADED, printed "ClassicCalendar: WorldBuff module loaded" and a "WorldBuff Debug: ... calendarShowWorldBuffs ..." line to every player. All three are behind DEBUG_MODE now. Peer Review D1 on self-audit e7184602. Tests/calendarhelper_spec.lua "World Buff module login chat output" fires a real ADDON_LOADED: nothing printed with debug off (red first, 2 lines), both lines with debug on. A read of every other print in WorldBuff.lua found the rest either gated or answering a player action.

The World Buffs checkbox read an unset value as off (ClassicCalendar.lua)

The calendar's World Buff display filter treats an unset CCConfig.calendarShowWorldBuffs as shown; CalendarFilter_IsSelected read the same unset value as unticked whenever CCConfig already existed. It now uses ~= false, the display filter's rule, and the toggle still flips correctly because it is computed from the getter. Found answering item 5 of self-audit e7184602. Tests/buttonmenus_spec.lua, red first. Also labelled Blizzard's original, commented-out CALENDAR_FILTER_CVARS as not live, since it has been misread as live twice.

One debug flag instead of two

The debug switch had two spellings, the DEBUG_MODE global and a debug field on the ClassicCalendar table, kept in step by hand at four write sites. The options loader in ClassicCalendar_Options.lua set only DEBUG_MODE, so /ch create's "Creating guild event" line and the /ch list login hint could follow a different switch from the rest of the same command.

  • Fix: DEBUG_MODE is the only flag. The field and its writes are gone (ClassicCalendar.lua, Patches.lua /caldebug, ClassicCalendar_Options.lua box and loader), and its readers in CalendarHelper.lua, ClassicCalendar.lua, WorldBuffDB.lua and WorldBuffSync.lua read DEBUG_MODE. /calwbsync no longer prints the second flag's value.
  • Specs: Tests/optionscheckbox_spec.lua "Debug mode has one switch": the options box turns the create line and the login hint on and off, and no file in the TOC mentions the old field. raidpicker_spec and worldbuffdb_spec toggle DEBUG_MODE now.

[v1.7.3] (2026-10-05)

WoW Forever: every holiday listed twice, some days without art

Files: Forever.lua. Call to Arms and Darkmoon Faire days had no art.

Operator's Forever screenshots (2026-10-05, thread d26b0d35): Hallow's End starting twice (4:00 AM and 8:00 AM), two fishing contests every Sunday, four "Call to Arms" lines every Tuesday. The Forever server sends its own holidays through C_Calendar, and stubbedGetDayEvent appends ours after the client's. Era and TBC servers send none. I never checked the Forever server before claiming below that no client gives dates at runtime.

  • Fix, Forever only: Forever.lua wraps stubbedGetNumDayEvents/stubbedGetDayEvent. Server events stay first, so C_Calendar.GetHolidayInfo's index still names them. Our HOLIDAY entry with the same title on the same day is dropped, since the server owns its dates. An exact title match only.
  • Art: Forever draws the server's iconTexture (wow-ui-source-forever Blizzard_Calendar.lua:1662-1685), which the Call to Arms and Darkmoon days lacked. A server holiday with none borrows ours for the same sequence day, unless that art is switched off.
  • Description: newGetHolidayInfo falls back to C_Calendar.GetHolidayInfo.
  • Cost: the merge is cached for one CalendarFrame_UpdateDayEvents call only.
  • The Tuesday-only Call to Arms is not a date bug: Forever runs whole weeks, Tuesday to Tuesday (both copies said 10/13 - 10/20). Names show on first and last days only; the days between get the banner art.
  • Tests: six in Tests/forever_spec.lua. Not seen in game.

CurseForge now installs LibDBIcon for Classic Era and TBC players

Files: Libs/LibDBIcon-1.0.

Found by the harness's .pkgmeta checker during release prep. The embedded LibDBIcon copy, which only the Forever TOC loads, carried a --@curseforge-project-slug: libdbicon-1-0@ marker on its first line. The BigWigs packager reads .pkgmeta's required-dependencies first. Then, while copying files, it sets relations[slug] = "embeddedLibrary" for every marker it finds (release.sh, copy_directory_tree), which overwrites the earlier entry. So since the embedded copy arrived in v1.7.0, every upload has told CurseForge that LibDBIcon is embedded. The CurseForge app then does not install it, although ClassicCalendar.toc and ClassicCalendar_TBC.toc list ## Dependencies: LibDBIcon-1.0. A new Era or TBC player with no other addon pulling LibDBIcon in would get no calendar. The marker line is removed; the library code is unchanged. The checker now reports libdbicon-1-0=requiredDependency. .luacheckrc also excludes Libs, because vendored upstream code is not ours to restyle.

Battleground art sits behind other holidays' art

Files: ClassicCalendar.lua, Patches.lua, Forever.lua.

Operator, 2026-10-05: "as the BG ones are 'all the time' can we have their art behind the other event art?" CalendarFrame_UpdateDayTextures drew the FIRST holiday's sequence banner. So a battleground listed first hid another holiday's banner, and a holiday with no banner of its own (the faire's middle days) left the cell bare even with a battleground running. New CalendarDayBanner (Patches.lua) picks the first holiday with a banner that is not a battleground, and falls back to the first battleground. Events carry isBattleground, set from the schedule's BattlegroundsArt; on Forever, the server's copies are marked by name in the merge. Blizzard's own rule stays: no banner for a sequence of 2 days or fewer. Three specs in Tests/monthcache_spec.lua. Not seen in game.

Also checked: Forever's Call to Arms really is a week long. Its Holidays rows 1840-1842 (build 1.60.1.70205) carry Duration_0 = 168 hours on and Duration_1 = 336 off, looping from 21 April 2026 08:00. Classic's weekend rows 283-285 carry 100 hours.

WoW Forever: raid resets for its own raids (HolidayData.lua)

Operator, 2026-10-05: "do we not have raid resets in forever?" GetClassicRaidResets returned nothing on Forever, by my own decision, until Blizzard published lockouts. It now lists Barrow Deeps, Hyjal Summit and Onyxia's Lair from their opening on 9 December 2026 (wowguide.net roadmap; lfcarry.com). They are weekly, anchored on the client's weekly reset (C_DateAndTime.GetSecondsUntilWeeklyReset) through the existing opens handling, so the first shows on Tuesday 15 December. Weekly is an assumption: Blizzard has published no lockout rules, and only a fan wiki (world-of-warcraft-forever.wiki) reports weekly. Names come from GetRealZoneText(249)/(534) where the client has them, not verified on Forever, else English. holidayyears_spec now pins the three raids instead of an empty list.

Added events cached per month: the viewed month and the one either side

Files: Patches.lua.

Operator, 2026-10-05, asking about micro stuttering: "you are only supposed to cache the current month, the previou and the next, then that way it's warmed and you look up once a month is scrolled". No such cache existed. stubbedGetNumDayEvents/stubbedGetDayEvent re-derived every day's added events from the schedule on every call, several times per day cell. v1.7.3 made that heavier: each call rebuilt GetClassicRaidResets, which now reads C_DateAndTime.GetSecondsUntilWeeklyReset and, on TBC, names seven raids and steps their anchors forward week by week. With the calendar closed none of this runs (ClassicCalendar.lua:1343).

  • Now: the added events (holidays, faires, battlegrounds, raid resets) are built once per month, in one pass. The cache keeps the client's current month and the one either side, which is exactly what one redraw reads, because the grid shows the edge days of both neighbours. Scrolling builds only the one new month; the other two are warm.
  • Live, not cached: the client's own events (guild events, invites, the Forever server's holidays) are read from C_Calendar on every call.
  • Rebuilt when anything the month was built from changes: the holiday schedule (rebuilt daily), the day, a filter CVar, a holiday's art setting, or the level-up raid setting.
  • The validity check allocates nothing. It runs on every lookup, about 250 per redraw. My first version built a signature string each time, which is the kind of garbage that makes the client hitch when the collector runs; it now compares values in one reused array.
  • Tests: new Tests/monthcache_spec.lua, 4 examples. Not measured in game.

Event dates checked against the game and published schedules, per client

Files: HolidayData.lua.

Every date the addon generates was checked against three sources, and the results are recorded with links in docs/Event_Date_Sources.md. That file also says how to get next year's dates. The three sources: each client's own Holidays table (wago.tools CSV, decoded), the client API source in F:\Blizzard API Docs, and published schedules. No client gives holiday dates at runtime. Only the WoW Forever client's table carries real dates; Era and TBC leave every date to the server. For 2025-2026, Forever's dates matched the published Classic dates. Until now all three clients got Classic Era's data unchanged. That was my design, and it was wrong for TBC and Forever.

  • Dates corrected (all clients):
    • Love is in the Air: was Feb 7-20, now Feb 11-15.
    • Noblegarden: was a week, now Easter Sunday only.
    • Children's Week: was May 1-7, now Apr 27 - May 4 in 2026. Game dates for 2025-2026, then the Monday on or before 1 May.
    • Harvest Festival: was Sep 27 - Oct 4. It now starts 4 days before the Mid-Autumn (moon) Festival, which reproduces all three of the game's dates (2025 Oct 2, 2026 Sep 21, 2027 Sep 11). New MidAutumn(year): published dates to 2035 (travelchinaguide, usemooncal), and a mean-new-moon fallback past that, measured within a day (9 of 11 table years exact).
    • Lunar Festival 2027: now Feb 2, where the Chinese New Year rule gave Feb 5.
    • Battleground weekends: now end Tuesday 04:00 (100 hours), was the 07:00 reset.
    • The game's dates live in a new KNOWN_STARTS table and win over the rules. A moved holiday keeps its template length (templateSpanDays).
  • Per-client schedules:
    • New CalendarFlavour() returns era, tbc (interface 2xxxx from GetBuildInfo) or forever. Forever can't be told from retail by build, so ForeverScrollBars.lua, which only the Forever TOC loads, now sets ClassicCalendar.isForever.
    • Template entries can carry flavours and portal gates.
    • TBC gains Brewfest, Sep 20 - Oct 6.
    • TBC's Lunar Festival runs three weeks: Feb 16 - Mar 9 2026, by both Zygor's 2026 guide and Warcraft Tavern's TBC guide. Classic Era keeps two weeks. Later years are assumed to keep three weeks, which is inferred from 2026 alone.
    • Forever puts Noblegarden on Easter Monday and adds Fireworks Spectacular on Jul 4, US realms only. Both come from its client table.
    • Forever replaces Classic's battleground weekends with its whole battleground weeks: Tuesday 08:00 to Tuesday 08:00, rotating Darkspear Islands, Arathi Basin and Warsong Gulch, each one week on and two off. These are marked keepTimes. Recurrences now restore their hours, because a daylight-saving change between two recurrences moved the clock hour (found by the new spec).
    • TBC gains its raid resets:
      • Karazhan, Gruul's Lair and Magtheridon's Lair from 19 Feb 2026.
      • Serpentshrine Cavern and Tempest Keep from 14 May.
      • Hyjal Summit and Black Temple from 27 Aug.
      • All are weekly, with phase dates from the expcarry roadmap. A raid never shows a reset from before it opened, including when the client gives the anchor.
      • Names come from GetRealZoneText(mapID) in the player's language (ZoneScriptDocumentation.lua:27 on TBC), with English as the fallback. The map IDs are from the TBC client's Map table.
      • Zul'Aman and Sunwell are left out until their dates are announced.
    • Forever gets none of the Classic raids, none of which exist there. Its own raids are covered in the Forever raid-resets section above.
    • HolidayText falls back to English, because only enUS has Brewfest.
  • Season of Discovery's Darkmoon Faire runs every two weeks: new SoDDarkmoonScheduleFor, Monday to Sunday every 14 days from Monday 13 January 2025 (Elwynn), alternating with Mulgore. It reproduces every 2025 date in zockify's SoD table and atlasforge's 2026 dates (21 September Elwynn, 5 October Mulgore, 19 October Elwynn, 2 November Mulgore). SoD used the monthly Classic faire before. The faire record is now built by one shared faireEntry.
  • Peer Review fixes (thread 2c750726):
    • The client-anchored weekly reset showed an hour early. CalendarTime has no seconds, so the anchor landed 0-59 seconds before the reset and the forced min = 0 turned 06:59 into 06:00. It now rounds UP to the next whole minute and keeps that minute. The reviewer suggested rounding to the nearest minute, but that still gives 06:59 past :30; the new spec, with the clock 45 s into a minute, caught it. The 52-week step back is now in calendar days, not seconds, so a daylight-saving change in between keeps the hour.
    • The holiday daylight-saving shift used the US rule for every region. getDSTDates and changeWeekdayOfDate are replaced by realmOnSummerTime, with per-region rules: US, EU (last Sunday of March to last Sunday of October), Oceanic (southern hemisphere), and none for KR/TW/CN. EU Hallow's End no longer ends at 22:59. Specs cover every region, including Oceanic, detected from "(AU)" in the realm name.
    • Fireworks Spectacular carries keepTimes: its client-table time is shown as written, not shifted. Which clock Holidays.db2 uses is not verified.
    • Noblegarden two-day bug: the new schedule-level spec was watched failing against the old 00:01 start (2026-04-04), then passing on the fix.
  • Weekly raid resets come from the game: C_DateAndTime.GetSecondsUntilWeeklyReset now anchors the weekly raids' day and hour on every client except SoD. Before, the day and hour were guessed from the region. The EU-Wednesday and AU-18h guesses remain only as the fallback. The anchor is stepped back 52 weeks, because Patches.lua projects resets only forward of it.
  • Test environment: cc.reset() now clears ClassicCalendar.isForever and scrollBarTemplate. Before this, the Forever spec's flag leaked into every later spec file.
  • .pkgmeta: all comments removed, per the packager rule. The reasoning they held is in CLAUDE.md.
  • Dead code removed from HolidayData.lua: GetFollowingSunday, the unused SoDBattlegroundWeekends table, UBRSName, and whitespace-only lines. Nothing called any of them.
  • Specs:
    • holidayyears_spec: the 2026 table now holds the sourced dates, and the Noblegarden and Lunar 2027 examples follow the sources.
    • holidaydata_spec: battleground weekends now end at 04:00.
    • New examples cover the game-table dates, the Children's Week rule, Brewfest on TBC only, Forever's Noblegarden, Fireworks (US only), the battleground weeks (Darkspear Islands on Oct 6 2026), Forever's raid list, and the weekly reset taken from the API.
  • Resolved later in this release: TBC raids, SoD's fortnightly faire and TBC's three-week Lunar Festival are above. The 2026 Era battleground rotation matches zockify's 2026 dates. Forever's raid resets and its server-sent holidays have their own sections.
  • Still not verified, on the todo list: TBC's Darkmoon Faire third site (Terokkar Forest), and TBC's Call to Arms rotation, which keeps the Classic one.

The Darkmoon Faire now appears in 2027 and later (HolidayData.lua)

  • Cause, mine. The faire came from two hand-typed tables, ClassicDarkmoonSchedule2025 and ClassicDarkmoonSchedule2026, and GetClassicDarkmoons returned the 2026 one for every year from 2026 on. Every entry carried year=2026, so from 1 January 2027 there would have been no faire on the calendar. It is the same defect v1.7.2 fixed for the seasonal holidays, and I left it in place then.
  • Fix. Both tables and GetClassicDarkmoons are replaced by DarkmoonScheduleFor(year). The Classic faire sets up on the first Friday of the month, opens the Monday after and runs to that Sunday; it is in Elwynn Forest in odd months and Mulgore in even ones. That rule is the one the 2026 table's own comment stated, and Icy Veins' and zockify's Classic schedules agree. It reproduces all twelve 2026 dates and eleven of the twelve 2025 dates; the 2025 table had January opening on Tuesday the 7th rather than Monday the 6th. GetClassicHolidays now adds the faires for last year, this year and next under the same one-year horizon as the seasonal holidays, so December also shows next January's faire, which it could not before.
  • Not verified: that Blizzard keeps this rule in 2027. As with the seasonal holidays, 2026 is the last year checked against the game. SoD still gets the Classic schedule, as before.
  • Spec. darkmoon_spec gains a DarkmoonScheduleFor block. It must reproduce the shipped 2026 dates and zones to the day, open on a Monday in every month of 2025-2030, put a March 2027 faire on a 2027 calendar, and show January 2027 from 20 December 2026 with the schedule still under the 500-entry rebuild limit. The existing zone examples' nil check now goes through assert(...).

Editor warnings cleared (no change for players)

v1.7.2 was tagged with 134 warnings in the editor's Problems panel; clearing that panel is now a step in the release checklist in CLAUDE.md, along with the Discord announcement, which was also missed. Every warning was read and checked against the code or Blizzard's source before it was changed.

  • One real slip, harmless: ClassicCalendar.lua passed selectedAM as a second argument to GameTime_ComputeStandardTime, which takes only hour on Classic Era (Blizzard_Minimap/Classic/GameTime_Shared.lua:62) and Forever (Blizzard_FrameXMLUtil/GameTimeUtil.lua:56). It was always ignored; removed.
  • CalendarTodayView.lua: two OnMouseUp handlers shadowed the enclosing self (renamed area); an unused ..., loop index, self and a dead cvarValue = "1" assignment removed; whitespace-only lines emptied. Behaviour unchanged.
  • Undeclared globals: the Today's Events XML frames (CalendarTodayViewFrameTitle, CalendarTodayViewHeaderBgDateText, ...) and the cross-file functions Calendar_Show and CalendarFrame_GetEventFrame are declared in .luarc.json, which .luacheckrc also reads.
  • Annotation false positives: UIDropDownMenu_AddButton(info, level) and DEFAULT_CHAT_FRAME:AddMessage(msg, r, g, b, id) are correct client calls (Classic Era UIDropDownMenu.lua:373, ChannelFrame.lua:376) that the editor's WoW API annotations declare with fewer parameters. Each is silenced on its own line with that evidence. Methods the specs stub (WorldBuffs:GetWorldBuffDataTable, EditEntry, CreateGuildEvent, WorldBuffSync:CatchUp) carry a line-scoped duplicate-set-field disable, and WorldBuffFrame = nil an assign-type-mismatch one.
  • Specs: nil reads go through assert(...) instead of being indexed unchecked, so a nil fails naming what was missing; specs that stub client globals on purpose carry a file-level duplicate-set-field disable with the reason. No example changed what it checks.
  • luacheck across the whole repo, for the first time. The desk's lint_all had been handing luacheck every file on one command line, which cmd.exe refused, so luacheck checked nothing; writ fixed that on 2026-10-05. Its first full run found four warnings in specs, now fixed. clientmodel_spec had two locals shadowing an outer text (renamed shipped and docText), localization_spec's UTF-8 check had a cc shadowing the env module (renamed cont), and wbconvergence_spec had an unused bob. .pkgmeta loses its "*.bat" ignore entry, which matched no file in the repo.

[v1.7.2] (2026-10-03)

WoW Forever: "Unable to load atlas entry UI-ClassicScrollBar-Center"

LibAceGUIWidgets thread 3ee3aa5b.

Reported from the Forever client (BugSack, 24x and then 28x): Blizzard_SharedXML/Shared/Scroll/TrimScrollBar.xml:337 Texture Texture: Unable to load atlas entry UI-ClassicScrollBar-Center.

  • Cause, mine. The calendar's four scroll bars (View Event and Create Event descriptions, the event picker, the texture picker) inherited Blizzard's WowClassicScrollBar in ClassicCalendar.xml. Its middle track art is that atlas (wow-ui-source-forever TrimScrollBar.xml:337), which Forever's client does not have. v1.7.1 restored the template after v1.7.0's per-flavour alias broke Classic Era, and I first declined this report as unfixable. That was wrong; the operator overruled it.
  • Why not Forever.lua: the atlas is applied only while the template is built, which happens during ClassicCalendar.xml and before any file loaded after it.
  • Fix. The four bars are no longer declared in ClassicCalendar.xml. Each owning frame's OnLoad builds its bar with the new CalendarCreateScrollBar (ClassicCalendar.lua), using the same anchors, strata and hidden background as the XML had. The template is ClassicCalendar.scrollBarTemplate or "WowClassicScrollBar", the same pattern as Blizzard's ScrollFrame_OnLoad (SecureUIPanelTemplates.lua:44). New ForeverScrollBars.lua, loaded by ClassicCalendar_Camelot.toc only and before ClassicCalendar.xml, sets it to MinimalScrollBar, which Forever's own calendar uses for the same four bars. Classic Era and TBC keep WowClassicScrollBar, and their TOCs are unchanged.
  • Specs. forever_spec builds the real XML both ways. Each of the four bars must come from WowClassicScrollBar (Era/TBC load) or MinimalScrollBar (Forever load). Whether a bar has the Init that v1.7.0 lost on Era cannot be checked offline: the harness builds Blizzard templates without their mixin Lua, so Init is nil there on every route (my first version of the spec asserted it and went red on both flavours). tocorder_spec now pins that the shared XML declares none of these bars, that only the Forever TOC loads ForeverScrollBars.lua, and that it loads it before the XML.
  • Tested by the operator in the Forever client (2026-10-03): "i'm in forever, no more lua errors, addon still works" (relayed on thread 3ee3aa5b). Still unverified in game: that the bars look and scroll as before on Classic Era and TBC.

Seasonal holidays now appear in 2027 and later (HolidayData.lua)

Found during the documentation check below; the defect is mine.

  • Cause. CLASSIC_CALENDAR_HOLIDAYS held fixed 2026 dates (plus Winter's Veil 2025-26), and GetClassicHolidays used them as they stood. From January 2027 nothing from Lunar Festival through Winter's Veil would have shown. The Easter and Chinese New Year helpers in the file were never called, and the Lunar Festival ones used +/-7 days, which matched none of the shipped data.
  • Fix. The table is now a 2026 template, and SeasonalHolidaysFor(year) derives each year from it, using the rules the 2026 data itself follows. Noblegarden starts on Easter Sunday (GetEasterDate) and runs a week. Lunar Festival starts the day before Chinese New Year and ends 13 days after it, using a table of published dates for 2025-2035 (chinatravel.com, vclock.com), with the moon-phase estimate as the fallback beyond that. Every other holiday keeps its month, day and times. The schedule builds last year, this year and next, so last year's Winter's Veil still shows in early January, and drops anything starting more than a year from today: the same one-year horizon the recurring events use, which holidaydata_spec "keeps recurring events inside the one-year rolling window" enforces. My first version added the whole of next year and failed that example on Harvest Festival. The dead GetLunarFestivalStart/End are replaced, and the separate Winter's Veil 2025-26 entry is gone because the derivation produces it.
  • Not verified: that Blizzard runs these holidays on the same rules in 2027. The 2026 dates were the only ones checked against the game; later years are the same pattern carried forward.
  • Not changed: the Darkmoon Faire still uses the 2026 schedule for every year from 2026 on (GetClassicDarkmoons).
  • Spec. New Tests/holidayyears_spec.lua. The 2026 derivation must reproduce the shipped 2026 dates to the day. Easter-based and Chinese New Year-based dates are checked for 2027-2030 against published dates. A 2027 clock must produce a 2027 Lunar Festival, and 1 January 2027 must still show Winter's Veil 2026.

/calwbclear guild asks first; the dead Fireworks Spectacular option is removed

  • /calwbclear guild (Patches.lua) wiped the whole guild's world-buff data immediately, while the README promised a prompt. The bare word now prints what it will do, and /calwbclear guild confirm does it: the same two steps as the personal /calwbclear confirm. New Tests/calwbclear_spec.lua pins both halves with a stand-in WorldBuffSync.WipeGuild.
  • The Fireworks Spectacular art checkbox (ClassicCalendar_Options.lua) is deleted. It was gated on isSoD, which Patches.lua sets two files later in the TOC, so it was never built on any client; and no holiday in HolidayData.lua uses FireworksSpectacularArt. Its default (CCConfig key) and locale strings are left alone. HolidayData.lua's moon-phase fallback no longer reassigns one local to a different type (language-server cast-local-type).

Documentation brought in line with the addon, again

README.md, docs/Curseforge_Description.html and docs/Curseforge_Description.md were checked against the code once more, and they still claimed things the code does not do. Each item was read in the source before the docs were changed.

  • The World Buff window is officer-only. /worldbuff returns before building the window for a non-officer (WorldBuff.lua:1660-1663), and the Add Events entry falls back to that command. The README said "everyone can view".
  • /calwbclear guild has no confirmation prompt. It calls WorldBuffSync:WipeGuild directly (Patches.lua:813-816). Only the window's Clear All (Guild) button asks first (WorldBuff.lua:558-583). /calwbclear confirm clears every world-buff table saved on the account, not only "your own".
  • There is no Fireworks Spectacular holiday. HolidayData.lua has no such entry. The art toggle in ClassicCalendar_Options.lua:517 is behind isSoD, which is not yet set when that file loads (it is set in Patches.lua, two files later), so the toggle is never created. Removed from the HTML in two places.
  • The seasonal holidays carry fixed 2026 dates (HolidayData.lua:289 onward). The HTML said "generated for the current year" and "accurate 2026 dates ... and more". Corrected, and fixed in the section above.
  • The tracked world buffs are exactly four (WorldBuff.lua:19-24), not "and more".
  • The Raid Events picker is officer-only (ClassicCalendar.lua:2320-2326) and lists CalendarHelper's saved raid events.
  • Added where missing: Shift+click on Add Events opens Today's Events; the Templates menu deletes templates and adds the Dibs loot line; Rank Auto-Confirm can be set while creating an event; the undocked World Buff window remembers its spot; untranslated option text falls back to English; the options' Discord link button; the "hide level-up raid resets" option shows everywhere but only has an effect on Season of Discovery; /ch list, create <number> and status.

[v1.7.1] (2026-09-30)

Event reminders: hour units and invite statuses (Patches.lua AlarmUpcomingEvents)

Found while checking the documentation against the code; both defects are mine.

  • An "N hours" reminder fired N minutes before the event. The options panel saves the unit as "minute"/"hour" (ClassicCalendar_Options.lua), and the reminder compared against "Minute"/"Hour", so the hour branch never matched and the multiplier stayed at 60 seconds. The comparison is now case-insensitive. The fully localized 15-minute message (CALENDAR_EVENT_ALARM_MESSAGE) had the same mismatch and was never used; it is now.
  • Reminders went to the wrong invite statuses. alarmInviteStatuses was a list indexed by position, and the check read alarmInviteStatuses[event.inviteStatus]. With Classic Era's Enum.CalendarStatus (Available 1, Declined 2, Confirmed 3, Out 4, Signedup 6, Tentative 8; CalendarConstantsDocumentation.lua), that matched Available, Declined, Confirmed and Out, and never Signed up or Tentative. It is now a set keyed by status value.
  • Spec: new Tests/eventalarm_spec.lua (10 examples) loads Patches.lua, fixes the clock, stubs one player event and fires VARIABLES_LOADED, then reads what reached chat.
  • Not changed, noted: the reminder only looks at today's events, so an hours reminder for an event early tomorrow does not fire the evening before.

Documentation brought in line with the addon

README.md and the CurseForge description were checked claim by claim against the code. Corrected: the filter list (there is no player/guild-event filter; Darkmoon Faire, weekly holidays, raid lockouts and raid resets were missing); the options layout (art toggles, guild settings and event alarms are sections of the General page, not separate tabs); /caldebug is available to everyone, not officers only; "itIT" removed from the locale list (no Italian table exists); the world-buff title rule ("Dropping" plus the buff, or "World Buff"); "placeholder entries" are a Create Placeholder button that schedules a TBD event. Added: the minimap icon's clicks, unlocking/hiding the calendar button, the Season of Discovery level-up raid reset option, Request Guild Settings, /ccwb log and reset, /calwbclear confirm and status, and three known issues the README lacked. docs/Curseforge_Description.md got the same corrections (its keyword list, "Art tab" and filter line were also stale), and README, the HTML and the .md now carry the same known issues: the Burning Crusade holiday-date caveat and the guild event invites broken since 1.15.3 were each missing from one or two of them.

Classic Era and TBC: the View Event window raised a scroll-bar error

A Forever change of mine that reached every flavour.

Reported from the client (BugSack, 16x): ScrollUtil.lua:127: attempt to call a nil value in Init, from CalendarViewEventDescriptionContainer_OnLoad (ClassicCalendar.lua:3805).

  • Cause, mine. v1.7.0 routed the calendar's four scroll bars through an addon template, ClassicCalendarScrollBar, defined per flavour (ScrollBarClassic.xml for Era/TBC, ScrollBarForever.xml for Forever), to quiet a missing-atlas warning on Forever. The operator's Forever work was the 1px border gap (Forever.lua); the scroll-bar change was a side change of mine, and because it lived in ClassicCalendar.xml, which every TOC shares, it shipped to Classic Era and TBC. There the View Event description's bar had no Init when ScrollUtil.InitScrollBar ran. The operator confirmed the revert in the Classic Era client: no error. Only that container calls Init; the Create Event one only registers, which is why it alone raised. Why the client built the bar without its mixin is not established -- the Era 1.15.9 source defines every template in the chain. The suite built it offline without complaint, so the harness does not reproduce it.
  • Fix. The four bars inherit Blizzard's WowClassicScrollBar directly again, exactly as v1.6.8 shipped. ScrollBarClassic.xml and ScrollBarForever.xml are deleted and gone from all three TOCs and from the eight specs that loaded them. tocorder_spec now pins every scroll bar in ClassicCalendar.xml (except the Today View's own MinimalScrollBar) to WowClassicScrollBar, and CLAUDE.md records that a Forever-only change never goes in the shared XML.
  • Known cost on Forever: the four "Unable to load atlas entry UI-ClassicScrollBar-Center" warnings come back there, because Forever's client lacks that one piece of the bar's art. Fixed in v1.7.2.

Test suite: the per-example reset no longer keeps old copies of the addon

WoWAPITesting, thread c0e6ec5b. Every old copy had stayed alive.

Development only; nothing a player runs changed.

  • Libraries the addon registers into are now reloaded on every reset. WoWAPITesting measured the suite's heap climbing from 7 MB to 270 MB with the frame count flat. The cause was mine. Tests/env_cc.lua reset the addon for every example but kept AceComm and VersionCheck-1.0 loaded, and each reload registered into them again. VersionCheck arrives through syncstartup_spec's DeltaSync load and then serves every later reload of ClassicCalendar.lua:192. So every example's copy of the addon stayed reachable. cc.reset() now forgets DeltaSync, AceCommQueue, VersionCheck, AceComm, AceSerializer and ChatThrottleLib. Each spec already loads the ones it needs right after the reset.
  • The hooked calendar functions no longer stack up. installGlobals() kept an existing CalendarFrame_ShowEventFrame (X = X or stub), so each example hooked the previous example's already-hooked function. CalendarCreateEventFrame_Update stacked up the same way through RaidTemplates.lua. Both are now cleared on reset.
  • Harness pin moved f7742d1 -> 544b409. Nothing in the Adoption log in between needed a change here.
  • Suite: 826 passed, 0 failed, one process, 120 s. WoWAPITesting read the run's telemetry: the heap now levels off at 22-41 MB and ends at 34 MB, where the previous full run reached 272 MB. Live frames stay around 6,000-8,400 instead of climbing.

[v1.7.0] (2026-09-27)

Raid scheduling: sign-ups, extra statuses, raid templates and the Dibs link

Contract DIBSREQ-CC-002.

Operator, 2026-09-26, through Dibs: raid scheduling, templates and sign-ups "belong in the clanedar", with "links to the right SR in dibs in the invite" and "New statuses in CC ouside of what the server tracks, synced with deltasync and canon hashes". Dibs owns the loot systems and READS what is built here.

  • Sign-up capture and sync (RaidSignups.lua, RaidSignupsSync.lua). Every client records its own answer to each upcoming guild event (GetGuildEventInfo), and whoever opens an event captures its whole invite list with class, note and EventGetInviteResponseTime. The merged roster rides the EXISTING DeltaSync host as one extension item, rs1:index (WorldBuffSync:RegisterExtension), not a second namespace. A player's row is split into records whose winner is decided whole -- obs, first, cls, note, extra -- so each keeps the canon its author minted (DeltaSync's canon rule); a field-level merge would produce rows no client authored. Event keys are tostring(eventID) and never converted back (a value above 2^53 loses precision). Captures wait for LibGuildRoster to be ready, so a half-built roster cannot key one player two ways.
  • The sign-up number never moves. It ranks by the EARLIEST response time ever seen for that player on that event, compared as numbers; no extra status and no re-answer changes it.
  • Extra statuses: late, tentative, bench, absent. Member-owned (a record is accepted only from the player it is about), newest wins. A strip docked under the open event window (RaidEventBar.lua) sets your own and, on hover, lists who holds which. The strip sits OUTSIDE the event windows because both are full; nothing of Blizzard's layout moved.
  • Raid templates (RaidTemplates.lua). A Templates button on the Create Event window, shown only when creating a guild event (where Mass Invite is hidden, so they never overlap), opens a menu: apply a template, save the form as one, delete one, and pick the Dibs loot system and raid. A template fills title, type and instance picture, time, description and lock; the leader presses Create (AddEvent is restricted). No size (Classic Era has no EventSetSize), no day (the leader clicks the day), no recurring nights (each still needs the leader's own Create press). Templates are the leader's, account-wide, never synced.
  • The Dibs link. The description ends with one line, Loot: <label> [cc:<system>:<raid>]. The whole description stays inside the box's 255-letter limit, and it is the rules text that gets shortened, never the loot line, and never mid-way through a UTF-8 character. When the open event carries the line and Dibs is loaded, the strip shows Loot in Dibs, which calls Dibs.API.Open(system, raid). Feature-detected by name; no dependency on Dibs.
  • Read API for Dibs: ClassicCalendar.API.VERSION = 1, UpcomingRaids(), Signups(key), LootLink(desc), and RaidsChanged / SignupsChanged callbacks. Everything returned is a copy. templateId is not in UpcomingRaids, although reply f0056fd5 named it: templates are never synced, so no reader's client could resolve one.
  • Three defects of mine, found while building this:
    • A clear could lose to the status it cleared. Stamps are whole seconds; Late then None inside one second tied, and the canon tie-break kept whichever hashed higher. RaidSignups.SetExtra now stamps a new choice after the one it replaces. Mutation-verified.
    • The strip's frame replaced its own module. It was created as CreateFrame("Frame", "ClassicCalendarRaidEventBar", ...), and a named frame is a global, so building it overwrote the module table of the same name. Renamed ClassicCalendarRaidEventStrip; pinned by a spec.
    • The Create Event window kept the last event's description. CalendarCreateEventFrame_Update cleared CalendarCreateEventDescriptionContainer.CalendarCreateEventDescriptionContainerScrollingEditBox, a key nothing sets (the container's OnLoad sets .ScrollingEditBox), so the box kept its old text while the event itself was reset to empty. What the leader saw was not what Create would send. Mutation-verified.
  • Specs: raidsignups_spec (38), raidsignupssync_spec (28) and the new raidtemplates_spec (26: the loot line, time conversion, the store, the real Create Event window, the strip). The client globals the create window reads, and the ScrollingEditBoxTemplate / ScrollingFontTemplate mixins, are now modelled by the harness (WoWAPITesting 7f2fd3f, bf8d140); the stand-ins that were staged here for them are deleted.
  • Suite runs in one process. The "15-minute limit" I first reported was the desk's default test timeout, not a harness limit. WoWAPITesting then found the harness kept every frame ever built alive (d77ed4f); on the current pin the whole suite runs in about 2.5 minutes. Release run: 826 passed, 0 failed.

Create Event times: noon stayed noon, and the Dibs line is plain text

  • Switching the Create Event window from 24-hour to 12-hour time moved a 12:30 PM event to 12:30 AM, a defect of mine. Blizzard's GameTime_ComputeStandardTime returns AM for hour 12 (GameTime_Shared.lua:62, Classic Era), and the format switch took both the hour and the AM/PM flag from it. The 12/24-hour conversion is now one shared function (it was in three places), used by the switch and by raid templates (ClassicCalendar.lua:4966, RaidTemplates.lua:174). Mutation-verified in raidtemplates_spec ("keeps 12:30 at noon").
  • The Dibs loot line in event descriptions is ASCII only (RaidTemplates.lua:119), the rule CalendarHelper already applies to text it writes, so a translated loot label can never put characters in a description that the import path strips.

Auto-Moderator: a name with no realm is your own realm, not any realm

Peer Review, thread 21985b78.

Measured by Peer Review on the operator's Old Blanchy client: a same-realm calendar invitee arrives as a bare name, and the client spells a realm two ways (GetRealmName "Old Blanchy", GetNormalizedRealmName "OldBlanchy").

  • Wrong player promoted, a defect of mine. AutoModerator.NamesMatch let a bare name match that name on ANY realm. In a connected group, a saved "Bob-Azuresong" would promote a different, bare "Bob" from the leader's own realm, and a bare "Bob" the leader typed matched every Bob. A missing realm now means the viewer's realm, on both sides. Mutation-verified.
  • One realm rule. GuildMemberSearch.NormRealm removes spaces AND apostrophes (the matcher used to drop only spaces, so "Quel'Thalas" never matched "QuelThalas"); the new GuildMemberSearch.OwnRealm() is the viewer's realm through it, used by the matcher and by RaidSignups.PlayerKey. The matcher deliberately does not go through PlayerKey: that resolves names through the rosters, which would turn a bare local "Bob" into a sister guild's "Bob-Azuresong".
  • Removing a bare name. Remove("Bob") takes the leader's own-realm Bob first, then the only stored Bob. With two other realms' Bobs it removes neither and the message names them and asks for the realm. The right-click menu used to print "removed" whether or not anything was.
  • Removing from the right-click menu could remove nothing. IsListed matched a saved bare "Bob" for an invitee shown as "Bob" plus your own realm, but Remove tried only the exact key, so the menu offered Remove and then printed "not on your list". Remove now takes out every entry IsListed would match. Specs cover the menu message both ways.
  • An unknown own realm no longer guesses (self-audit 9af0d144, D). While the client returns no realm name, a bare name matches no realm-qualified entry (the next invite-list update tries again), RaidSignups.PlayerKey gives a bare name no key instead of minting "Ana-", a one-player sync push it cannot key sends nothing instead of every row, and the open-event capture waits. Not seen in a client: whether the realm is ever unknown at those moments.
  • RaidSignups no longer wraps GuildMemberSearch.NormRealm in a local forwarder; it calls it directly.
  • A cross-realm invitee's spelling in the invite list is still unmeasured; nothing depends on it.

Closing the suggestion menu no longer reads a private library field

  • GuildMemberSearch.CloseMenuFor now calls LibAceGUIWidgets' public W:CloseMenuFor(anchor, ...) (MINOR 31, built for us on contract thread 4d698ce3) and no longer reads W._menuAnchor (v1.6.8). A library older than 31 gets the unconditional close. The "never closes somebody else's menu" examples in namesuggestions_spec pass against the installed MINOR 36.

WoW Forever support

Operator, 2026-09-27: players report no calendar on WoW Forever, and there is none in game. Forever's client carries Blizzard's calendar, but a server game rule turns it off: Blizzard_Minimap/Mainline/GameTime.lua:31 disables the clock button under Enum.GameRule.IngameCalendarDisabled, and /calendar returns early on the same rule. So Forever is the Era/TBC case, not the Wrath one.

  • New ClassicCalendar_Camelot.toc, Interface 16001, same file list as the other TOCs. It loads the embedded LibDataBroker/LibDBIcon instead of requiring the standalone, which does not list 16001 (TOGTools contract 2b570b7d, step 5), so Libs/LibDBIcon-1.0 is kept for it.
  • wow-version-replication.ps1 now also copies to _classic_beta_, the Forever install.
  • tocorder_spec accepts the _Camelot suffix, checks Forever's embedded load order and its 16xxx interface, and allows only those two library lines to differ between TOCs.
  • Needs CalendarHelper on Forever (a required dependency): delivered by CalendarHelper on thread fc85d148.
  • Creating an event raised "attempt to call a nil value" (ClassicCalendar.lua:5869) on Forever. SetDesaturation is a Blizzard global in Classic Era (Blizzard_UIParent/Shared/UIParent.lua:546) that Forever's client does not have. ClassicCalendar.lua now carries its own local copy, a wrapper for texture:SetDesaturated, which every client has. Found by the operator in the Forever client.
  • Four "Unable to load atlas entry UI-ClassicScrollBar-Center" warnings on Forever. The calendar's four scroll bars inherited WowClassicScrollBar, whose middle art Forever lacks. They now inherit ClassicCalendarScrollBar, defined per flavour: ScrollBarClassic.xml (Era/TBC, still WowClassicScrollBar) and ScrollBarForever.xml (MinimalScrollBar, what Forever's own calendar uses at the same four places). The seven specs that build the calendar XML load the Classic file; tocorder_spec pins each TOC defining the template before ClassicCalendar.xml.
  • The dark window borders' right edge sat 1-2px outside the corners on Forever (operator's screenshot; /fstack names CalendarViewHolidayFrameBorder). Blizzard's "Dialog" nine-slice layout is the same code in both clients, so the difference is Forever's atlas art. New Forever.lua, loaded by the Forever TOC only, re-applies a COPY of that layout with the right edge moved in on our seven dark-bordered windows. Classic Era and TBC never load it; Blizzard's own layout is not modified. UNVERIFIED: the first in-game try showed no visible change. RIGHT_EDGE_X (-1 UI unit) is a judgement from a screenshot, not a measurement; ClassicCalendar.ForeverBorderReport() prints the real edge and corner positions so it can be set from numbers, and is to be deleted once it is.
  • New Tests/forever_spec.lua (self-audit 9af0d144): builds the real calendar XML, removes the SetDesaturation global and opens Create Event's class buttons (mutation-verified: without the local copy it raises the exact Forever error at ClassicCalendar.lua:5876), and runs Forever.lua against the real windows -- a renamed window or a lost Dialog border turns it red instead of silently skipping.
  • Tested by the operator in the Forever client (2026-09-27): the addon loads, the calendar opens, Create Event works after the SetDesaturation fix, and the scroll bar warnings are gone. Still open there: the border edge above, and scrolling a long description or the pickers with the new scroll bar.

Addon list title in the fleet's brand colour

  • Both TOCs' ## Title is now |cffFF8000Classic Calendar - Revived|r, the orange every one of our addons uses (DeltaSync thread dd94f8d6). No version tag and no ## Category: this is not a library.

LibDBIcon-1.0 is a required dependency, not an embedded copy

TOGTools contract 2b570b7d.

An embedded LibDBIcon only updates for players when this addon is re-released; as a CurseForge required dependency it updates for everyone on its own.

  • Both TOCs list LibDBIcon-1.0 in ## Dependencies and no longer load Libs/LibDBIcon-1.0; .pkgmeta lists libdbicon-1-0 under required-dependencies. The standalone addon brings LibDataBroker-1.1 with it. No Lua changed: LibStub("LibDBIcon-1.0") resolves the same.
  • The Era and TBC TOCs no longer load the embedded Libs/LibDBIcon-1.0. The folder stays: the WoW Forever TOC (below) still loads it, as the contract's step 5 directs.
  • Known cost: a player who installs by hand must also install LibDBIcon-1.0.
  • minimapbutton_spec loads the installed standalone library first, as the client does for a dependency. LibDBIcon captures Minimap when its file loads, so the spec brings up the harness's frame layer before the first load.
  • The README and CurseForge page list LibDBIcon-1.0 with the other required libraries.

The "Hide the calendar minimap button" option came undone at every login

Reported by a player on Discord, 2026-09-22: "Every time I login the minimap button is there, and I have to deselect and reselect the option to get it hidden again." Two defects of mine, and they compounded.

  • The option was applied before the button existed. CCOptionsHandler called LibDBIcon:Hide at ADDON_LOADED; the button is registered at PLAYER_LOGIN (ClassicCalendar.lua), so the Hide found nothing. LibDBIcon's Register then shows the button unless db.hide is set (LibDBIcon-1.0.lua:321-327), and its Hide never records anything (:409-414) -- so nothing ever set it, and the button came back every login.
  • The LibDBIcon db was never saved. It was local ClassicCalendarDB = ClassicCalendarDB or {} at file scope, which runs before the client restores saved variables, so it bound a fresh table every session. The global ClassicCalendarDB (declared in both TOCs) was never assigned by this addon at all -- which also means the button's dragged position has never survived a login.
  • Fix: one function, ClassicCalendar.SetMinimapButtonHidden(hidden), records the choice in CCConfig AND in ClassicCalendarDB.minimap.hide (the saved variable, created at PLAYER_LOGIN), then hides or shows the button. The login path copies CCConfig.HideMinimapButton into the db before Register, so the button is created hidden. The options check box calls the same function; the dead ADDON_LOADED hide is removed.
  • New Tests/minimapbutton_spec.lua, 5 examples, on the whole-addon load with the real LibDBIcon (now the standalone dependency, see above) and a real PLAYER_LOGIN broadcast (so LibDBIcon's login frame and ours run in their real order): hidden stays hidden at login, shown when off, the db IS the saved variable, a choice carried over through the saved variables holds on the next login, and a dragged position survives. Mutation-verified: removing the pre-Register db.hide line turns the reported case red. Suite 713.

Older versions (v0.7.0beta to v1.6.8) are in CHANGELOG_ARCHIVE.md.

This mod has no additional files