updateday 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -2
  3. package/assets/hourglass/10m.png +0 -0
  4. package/assets/hourglass/12h.png +0 -0
  5. package/assets/hourglass/15m.png +0 -0
  6. package/assets/hourglass/1h.png +0 -0
  7. package/assets/hourglass/24h.png +0 -0
  8. package/assets/hourglass/2h.png +0 -0
  9. package/assets/hourglass/30m.png +0 -0
  10. package/assets/hourglass/3h.png +0 -0
  11. package/assets/hourglass/45m.png +0 -0
  12. package/assets/hourglass/4h.png +0 -0
  13. package/assets/hourglass/5h.png +0 -0
  14. package/assets/hourglass/5m.png +0 -0
  15. package/assets/hourglass/6h.png +0 -0
  16. package/assets/hourglass/default.png +0 -0
  17. package/assets/hourglass/now.png +0 -0
  18. package/assets/hourglass-styles/blocky.png +0 -0
  19. package/assets/hourglass-styles/classic.png +0 -0
  20. package/assets/hourglass-styles/glow.png +0 -0
  21. package/assets/hourglass-styles/outline.png +0 -0
  22. package/assets/hourglass-styles/pixel.png +0 -0
  23. package/assets/hourglass-styles/rounded.png +0 -0
  24. package/bin/updateday.js +5 -0
  25. package/dist/bundle.js +54 -0
  26. package/dist/cli.js +254 -0
  27. package/dist/config.js +261 -0
  28. package/dist/doctor.js +119 -0
  29. package/dist/experiments.js +349 -0
  30. package/dist/gamePage.js +143 -0
  31. package/dist/gifts.js +55 -0
  32. package/dist/history.js +68 -0
  33. package/dist/icons.js +216 -0
  34. package/dist/labels.js +64 -0
  35. package/dist/mcp.js +302 -0
  36. package/dist/moderation.js +86 -0
  37. package/dist/notify.js +72 -0
  38. package/dist/openCloud.js +211 -0
  39. package/dist/playerData.js +109 -0
  40. package/dist/preflight.js +202 -0
  41. package/dist/runner.js +360 -0
  42. package/dist/schedule.js +162 -0
  43. package/dist/server.js +929 -0
  44. package/dist/state.js +29 -0
  45. package/dist/swap.js +196 -0
  46. package/dist/zip.js +82 -0
  47. package/docs/AGENTS.md +123 -0
  48. package/docs/COVERAGE.md +50 -0
  49. package/docs/GUIDE.md +288 -0
  50. package/docs/hourglass-styles.png +0 -0
  51. package/docs/screenshot.png +0 -0
  52. package/example/multi-game.json +16 -0
  53. package/example/updateday.json +37 -0
  54. package/package.json +63 -5
  55. package/roblox/UpdateDayBanner.client.luau +159 -0
  56. package/roblox/UpdateDayCountdown.server.luau +48 -0
  57. package/roblox/UpdateDayGifts.luau +119 -0
  58. package/roblox/UpdateDayListener.luau +43 -0
  59. package/templates/github-actions/updateday.yml +35 -0
  60. package/ui/core.js +325 -0
  61. package/ui/countdown.js +775 -0
  62. package/ui/events.js +249 -0
  63. package/ui/gifts.js +215 -0
  64. package/ui/history.js +156 -0
  65. package/ui/index.html +530 -0
  66. package/ui/jsonEditor.js +417 -0
  67. package/ui/page.js +94 -0
  68. package/ui/players.js +256 -0
  69. package/ui/run.js +391 -0
  70. package/ui/sale.js +366 -0
  71. package/ui/settings.js +414 -0
  72. package/ui/shell.js +193 -0
package/docs/GUIDE.md ADDED
@@ -0,0 +1,288 @@
1
+ # UpdateDay guide
2
+
3
+ The full reference. New here? Start with the [README](../README.md).
4
+
5
+ Free, open-source update-day automation for Roblox games. You give it an Open
6
+ Cloud API key and an update time. It runs the countdown icons, sale, game page
7
+ text, event, place publish and server restarts on schedule.
8
+
9
+ - **Your key stays with you.** It's read from an environment variable and only
10
+ ever sent to `apis.roblox.com`. There's no UpdateDay server.
11
+ - **No cookies, no logins.** Everything goes through official Open Cloud
12
+ endpoints. See [docs/COVERAGE.md](COVERAGE.md).
13
+ - **Safe sales.** Original prices are written to a state file *before* anything
14
+ is discounted, every write is read back, and `restore` puts everything back
15
+ at any time.
16
+ - **Runs anywhere.** Use your own PC (`watch`), or a free GitHub Actions cron
17
+ so it keeps going with your PC off.
18
+ - **Nothing is forced.** Every feature is optional and off until you turn it on.
19
+
20
+ ## What it does
21
+
22
+ | Step | When |
23
+ |---|---|
24
+ | Creates a Roblox event for the update | Right away |
25
+ | Saves your current game icon | Right away |
26
+ | Countdown icons, built automatically from your icon + hourglass + time badge | At each mark (24h … 5m, NOW) |
27
+ | Game name / description changes | Any offset, e.g. `24h`, `0`, `+7d` |
28
+ | In-game warnings via MessagingService | e.g. 10m, 5m, 1m before |
29
+ | Publishes your place file | At update time |
30
+ | Restarts old servers, with optional bleed-off | At update time |
31
+ | Sale: X% off passes and products | At update time, for N hours |
32
+ | Original icon back, or your new update image if `newIcon` is set | After the admin-abuse window |
33
+
34
+ If it starts late, it catches up. Stale countdown icons and late warnings are
35
+ skipped, not spammed.
36
+
37
+ ## The setup page
38
+
39
+ ```bash
40
+ npx updateday ui updateday.json
41
+ ```
42
+
43
+ Opens a page on `http://127.0.0.1:4747` (only reachable from your own PC), with light and dark themes and a phone layout:
44
+
45
+ - **Countdown**: a live preview of the icon players see, every stage (click to switch one off), the full schedule, the icon look, your own hourglass art, and a .zip of every icon
46
+ - **Sale**: discount, when it runs (or the admin-abuse hour in one click), name tags like `[30% OFF]`, and which passes and products
47
+ - **Events**: create, edit and delete Roblox events, or have one made for every update
48
+ - **Game page**: your page as players see it, with a checklist before the update
49
+ - **Player data**: look up a player, edit their save as fields or JSON, with a backup before every change
50
+ - **Gifts**: send a player something through your game's gift box
51
+ - **History**: everything UpdateDay did, searchable, with a CSV download
52
+ - **Settings**: games, update time and repeats, alerts, server restarts and in-game warnings
53
+
54
+ The bar at the bottom shows what happens next, with **Check plan**, **Start**, **Stop**, **Dry run** and **Undo all**.
55
+
56
+ Everything it saves is the same `updateday.json` the commands below use, so you can switch
57
+ between the page and the terminal freely. Keys pasted into the page stay in memory and
58
+ are never written to disk.
59
+
60
+ ## Quick start
61
+
62
+ 1. Install Node.js 22.18 or newer. (From a clone of this repo, use `node src/cli.ts` wherever this says `npx updateday`.)
63
+ 2. Create an API key in Creator Hub → **Open Cloud → API Keys**, add your
64
+ experience, and give it the scopes for the features you use:
65
+ `legacy-universe:manage`, `game-pass:read/write`,
66
+ `developer-product:read/write`, `universe.event:write`,
67
+ `universe-messaging-service:publish`, `universe:write`,
68
+ `universe-places:write`.
69
+ 3. Copy `example/updateday.json` and edit it. The minimum is `universeId` and
70
+ `updateAt`. You don't need any images: run `preview` to see the countdown
71
+ icons it will build from your live game icon.
72
+ 4. Run:
73
+
74
+ ```bash
75
+ export UPDATEDAY_API_KEY=your-key
76
+ npx updateday plan updateday.json
77
+ npx updateday preview updateday.json
78
+ npx updateday check updateday.json
79
+ npx updateday watch updateday.json
80
+ ```
81
+
82
+ Use `tick --dry-run` to see what would run right now without touching anything.
83
+
84
+ ### Countdown icon options
85
+
86
+ All optional, under `countdown` in the config:
87
+
88
+ | Key | Default | What it does |
89
+ |---|---|---|
90
+ | `marks` | `24h 12h 6h 5h 4h 3h 2h 1h 45m 30m 15m 10m 5m` | When icons change (a NOW icon is always added) |
91
+ | `style` | `original` | `original` (the hand-drawn set with the time drawn in), or a text-free style: `classic`, `pixel`, `rounded`, `glow`, `blocky`, `outline`. Styles draw the time text in code |
92
+ | `hourglassSize`, `hourglassY` | `0.66`, `0.4` | With a style: hourglass size, and the height of its centre (0 = top, 1 = bottom) |
93
+ | `textSize`, `textY` | `0.15`, `0.85` | With a style: text size, and the height of its centre |
94
+ | `labels` | none | Your own text for any stage, e.g. `{ "6h": "6 HRS!", "now": "LIVE!" }` |
95
+ | `anchor` | `center` | Where the hourglass goes: `top-right`, `top-left`, `bottom-right`, `bottom-left`, `center`, or `full` for art drawn to cover the whole icon |
96
+ | `scale` | `0.88` | Hourglass size as a fraction of the icon |
97
+ | `dim` | `0.45` | Opacity of the dark layer over the game icon, `0` for none |
98
+ | `overlayDir` | bundled hourglasses | Your own art: `24h.png`, `5m.png`, … (with the time text drawn in), plus an optional `default.png` without text |
99
+ | `label` | `true` | For marks with no art of their own, draw the time text under `default.png` |
100
+ | `labelColor`, `labelOutline` | white, black | Colours of that drawn text |
101
+ | `customDir` | none | Fully hand-made icons per mark (`24h.png` …), used instead of generating |
102
+ | `skip` | `[]` (every stage on) | Stages to leave out, e.g. `["5h", "4h", "10m"]`; `"now"` works too |
103
+ | `newIcon` | none | Your new update image; it replaces the original at the end |
104
+ | `newIconAt` | `end` | `now` shows the NOW hourglass over the new image at update time, then the clean new image after the admin-abuse hour |
105
+
106
+ The bundled set covers every default mark plus NOW. Any other mark, such as
107
+ `10m`, uses the plain hourglass with the time drawn underneath in the same style.
108
+
109
+ Set `"countdown": false` to leave the icon alone.
110
+
111
+ #### How the icon changes on Roblox
112
+
113
+ Roblox doesn't let API keys change a game's main icon, so there are two modes:
114
+
115
+ - **`"iconMethod": "reminder"`** (default): at each stage UpdateDay saves the finished icon to
116
+ `updateday-icons/` and the setup page shows it with a Download button, so you can upload it in Creator Hub.
117
+ - **`"iconMethod": "languageSwap"`**: fully automatic. UpdateDay briefly makes another language the
118
+ game's source language (the least-used one, or `swapLanguage`), then sets the countdown as every other
119
+ language's icon. Each stage goes up `uploadLeadSeconds` early (default 90) because Roblox reviews
120
+ every icon upload, which takes about 30 seconds. At the end it removes the countdown icons, puts back any
121
+ translated icons you already had, and restores the source language. Players whose Roblox language is the
122
+ temporary one see your normal icon. A *new* update icon still has to be uploaded in Creator Hub at the end;
123
+ you get a reminder.
124
+
125
+ In swap mode every language gets its own label ("24 STUNDEN", "24 ЧАСА", "24時間" and so on), with
126
+ correct plurals. English keeps the hand-drawn art. Set `"localizeLabels": false` to use English everywhere.
127
+ Run `doctor` (or **Check key** in the page) first: it lists any permission the key is missing.
128
+
129
+ #### Picking stages
130
+
131
+ Every stage is on by default. List the ones you don't want in `skip`, and the
132
+ rest of the schedule stays the same. If you skip `now`, the final icon goes up
133
+ right at update time instead of waiting out the admin-abuse hour.
134
+
135
+ #### A new icon for the update
136
+
137
+ ```json
138
+ "countdown": {
139
+ "newIcon": "update-5-icon.png",
140
+ "newIconAt": "now"
141
+ }
142
+ ```
143
+
144
+ With `newIconAt: "end"`, the default, the countdown runs on your old icon and the
145
+ new one replaces it after the admin-abuse hour. With `"now"`, the NOW hourglass
146
+ appears on top of the new image the moment the update goes live, so players see
147
+ both at once, and the clean new image follows after the admin-abuse hour.
148
+
149
+ ### Title countdown
150
+
151
+ ```json
152
+ "title": { "format": "[{time}] {name}", "nowFormat": "[LIVE] {name}", "afterFormat": "{name}" }
153
+ ```
154
+
155
+ Puts the time left in the game's title at every stage: "[6H] Your Game", "[45M] Your Game", then "[LIVE] Your Game"
156
+ at update time. After the admin-abuse hour, `afterFormat` is used: `{name}` puts the original title back, or name the
157
+ update, e.g. `"{name} [UPDATE 5]"`. `{time}` becomes `6H`, `45M` and so on, and `{name}` is the title the game had
158
+ when the run started. The real title is saved first, **Undo all** puts it back, and titles are kept to 50 characters.
159
+ It follows the countdown's stages (or its own `marks`) and works with the icon countdown switched off too.
160
+ Needs the `universe.place:write` permission.
161
+
162
+ The countdown title shows to players whose Roblox language is your game's main (source) language. Players in other
163
+ languages keep seeing Roblox's translation of your normal title, which isn't touched and is never left stale.
164
+
165
+ ### Moderation check
166
+
167
+ **Countdown → Stages → Check with moderation** uploads each stage's icon to your Roblox inventory as a private image,
168
+ so Roblox reviews it days before the update instead of at the moment it goes up. Each stage then shows Approved,
169
+ In review or Rejected, and **Check plan** flags a rejected icon. Only icons that changed since the last check are sent
170
+ again. Needs the `asset:read` and `asset:write` permissions.
171
+
172
+ ### Your own hourglass art
173
+
174
+ In the page: **Countdown → Hourglass art → My own art**, then click any stage to upload its art.
175
+ Stages you don't upload use your default art with the time drawn on it, or the built-in hourglass.
176
+ In the config this is `countdown.overlayDir` (see the table above).
177
+
178
+ ### Check before you go
179
+
180
+ `npx updateday check` (or **Check plan** in the page) runs a pre-flight: the key's permissions, the update
181
+ time, missing files, every countdown icon rendering, which items will go on sale, the event's timing and
182
+ your alerts. **Start** runs the same check and tells you if something needs fixing, but you can start anyway.
183
+
184
+ ### Repeating updates
185
+
186
+ ```json
187
+ "repeat": { "every": "week", "until": "2027-01-01T00:00:00Z" }
188
+ ```
189
+
190
+ `every` is `day`, `week`, `2weeks` or `month`. Once an update day is over (countdown, admin-abuse hour, sale
191
+ and event all done), the plan moves to the next one by itself, and fixed sale times move with it. `watch`
192
+ and the page keep running between them.
193
+
194
+ ### Alerts
195
+
196
+ ```json
197
+ "notify": { "events": ["reminders", "failures", "live", "finished"], "mention": "@here" }
198
+ ```
199
+
200
+ Posts to a Discord channel through a webhook: when an icon needs uploading (with the icon attached), when
201
+ something fails, when the update goes live and when everything is done. Add `"steps"` for every single step.
202
+ The webhook link is read from `UPDATEDAY_DISCORD_WEBHOOK` (or `notify.discordWebhookEnv`), or pasted into the
203
+ page for the session. Like the key, it's never written to disk. The page can also show desktop alerts while it's open.
204
+
205
+ ### History
206
+
207
+ Everything UpdateDay does (each step, every price it changes and puts back, reminders and problems) is
208
+ appended to `updateday-history.jsonl` next to your plan. The **History** tab shows it.
209
+
210
+ ### AI agents (MCP)
211
+
212
+ `npx updateday mcp updateday.json` runs UpdateDay as an MCP server, so Claude, Cursor and other agents can read
213
+ and change the plan, run the pre-flight check, preview icons, list sale items and read the history. Anything that
214
+ changes the live game (`run_due_steps`, `restore_everything`) needs `confirm: true`, and runs default to a dry run.
215
+
216
+ ```json
217
+ {
218
+ "mcpServers": {
219
+ "updateday": {
220
+ "command": "npx",
221
+ "args": ["updateday", "mcp", "/path/to/updateday.json"],
222
+ "env": { "UPDATEDAY_API_KEY": "your-key" }
223
+ }
224
+ }
225
+ }
226
+ ```
227
+
228
+ ### Several games at once
229
+
230
+ Put shared settings at the top and list the games under `games`. Each game can
231
+ override anything: its own `updateAt`, `"sale": false`, a different
232
+ `countdown` look, and so on. Sections like `countdown` and `sale` merge, so a game only
233
+ lists what's different. Games owned by a different user or group can name their
234
+ own key variable with `apiKeyEnv`. See `example/multi-game.json`.
235
+
236
+ Every command runs for all games; add `--game "Main Game"` (or the universe ID)
237
+ to pick one. Each game keeps its own state file, `updateday.<universeId>.state.json`,
238
+ so a problem in one game never blocks the others.
239
+
240
+ ### With your PC off (GitHub Actions)
241
+
242
+ Put `updateday.json` and your icons in a private repo, add the key as the
243
+ repository secret `UPDATEDAY_API_KEY` (and `UPDATEDAY_DISCORD_WEBHOOK` for alerts), and copy
244
+ `templates/github-actions/updateday.yml` into `.github/workflows/`. It ticks
245
+ every 5 minutes and commits its progress file back. GitHub cron can run a few
246
+ minutes late.
247
+
248
+ ### In-game countdown
249
+
250
+ Turn on **Warn players in live servers**, then download the Roblox kit (**Settings → Download kit**, or the
251
+ `roblox/` folder):
252
+
253
+ | File | Goes in | What it does |
254
+ |---|---|---|
255
+ | `UpdateDayListener.luau` | ServerScriptService (ModuleScript) | Receives UpdateDay's warnings |
256
+ | `UpdateDayCountdown.server.luau` | ServerScriptService (Script) | Shares the update time with every player |
257
+ | `UpdateDayBanner.client.luau` | StarterPlayerScripts (LocalScript) | An "UPDATE IN 04:59" banner players can close, warning toasts, and "THE UPDATE IS LIVE!" |
258
+ | `UpdateDayGifts.luau` | ServerScriptService (ModuleScript) | Optional: the gift box behind the Gifts tab |
259
+
260
+ Colours, text, and how early the banner shows are constants at the top of each file. To use the banner
261
+ without warnings, set `MANUAL_UPDATE_AT` in `UpdateDayCountdown.server.luau`.
262
+
263
+ Or use the listener directly:
264
+
265
+ ```lua
266
+ local UpdateDayListener = require(path.to.UpdateDayListener)
267
+
268
+ UpdateDayListener.OnWarning(function(updateUnix, mark)
269
+ -- show "Update in 5 minutes!"
270
+ end)
271
+ UpdateDayListener.Start()
272
+ ```
273
+
274
+ ## Tests
275
+
276
+ ```bash
277
+ npm test # unit tests, a full update day against a fake Roblox with rate limits,
278
+ # server errors and a crash mid-sale, edge cases and the local server's security
279
+ npm run test:ui # every tab at desktop and phone width in both themes (needs Chrome; set CHROME_PATH)
280
+ ```
281
+
282
+ ## Status
283
+
284
+ Tested live on a real game: sales with exact restore, name tags, events, warnings, server restarts, name
285
+ changes, the key check and the fully automatic language-swap countdown. Player data and gifts are tested
286
+ against a fake Roblox but still need a live run.
287
+
288
+ MIT licensed.
Binary file
Binary file
@@ -0,0 +1,16 @@
1
+ {
2
+ "updateAt": "2026-11-07T17:00:00Z",
3
+ "adminAbuseMinutes": 60,
4
+ "sale": {
5
+ "percentOff": 30,
6
+ "durationHours": 48,
7
+ "gamePasses": "all",
8
+ "developerProducts": "all"
9
+ },
10
+ "restartServers": { "bleedOffMinutes": 5 },
11
+ "games": [
12
+ { "name": "Main Game", "universeId": 1234567890 },
13
+ { "name": "Spin-off", "universeId": 2345678901, "updateAt": "2026-11-08T17:00:00Z", "sale": false },
14
+ { "name": "Group Game", "universeId": 3456789012, "apiKeyEnv": "UPDATEDAY_GROUP_KEY", "countdown": { "dim": 0.6 } }
15
+ ]
16
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "universeId": 1234567890,
3
+ "name": "My Game",
4
+ "updateAt": "2026-11-07T17:00:00Z",
5
+ "adminAbuseMinutes": 60,
6
+ "countdown": {
7
+ "style": "classic",
8
+ "skip": ["5h", "4h"]
9
+ },
10
+ "title": {
11
+ "afterFormat": "{name} [UPDATE 5]"
12
+ },
13
+ "sale": {
14
+ "percentOff": 30,
15
+ "durationHours": 48,
16
+ "gamePasses": "all",
17
+ "developerProducts": "all",
18
+ "nameTag": "[{percent}% OFF]"
19
+ },
20
+ "event": {
21
+ "title": "Update 5",
22
+ "subtitle": "New world, new pets",
23
+ "description": "Join for the admin abuse hour!",
24
+ "durationHours": 2,
25
+ "visibility": "Public"
26
+ },
27
+ "broadcast": {
28
+ "topic": "UpdateDay",
29
+ "marks": ["10m", "5m", "1m"]
30
+ },
31
+ "restartServers": {
32
+ "bleedOffMinutes": 5
33
+ },
34
+ "notify": {
35
+ "events": ["reminders", "failures", "live", "finished"]
36
+ }
37
+ }
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
- "name": "updateday",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "updateday",
3
+ "version": "0.2.0",
4
+ "description": "Free, open-source update-day automation for Roblox games: countdown icons and titles, sales, events, in-game warnings and server restarts, run with your Open Cloud API key.",
5
+ "keywords": [
6
+ "roblox",
7
+ "open-cloud",
8
+ "update",
9
+ "countdown",
10
+ "game-icon",
11
+ "sale",
12
+ "mcp",
13
+ "automation"
14
+ ],
15
+ "author": "hiyouwhatsapp",
16
+ "license": "MIT",
17
+ "homepage": "https://github.com/hiyouwhatsapp/UpdateDay#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/hiyouwhatsapp/UpdateDay.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/hiyouwhatsapp/UpdateDay/issues"
24
+ },
25
+ "type": "module",
26
+ "bin": {
27
+ "updateday": "bin/updateday.js"
28
+ },
29
+ "files": [
30
+ "bin",
31
+ "dist",
32
+ "ui",
33
+ "assets",
34
+ "roblox",
35
+ "templates",
36
+ "example",
37
+ "docs",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "engines": {
42
+ "node": ">=22.18"
43
+ },
44
+ "scripts": {
45
+ "ui": "node src/cli.ts ui",
46
+ "plan": "node src/cli.ts plan",
47
+ "tick": "node src/cli.ts tick",
48
+ "watch": "node src/cli.ts watch",
49
+ "typecheck": "tsc && tsc -p test",
50
+ "test": "node --test \"test/*.test.ts\"",
51
+ "test:ui": "node scripts/ui-smoke.mjs",
52
+ "check": "node src/cli.ts check",
53
+ "mcp": "node src/cli.ts mcp",
54
+ "build": "tsc -p tsconfig.build.json",
55
+ "prepare": "npm run build"
56
+ },
57
+ "dependencies": {
58
+ "sharp": "^0.34.3"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^22.15.0",
62
+ "typescript": "^5.8.3"
63
+ }
64
+ }
@@ -0,0 +1,159 @@
1
+ local Players = game:GetService("Players")
2
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
3
+ local TweenService = game:GetService("TweenService")
4
+
5
+ local TIME_ATTRIBUTE = "UpdateDayAt"
6
+ local WARNING_EVENT_NAME = "UpdateDayWarning"
7
+ local SHOW_WITHIN_MINUTES = 60
8
+ local LIVE_MESSAGE_SECONDS = 20
9
+ local TOAST_SECONDS = 6
10
+ local REFRESH_SECONDS = 0.25
11
+ local COUNTDOWN_TEXT = "UPDATE IN %s"
12
+ local LIVE_TEXT = "THE UPDATE IS LIVE! Rejoin to play it"
13
+ local TOAST_TEXT = "Update in %s! Finish what you're doing"
14
+ local BACKGROUND = Color3.fromRGB(16, 18, 24)
15
+ local ACCENT = Color3.fromRGB(255, 196, 61)
16
+ local TEXT_COLOR = Color3.fromRGB(255, 255, 255)
17
+ local FONT = Enum.Font.GothamBlack
18
+ local BANNER_SIZE = UDim2.fromOffset(300, 46)
19
+ local BANNER_POSITION = UDim2.new(0.5, 0, 0, 12)
20
+ local TOAST_POSITION = UDim2.new(0.5, 0, 0, 66)
21
+ local TWEEN = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
22
+
23
+ local player = Players.LocalPlayer
24
+ local dismissed = false
25
+
26
+ local function corner(parent, radius)
27
+ local item = Instance.new("UICorner")
28
+ item.CornerRadius = UDim.new(0, radius)
29
+ item.Parent = parent
30
+ end
31
+
32
+ local function stroke(parent)
33
+ local item = Instance.new("UIStroke")
34
+ item.Color = ACCENT
35
+ item.Thickness = 2
36
+ item.Parent = parent
37
+ end
38
+
39
+ local function label(parent, size, position)
40
+ local text = Instance.new("TextLabel")
41
+ text.BackgroundTransparency = 1
42
+ text.Size = size
43
+ text.Position = position
44
+ text.Font = FONT
45
+ text.TextColor3 = TEXT_COLOR
46
+ text.TextScaled = true
47
+ text.Parent = parent
48
+ local limit = Instance.new("UITextSizeConstraint")
49
+ limit.MaxTextSize = 22
50
+ limit.Parent = text
51
+ return text
52
+ end
53
+
54
+ local function pill(gui, name, position)
55
+ local frame = Instance.new("Frame")
56
+ frame.Name = name
57
+ frame.AnchorPoint = Vector2.new(0.5, 0)
58
+ frame.Size = BANNER_SIZE
59
+ frame.Position = position
60
+ frame.BackgroundColor3 = BACKGROUND
61
+ frame.BackgroundTransparency = 0.1
62
+ frame.Visible = false
63
+ frame.Parent = gui
64
+ corner(frame, 12)
65
+ stroke(frame)
66
+ return frame
67
+ end
68
+
69
+ local function buildGui()
70
+ local gui = Instance.new("ScreenGui")
71
+ gui.Name = "UpdateDayBanner"
72
+ gui.ResetOnSpawn = false
73
+ gui.IgnoreGuiInset = false
74
+ gui.DisplayOrder = 50
75
+ gui.Parent = player:WaitForChild("PlayerGui")
76
+
77
+ local banner = pill(gui, "Banner", BANNER_POSITION)
78
+ local bannerText = label(banner, UDim2.new(1, -52, 1, -12), UDim2.fromOffset(12, 6))
79
+ local close = Instance.new("TextButton")
80
+ close.Size = UDim2.fromOffset(28, 28)
81
+ close.Position = UDim2.new(1, -36, 0.5, -14)
82
+ close.BackgroundTransparency = 1
83
+ close.Text = "X"
84
+ close.Font = FONT
85
+ close.TextColor3 = ACCENT
86
+ close.TextSize = 16
87
+ close.Parent = banner
88
+ close.Activated:Connect(function()
89
+ dismissed = true
90
+ banner.Visible = false
91
+ end)
92
+
93
+ local toast = pill(gui, "Toast", TOAST_POSITION)
94
+ toast.Size = UDim2.fromOffset(380, 46)
95
+ local toastText = label(toast, UDim2.new(1, -24, 1, -12), UDim2.fromOffset(12, 6))
96
+
97
+ return banner, bannerText, toast, toastText
98
+ end
99
+
100
+ local function clock(seconds)
101
+ local total = math.max(0, math.floor(seconds))
102
+ local hours = total // 3600
103
+ local minutes = (total % 3600) // 60
104
+ local secs = total % 60
105
+ if hours > 0 then
106
+ return string.format("%d:%02d:%02d", hours, minutes, secs)
107
+ end
108
+ return string.format("%02d:%02d", minutes, secs)
109
+ end
110
+
111
+ local function markText(mark)
112
+ local count, unit = string.match(tostring(mark), "^(%d+)([smhd])$")
113
+ if not count then
114
+ return tostring(mark)
115
+ end
116
+ local words = { s = "second", m = "minute", h = "hour", d = "day" }
117
+ local n = tonumber(count)
118
+ return string.format("%d %s%s", n, words[unit], n == 1 and "" or "s")
119
+ end
120
+
121
+ local banner, bannerText, toast, toastText = buildGui()
122
+
123
+ local function showToast(text)
124
+ toastText.Text = text
125
+ toast.Visible = true
126
+ toast.BackgroundTransparency = 1
127
+ TweenService:Create(toast, TWEEN, { BackgroundTransparency = 0.1 }):Play()
128
+ task.delay(TOAST_SECONDS, function()
129
+ if toastText.Text == text then
130
+ toast.Visible = false
131
+ end
132
+ end)
133
+ end
134
+
135
+ local warningEvent = ReplicatedStorage:WaitForChild(WARNING_EVENT_NAME)
136
+ warningEvent.OnClientEvent:Connect(function(_, mark)
137
+ showToast(string.format(TOAST_TEXT, markText(mark)))
138
+ end)
139
+
140
+ ReplicatedStorage:GetAttributeChangedSignal(TIME_ATTRIBUTE):Connect(function()
141
+ dismissed = false
142
+ end)
143
+
144
+ while true do
145
+ local updateAt = ReplicatedStorage:GetAttribute(TIME_ATTRIBUTE)
146
+ local visible = false
147
+ if typeof(updateAt) == "number" and not dismissed then
148
+ local left = updateAt - workspace:GetServerTimeNow()
149
+ if left > 0 and left <= SHOW_WITHIN_MINUTES * 60 then
150
+ bannerText.Text = string.format(COUNTDOWN_TEXT, clock(left))
151
+ visible = true
152
+ elseif left <= 0 and left > -LIVE_MESSAGE_SECONDS then
153
+ bannerText.Text = LIVE_TEXT
154
+ visible = true
155
+ end
156
+ end
157
+ banner.Visible = visible
158
+ task.wait(REFRESH_SECONDS)
159
+ end
@@ -0,0 +1,48 @@
1
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
2
+ local ServerScriptService = game:GetService("ServerScriptService")
3
+
4
+ local TOPIC = "UpdateDay"
5
+ local MANUAL_UPDATE_AT = ""
6
+ local TIME_ATTRIBUTE = "UpdateDayAt"
7
+ local WARNING_EVENT_NAME = "UpdateDayWarning"
8
+
9
+ local UpdateDayListener = require(ServerScriptService:WaitForChild("UpdateDayListener"))
10
+
11
+ local function warningEvent()
12
+ local existing = ReplicatedStorage:FindFirstChild(WARNING_EVENT_NAME)
13
+ if existing then
14
+ return existing
15
+ end
16
+ local event = Instance.new("RemoteEvent")
17
+ event.Name = WARNING_EVENT_NAME
18
+ event.Parent = ReplicatedStorage
19
+ return event
20
+ end
21
+
22
+ local function setUpdateTime(unixSeconds)
23
+ if typeof(unixSeconds) == "number" and unixSeconds > 0 then
24
+ ReplicatedStorage:SetAttribute(TIME_ATTRIBUTE, unixSeconds)
25
+ end
26
+ end
27
+
28
+ local function applyManualTime()
29
+ if MANUAL_UPDATE_AT == "" then
30
+ return
31
+ end
32
+ local parsed = DateTime.fromIsoDate(MANUAL_UPDATE_AT)
33
+ if parsed then
34
+ setUpdateTime(parsed.UnixTimestamp)
35
+ else
36
+ warn("UpdateDayCountdown: MANUAL_UPDATE_AT isn't an ISO date like 2026-11-01T17:00:00Z")
37
+ end
38
+ end
39
+
40
+ local event = warningEvent()
41
+ applyManualTime()
42
+
43
+ UpdateDayListener.OnWarning(function(unixSeconds, mark)
44
+ setUpdateTime(unixSeconds)
45
+ event:FireAllClients(unixSeconds, mark)
46
+ end)
47
+
48
+ UpdateDayListener.Start(TOPIC)