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.
- package/LICENSE +21 -0
- package/README.md +109 -2
- package/assets/hourglass/10m.png +0 -0
- package/assets/hourglass/12h.png +0 -0
- package/assets/hourglass/15m.png +0 -0
- package/assets/hourglass/1h.png +0 -0
- package/assets/hourglass/24h.png +0 -0
- package/assets/hourglass/2h.png +0 -0
- package/assets/hourglass/30m.png +0 -0
- package/assets/hourglass/3h.png +0 -0
- package/assets/hourglass/45m.png +0 -0
- package/assets/hourglass/4h.png +0 -0
- package/assets/hourglass/5h.png +0 -0
- package/assets/hourglass/5m.png +0 -0
- package/assets/hourglass/6h.png +0 -0
- package/assets/hourglass/default.png +0 -0
- package/assets/hourglass/now.png +0 -0
- package/assets/hourglass-styles/blocky.png +0 -0
- package/assets/hourglass-styles/classic.png +0 -0
- package/assets/hourglass-styles/glow.png +0 -0
- package/assets/hourglass-styles/outline.png +0 -0
- package/assets/hourglass-styles/pixel.png +0 -0
- package/assets/hourglass-styles/rounded.png +0 -0
- package/bin/updateday.js +5 -0
- package/dist/bundle.js +54 -0
- package/dist/cli.js +254 -0
- package/dist/config.js +261 -0
- package/dist/doctor.js +119 -0
- package/dist/experiments.js +349 -0
- package/dist/gamePage.js +143 -0
- package/dist/gifts.js +55 -0
- package/dist/history.js +68 -0
- package/dist/icons.js +216 -0
- package/dist/labels.js +64 -0
- package/dist/mcp.js +302 -0
- package/dist/moderation.js +86 -0
- package/dist/notify.js +72 -0
- package/dist/openCloud.js +211 -0
- package/dist/playerData.js +109 -0
- package/dist/preflight.js +202 -0
- package/dist/runner.js +360 -0
- package/dist/schedule.js +162 -0
- package/dist/server.js +929 -0
- package/dist/state.js +29 -0
- package/dist/swap.js +196 -0
- package/dist/zip.js +82 -0
- package/docs/AGENTS.md +123 -0
- package/docs/COVERAGE.md +50 -0
- package/docs/GUIDE.md +288 -0
- package/docs/hourglass-styles.png +0 -0
- package/docs/screenshot.png +0 -0
- package/example/multi-game.json +16 -0
- package/example/updateday.json +37 -0
- package/package.json +63 -5
- package/roblox/UpdateDayBanner.client.luau +159 -0
- package/roblox/UpdateDayCountdown.server.luau +48 -0
- package/roblox/UpdateDayGifts.luau +119 -0
- package/roblox/UpdateDayListener.luau +43 -0
- package/templates/github-actions/updateday.yml +35 -0
- package/ui/core.js +325 -0
- package/ui/countdown.js +775 -0
- package/ui/events.js +249 -0
- package/ui/gifts.js +215 -0
- package/ui/history.js +156 -0
- package/ui/index.html +530 -0
- package/ui/jsonEditor.js +417 -0
- package/ui/page.js +94 -0
- package/ui/players.js +256 -0
- package/ui/run.js +391 -0
- package/ui/sale.js +366 -0
- package/ui/settings.js +414 -0
- 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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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)
|