Spaces:
Running
Running
Download studwright/modules.json from CyberMax-tools/cybermax: direct link, hf CLI and curl.
- Browser
- Download file 17.8 kB
-
https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/studwright/modules.json
- Command line
-
hf download hf://spaces/CyberMax-tools/cybermax/studwright/modules.json
-
curl -L -o modules.json https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/studwright/modules.json
17.8 kB
| { | |
| "version": "1.0.0", | |
| "tests": 66, | |
| "kit_bytes": 75081, | |
| "total_lines": 2228, | |
| "modules": [ | |
| { | |
| "name": "SaveData", | |
| "side": "Server", | |
| "summary": "Session-locked player saves on DataStoreService.", | |
| "usage": " local SaveData = require(path.to.Studwright.SaveData)\n local store = SaveData.new({\n name = \"PlayerData_v1\",\n template = { Coins = 0, Level = 1, Inventory = {} },\n })\n Players.PlayerAdded:Connect(function(player)\n local profile = store:Load(player) -- nil if the player left or the save is busy\n if not profile then return end\n profile.Data.Coins += 10 -- edit Data freely; it autosaves\n end)\n\nWhat it handles for you:\n * Session lock: one server owns a save at a time, so a player who hops servers fast\n can't duplicate items. A lock older than lockTimeout (default 30 min) is treated as stale\n (crashed server) and taken over.\n * Only UpdateAsync, retries with backoff, request-budget waits.\n * Template reconcile (new keys reach old saves) and numbered migrations.\n * Autosave, save on leave, and a BindToClose flush so shutdowns don't lose progress.\n * Erase(userId) for right-to-erasure requests.", | |
| "functions": [ | |
| "SaveData.new(config)", | |
| "SaveData:Load(player)", | |
| "SaveData:Get(player)", | |
| "SaveData:WaitFor(player, timeout: number?)", | |
| "SaveData:Erase(userId: number)", | |
| "SaveData:Peek(userId: number)", | |
| "Profile:IsActive()", | |
| "Profile:Save(release: boolean?)", | |
| "Profile:Release()", | |
| "Profile:Reset()" | |
| ], | |
| "lines": 388, | |
| "free": false | |
| }, | |
| { | |
| "name": "Receipts", | |
| "side": "Server", | |
| "summary": "A MarketplaceService.ProcessReceipt handler that never double-grants and never loses a purchase.", | |
| "usage": "and never loses a purchase.\n\n local Receipts = require(path.to.Studwright.Receipts)\n Receipts.start(store, {\n [123456789] = function(profile, receipt) -- Developer Product id -> grant\n profile.Data.Coins += 1000\n end,\n })\n\nRules it follows (from Roblox's own guidance on ProcessReceipt):\n * Grant only when the buyer's save is loaded on THIS server; otherwise NotProcessedYet,\n and Roblox retries later (for example when the player rejoins).\n * Every PurchaseId is remembered in the save (last 100 kept), so a retried receipt is\n acknowledged without granting twice.\n * PurchaseGranted is returned only after the save containing that PurchaseId succeeded.\n * A grant function that errors does not consume the purchase.", | |
| "functions": [ | |
| "Receipts.process(receipt)", | |
| "Receipts.start(store, map)", | |
| "Receipts.add(productId: number, fn)" | |
| ], | |
| "lines": 92, | |
| "free": false | |
| }, | |
| { | |
| "name": "Codes", | |
| "side": "Server", | |
| "summary": "Promo / Twitter-style codes, redeemed safely on the server.", | |
| "usage": " local codes = Codes.new({\n RELEASE = { reward = function(profile) profile.Data.Coins += 500 end },\n SPOOKY = { reward = giveHat, expires = DateTime.fromIsoDate(\"2026-11-01\").UnixTimestamp },\n [\"1KLIKES\"] = { reward = giveGems, minLevel = 5, levelOf = function(p) return p.Data.Level end },\n })\n local ok, message = codes:Redeem(profile, textFromClient)\n\nCodes are case- and space-insensitive, each one works once per player (remembered in the\nsave), can expire, can be limited to players at or above a level, and attempts are\nthrottled (5 per 30 s per player by default) to stop brute-forcing.", | |
| "functions": [ | |
| "Codes.new(list, options)", | |
| "Codes:Add(name: string, def)", | |
| "Codes:Remove(name: string)", | |
| "Codes:Redeem(profile, text)", | |
| "Codes:Active()" | |
| ], | |
| "lines": 126, | |
| "free": false | |
| }, | |
| { | |
| "name": "DailyRewards", | |
| "side": "Server", | |
| "summary": "A login streak that resets at a fixed hour, not 24 h after the last claim.", | |
| "usage": " local daily = DailyRewards.new({\n rewards = { 100, 150, 200, 300, 400, 600, \"Chest\" }, -- day 1..7, then it cycles\n grant = function(profile, reward, day) ... end,\n })\n local status = daily:Status(profile) -- { canClaim, day, streak, nextIn, reward }\n local ok, reward, day = daily:Claim(profile)\n\nDays are counted in UTC (resetHour, default 0 = midnight UTC). Missing one whole day resets\nthe streak unless graceDays allows it. All checks run on the server from os.time(), so\nchanging the device clock does nothing.", | |
| "functions": [ | |
| "DailyRewards.new(config)", | |
| "DailyRewards:dayNumber(t: number)", | |
| "DailyRewards:Status(profile)", | |
| "DailyRewards:Claim(profile)" | |
| ], | |
| "lines": 90, | |
| "free": false | |
| }, | |
| { | |
| "name": "PlaytimeRewards", | |
| "side": "Server", | |
| "summary": "\"stay 5 / 10 / 20 minutes, get a gift\" for each play session.", | |
| "usage": " local gifts = PlaytimeRewards.new({\n { after = 5 * 60, reward = function(profile) profile.Data.Coins += 100 end, label = \"100 Coins\" },\n { after = 15 * 60, reward = giveSpin, label = \"Free Spin\" },\n })\n gifts:Begin(profile) -- when the save loads\n gifts:Status(profile) -- list of { label, after, remaining, claimed, ready }\n gifts:Claim(profile, 1) -- from a RemoteFunction; the server checks the time\n gifts:End(profile) -- on leave (Cleanup does this if you add it)\n\nThe timer lives on the server, so clients can't fast-forward it. Set `persist = true` to keep\nprogress across sessions on the same UTC day instead of restarting each join.", | |
| "functions": [ | |
| "PlaytimeRewards.new(list, options)", | |
| "PlaytimeRewards:Begin(profile)", | |
| "PlaytimeRewards:Elapsed(profile)", | |
| "PlaytimeRewards:Status(profile)", | |
| "PlaytimeRewards:Claim(profile, index: number)", | |
| "PlaytimeRewards:End(profile)" | |
| ], | |
| "lines": 116, | |
| "free": false | |
| }, | |
| { | |
| "name": "Leaderboard", | |
| "side": "Server", | |
| "summary": "Leaderstats in the player list + an all-time global top list.", | |
| "usage": " Leaderboard.stats(player, { Coins = \"Coins\", Level = \"Level\" }, profile) -- leaderstats folder, kept in sync\n local board = Leaderboard.global({ name = \"TopCoins\", size = 50, refresh = 120 })\n board:Submit(player.UserId, profile.Data.Coins) -- on leave / autosave (throttled per user)\n board:Top() -- { { rank, userId, value, name } ... } (cached)\n board.Updated:Connect(function(rows) ... end)\n\nOrderedDataStores only store integers, so values are floored and capped at 2^53. For values\nthat can grow past that (huge simulator numbers), use Leaderboard.encodeLog / decodeLog.", | |
| "functions": [ | |
| "Leaderboard.stats(player, map, profile, interval: number?)", | |
| "Leaderboard.encodeLog(x: number)", | |
| "Leaderboard.decodeLog(n: number)", | |
| "Leaderboard.global(config)", | |
| "Leaderboard:Submit(userId: number, value: number, force: boolean?)", | |
| "Leaderboard:Refresh()", | |
| "Leaderboard:Top()" | |
| ], | |
| "lines": 152, | |
| "free": false | |
| }, | |
| { | |
| "name": "LootTable", | |
| "side": "Shared", | |
| "summary": "Weighted rolls for eggs, crates, spins and drops, with pity and luck.", | |
| "usage": " local egg = LootTable.new({\n { id = \"Dog\", weight = 60, rarity = \"Common\" },\n { id = \"Cat\", weight = 30, rarity = \"Rare\" },\n { id = \"Dragon\", weight = 9.5, rarity = \"Epic\" },\n { id = \"Phoenix\",weight = 0.5, rarity = \"Legendary\" },\n }, { pity = { rarity = \"Legendary\", after = 150 } })\n\n local item = egg:Roll({ luck = 2, state = profile.Data.EggPity }) -- server only\n egg:Odds({ luck = 2 }) --> { { id = \"Dog\", chance = 0.5797, text = \"57.97%\" }, ... } for the UI\n\nLuck multiplies the weight of every entry rarer than `luckFrom` (default: all but the most common),\nand Odds() shows players the true chances after luck, as Roblox's policy on paid random items asks.\nPity guarantees the pity rarity after `after` rolls without it; the counter lives in a table you\nkeep in the save. Rolls use a server Random object (pass `rng` for seeded tests).", | |
| "functions": [ | |
| "LootTable.new(entries, options)", | |
| "LootTable:Odds(options)", | |
| "LootTable:Roll(options)", | |
| "LootTable:RollMany(n: number, options)" | |
| ], | |
| "lines": 127, | |
| "free": false | |
| }, | |
| { | |
| "name": "Inventory", | |
| "side": "Shared", | |
| "summary": "Stackable items, slot limits and equipping, stored as plain save data.", | |
| "usage": " local inv = Inventory.wrap(profile.Data.Inventory, {\n capacity = 40, -- slots\n maxStack = { Potion = 99, Sword = 1 }, -- default stack limit: 1\n })\n inv:Add(\"Potion\", 5) --> added (number), leftover (number)\n inv:Remove(\"Potion\", 2) --> true / false (never goes negative)\n inv:Count(\"Potion\") --> 3\n inv:Equip(\"Sword\", \"Weapon\")\n inv.Changed:Connect(function(itemId, newCount) ... end)\n\nThe saved shape is DataStore-safe (lists and string keys only):\n { slots = { { id = \"Potion\", n = 3 }, ... }, equipped = { Weapon = \"Sword\" } }", | |
| "functions": [ | |
| "Inventory.wrap(data, options)", | |
| "Inventory:StackLimit(id: string)", | |
| "Inventory:Count(id: string)", | |
| "Inventory:Has(id: string, amount: number?)", | |
| "Inventory:FreeSlots()", | |
| "Inventory:SpaceFor(id: string)", | |
| "Inventory:Add(id: string, amount: number?)", | |
| "Inventory:Remove(id: string, amount: number?)", | |
| "Inventory:Equip(id: string, slot: string)", | |
| "Inventory:Unequip(slot: string)", | |
| "Inventory:Equipped(slot: string)", | |
| "Inventory:List()" | |
| ], | |
| "lines": 175, | |
| "free": false | |
| }, | |
| { | |
| "name": "Currency", | |
| "side": "Shared", | |
| "summary": "Wallets with multipliers, safe spending and rebirths.", | |
| "usage": "local coins = Currency.new(\"Coins\", { max = 1e300 })\ncoins:AddMultiplier(\"VIP\", 2) -- e.g. a Game Pass\ncoins:Give(profile, 10) --> 20 (multiplied)\ncoins:Spend(profile, 15) --> true / false (never below zero)\ncoins.Changed:Connect(function(profile, newValue, delta) ... end)\n\nlocal rebirth = Currency.rebirth({ currency = coins, base = 1e4, growth = 3, key = \"Rebirths\",\n onRebirth = function(profile, count) profile.Data.Level = 1 end })\nrebirth:Cost(profile) --> 10000 for the first, 30000 for the second, ...\nrebirth:Try(profile) --> true / false", | |
| "functions": [ | |
| "Currency.new(key: string, options)", | |
| "Currency:Get(profile)", | |
| "Currency:AddMultiplier(name: string, factor: number?)", | |
| "Currency:SetPlayerMultiplier(profile, name: string, factor: number?)", | |
| "Currency:Multiplier(profile)", | |
| "Currency:Give(profile, amount: number, raw: boolean?)", | |
| "Currency:CanAfford(profile, cost: number)", | |
| "Currency:Spend(profile, cost: number)", | |
| "Currency:Forget(profile)", | |
| "Currency.rebirth(config)", | |
| "Rebirth:Count(profile)", | |
| "Rebirth:Cost(profile)", | |
| "Rebirth:Bonus(profile)", | |
| "Rebirth:Try(profile)" | |
| ], | |
| "lines": 139, | |
| "free": false | |
| }, | |
| { | |
| "name": "RemoteGuard", | |
| "side": "Server", | |
| "summary": "Rate limits and argument checks for RemoteEvents / RemoteFunctions.", | |
| "usage": "Never trust the client: every value an exploiter sends is checked before your code sees it.\n\n local T = RemoteGuard.types\n RemoteGuard.onEvent(remotes.BuyItem, {\n rate = 4, per = 1, -- at most 4 calls per second per player\n args = { T.string(1, 32), T.integer(1, 99) },\n }, function(player, itemId, amount)\n ... -- only runs with a valid itemId and amount\n end)\n\n RemoteGuard.onInvoke(remotes.Redeem, { rate = 1, per = 2, args = { T.string(1, 40) } },\n function(player, code) return codes:Redeem(store:Get(player), code) end)\n\nChecks reject NaN and infinity in numbers, wrong types, over-long strings, unknown enum values,\nextra arguments and tables that are too deep or too big. Rejected calls are counted per player;\n`onReject(player, remoteName, reason)` lets you log or kick.", | |
| "functions": [ | |
| "T.number(min: number?, max: number?)", | |
| "T.integer(min: number?, max: number?)", | |
| "T.string(minLen: number?, maxLen: number?)", | |
| "T.boolean()", | |
| "T.oneOf(...)", | |
| "T.Vector3(maxMagnitude: number?)", | |
| "T.CFrame()", | |
| "T.instance(className: string?)", | |
| "T.optional(check)", | |
| "T.array(check, maxItems: number?)", | |
| "T.shape(fields)", | |
| "RemoteGuard.limiter(rate: number, per: number, clock)", | |
| "Limiter:Allow(key)", | |
| "Limiter:Forget(key)", | |
| "RemoteGuard.check(spec, ...)", | |
| "RemoteGuard.wrap(name: string, spec, fn)", | |
| "RemoteGuard.onEvent(remote, spec, fn)", | |
| "RemoteGuard.onInvoke(remote, spec, fn)" | |
| ], | |
| "lines": 272, | |
| "free": false | |
| }, | |
| { | |
| "name": "Cooldown", | |
| "side": "Shared", | |
| "summary": "Per-player, per-action cooldowns checked on the server.", | |
| "usage": " local cd = Cooldown.new({ Punch = 0.6, Dash = 3, Spin = 86400 })\n if cd:Use(player.UserId, \"Dash\") then ... end -- true and starts the cooldown, or false\n cd:Remaining(player.UserId, \"Dash\") -- seconds left (0 when ready)\n cd:Reset(player.UserId) -- e.g. on respawn or leave\n\nUse a clock that fits the action: os.clock() (default) for combat, os.time() for long\ncooldowns that must survive a server restart (save `Export()` and `Import()` it).", | |
| "functions": [ | |
| "Cooldown.new(durations, clock)", | |
| "Cooldown:Remaining(id, action: string)", | |
| "Cooldown:Ready(id, action: string)", | |
| "Cooldown:Use(id, action: string, duration: number?)", | |
| "Cooldown:Reset(id, action: string?)", | |
| "Cooldown:Export(id)", | |
| "Cooldown:Import(id, t)" | |
| ], | |
| "lines": 72, | |
| "free": false | |
| }, | |
| { | |
| "name": "StateMachine", | |
| "side": "Shared", | |
| "summary": "Clear states for rounds, NPCs, doors and boss phases.", | |
| "usage": " local round = StateMachine.new({\n initial = \"Intermission\",\n states = {\n Intermission = { enter = function(sm) sm:After(15, \"Playing\") end },\n Playing = { enter = startRound, exit = cleanupMap, update = checkWinner },\n Ended = { enter = function(sm) sm:After(5, \"Intermission\") end },\n },\n transitions = { Intermission = { \"Playing\" }, Playing = { \"Ended\" }, Ended = { \"Intermission\" } },\n })\n round:Start()\n round:Go(\"Ended\") -- false if Playing -> Ended weren't allowed\n round.Changed:Connect(function(new, old) ... end)\n RunService.Heartbeat:Connect(function(dt) round:Update(dt) end)\n\nAfter(seconds, state) timers are cancelled automatically when the state changes.", | |
| "functions": [ | |
| "StateMachine.new(config)", | |
| "StateMachine:Start()", | |
| "StateMachine:Can(to: string)", | |
| "StateMachine:Go(to: string)", | |
| "StateMachine:Is(name: string)", | |
| "StateMachine:Update(dt: number)", | |
| "StateMachine:After(seconds: number, to: string)" | |
| ], | |
| "lines": 105, | |
| "free": false | |
| }, | |
| { | |
| "name": "Spring", | |
| "side": "Shared", | |
| "summary": "Smooth, interruptible motion for UI, cameras and bobbing parts.", | |
| "usage": "A damped harmonic spring solved exactly each step, so it stays stable at any frame rate.\n\n local s = Spring.new(0, { speed = 12, damping = 0.8 }) -- damping 1 = no overshoot\n s:SetTarget(1)\n RunService.RenderStepped:Connect(function(dt)\n frame.Size = UDim2.fromScale(s:Step(dt), 0.1)\n end)\n s:Impulse(4) -- kick it (e.g. a coin counter bounce)\n\nWorks on numbers. For Vector3 / UDim2 use one spring per axis or Spring.group(n).", | |
| "functions": [ | |
| "Spring.new(initial: number, options)", | |
| "Spring:SetTarget(target: number)", | |
| "Spring:Impulse(velocity: number)", | |
| "Spring:Snap(value: number)", | |
| "Spring:IsResting(epsilon: number?)", | |
| "Spring:Step(dt: number)", | |
| "Spring.group(n: number, initial: number, options)" | |
| ], | |
| "lines": 90, | |
| "free": false | |
| }, | |
| { | |
| "name": "Signal", | |
| "side": "Shared", | |
| "summary": "A small, fast event object for Luau.", | |
| "usage": "Connect / Once / Wait / Fire / DisconnectAll. Handlers added or removed while a\nsignal is firing are safe: the current Fire uses a snapshot of the handler list.", | |
| "functions": [ | |
| "ConnectionClass:Disconnect()", | |
| "Signal.new()", | |
| "Signal:Connect(fn)", | |
| "Signal:Once(fn)", | |
| "Signal:Fire(...)", | |
| "Signal:Wait()", | |
| "Signal:DisconnectAll()" | |
| ], | |
| "lines": 73, | |
| "free": true | |
| }, | |
| { | |
| "name": "Cleanup", | |
| "side": "Shared", | |
| "summary": "Collect everything a system creates, then undo it in one call.", | |
| "usage": "Accepts Instances, RBXScriptConnections, Studwright Connections, functions, threads\nand any table with a Destroy or Disconnect method. Clean() runs in reverse order\n(last added, first cleaned) and is safe to call twice.", | |
| "functions": [ | |
| "Cleanup.new()", | |
| "Cleanup:Add(item)", | |
| "Cleanup:Connect(signal, fn)", | |
| "Cleanup:Remove(item)", | |
| "Cleanup:Count()", | |
| "Cleanup:Clean()", | |
| "Cleanup:AttachTo(instance)" | |
| ], | |
| "lines": 91, | |
| "free": true | |
| }, | |
| { | |
| "name": "Format", | |
| "side": "Shared", | |
| "summary": "The number and time text every simulator, tycoon and obby needs.", | |
| "usage": "Format.abbreviate(1530000) --> \"1.53M\"\nFormat.commas(1234567) --> \"1,234,567\"\nFormat.clock(3725) --> \"1:02:05\"\nFormat.duration(3725) --> \"1h 2m\"\nFormat.ordinal(22) --> \"22nd\"\nFormat.percent(0.0125) --> \"1.25%\"", | |
| "functions": [ | |
| "Format.setSuffixes(list: { string })", | |
| "Format.abbreviate(n: number, decimals: number?)", | |
| "Format.commas(n: number)", | |
| "Format.clock(seconds: number)", | |
| "Format.duration(seconds: number)", | |
| "Format.ordinal(n: number)", | |
| "Format.percent(fraction: number, decimals: number?)" | |
| ], | |
| "lines": 120, | |
| "free": true | |
| } | |
| ] | |
| } | |