Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 148 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,148 @@
# Galaxy
# Galaxy

A small lightweight framework that lets you grab any module from anywhere with `shared("Name")`.

## Features

- Grab any module by name with `shared("Name")`.
- Server and client code is detected and started for you.
- Modules load only when first used, then get cached.

## Installation

Add this to your `wally.toml` and run `wally install`:

```toml
[dependencies]
Galaxy = "artisticvampyre/galaxy@1.2.0"
```

## Getting Started

### 1. Start Galaxy

From a server `Script` and a client `LocalScript`, tell Galaxy where to look for your code:

```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Galaxy = require(ReplicatedStorage.Packages.Galaxy)

Galaxy.Launch({
-- Where your reusable modules live
Modules = { ReplicatedStorage.Packages, ReplicatedStorage.Shared },
-- Where your systems live
Systems = { script:WaitForChild("systems") },
})
```

### 2. Make a system

A system is just a module whose name ends in `Service` (server) or `Controller` (client). Galaxy runs the right ones automatically and calls `onInit` then `onStart`.

```lua
-- MainService.luau
local log = shared("log")

local MainService = {}

function MainService:onInit()
log.info("MainService initialized")
end

function MainService:onStart()
log.info("MainService started")
end

return MainService
```

### 3. Use other modules

Call `shared("Name")` to get any module or system by its name:

```lua
local MainService = shared("MainService")
local log = shared("log")
```

### Tip: autocomplete for `shared(...)`

Because `shared(...)` takes a string, the editor doesn't know the type by itself. Add a `--@module` annotation above the line so the language server treats it as that module and gives you autocomplete:

```lua
local MainService = shared("MainService") --@module MainService
```

## How It Works

### Modules and systems

- **Modules** are any `ModuleScript`. They load the first time you ask for them with `shared(...)`.
- **Systems** are modules named `...Service` or `...Controller`. Galaxy starts them for you when the game launches.

### Server vs. client

Galaxy looks at where the code is running and picks the right systems:

- On the server it runs anything named `Service`.
- On the client it runs anything named `Controller`.

So you can keep both in the same folder and each side only runs its own.

### When things start

When you call `Galaxy.Launch`:

1. `onInit` is called on every system, one at a time. Use it for setup.
2. `onStart` is called on every system after all setup is done. Use it for your main logic.

### Loading extra folders into a system

If a system needs helper modules from nearby folders, list them in `DirectoriesToLoad` and Galaxy attaches them for you:

```lua
local MyService = { DirectoriesToLoad = { "components", "data" } }

function MyService:onInit()
-- self.SomeComponent is now available
end

return MyService
```

## When Something Goes Wrong

- If a module fails to load, Galaxy warns you and keeps going, so one mistake doesn't break everything.
- If a module takes too long to load (over 3 seconds), Galaxy tells you which one.
- If two modules depend on each other in a loop, Galaxy warns you and names them.

## Reference

- `Galaxy.Launch({ Modules = {...}, Systems = {...} })` starts the framework.
- `shared("Name")` returns a module or system by name. Works anywhere after `Launch`.
- `Galaxy._VERSION` is the version string.
- On a system, `onInit(self)` runs first for setup, `onStart(self)` runs after for main logic.

## Project Layout

```
src/
init.luau the framework
CycleMetatable.luau cyclic dependency helper
wally.toml package info
test/
server/ client/ shared/ example game using Galaxy
```

## Development

Install tools with [Aftman](https://github.com/LPGhatguy/aftman), then run the test place:

```sh
aftman install
rojo serve test.project.json
```

## License

MIT. See [LICENSE](LICENSE).
76 changes: 57 additions & 19 deletions src/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,26 @@
• ⚡ Optimized for performance
• 🛡️ Type-safe with Luau

Version: 1.0.0
Version: 1.2.0
Author: ArtisticVampyre
Contributor: @32_Bits (https://github.com/git0uttahere)
--]]

-- 🔧 Core Services
local HttpService: HttpService = game:GetService("HttpService")
local RunService: RunService = game:GetService("RunService")

-- ⚙️ Configuration
-- How long (in seconds) a require/onInit may yield before Galaxy warns about it.
local INIT_YIELD_WARNING_TIME = 3

-- 📝 Type Definitions
type LaunchParams = {
Modules: { Instance },
Systems: { Instance },
}

-- 🌟 Main Module
local Galaxy = { _VERSION = "1.0.0" }
local Galaxy = { _VERSION = "1.2.0" }

-- 🎯 Environment Detection
local SystemKey: string = RunService:IsServer() and "service" or "controller"
Expand All @@ -39,6 +43,30 @@ local Systems: { [string]: any } = {}
-- Helper Functions
-----------------------------------------------------

local function _errorHandler(err: any): string
return tostring(err) .. "\n" .. debug.traceback()
end

--[[
Requires a module with error isolation + a yield watchdog. require() runs the
module's top-level body, which can both error AND yield (e.g. a top-level
WaitForChild / :Wait()). Either would otherwise silently stall the whole boot,
so we catch errors (returning ok = false) and warn by name if the require
yields longer than INIT_YIELD_WARNING_TIME.
]]
local function _safeRequire(module: ModuleScript, label: string): (boolean, any)
local completed = false
task.delay(INIT_YIELD_WARNING_TIME, function()
if not completed then
warn(`⏳[Galaxy][IMPORTANT] requiring {label} {module.Name} has been yielding for {INIT_YIELD_WARNING_TIME}s+. A top-level WaitForChild/:Wait()/yield during require blocks the whole boot, move it into onStart.`)
end
end)

local ok, result = xpcall(require, _errorHandler, module)
completed = true
return ok, result
end

-- Count the number of ancestors for a given instance.
local function countAncestors(instance: Instance): number
local count = 0
Expand Down Expand Up @@ -74,14 +102,14 @@ end
local function _load(module: ModuleScript): any?
CycleMetatable.CurrentModuleLoading = module.Name

local success, dataOrErr = pcall(require, module)
local success, dataOrErr = _safeRequire(module, "module")
CycleMetatable.CurrentModuleLoading = nil

if success then
return dataOrErr
end

warn(("❌ Failed to load module '%s' 💥\nError: %s"):format(module.Name, dataOrErr))
warn(`❌ [Galaxy][CRITICAL] Failed to require module {module.Name} (skipped — other modules still load):\n{dataOrErr}`)
return nil
end

Expand Down Expand Up @@ -142,7 +170,7 @@ local function _loadInternal(system, rootModule: ModuleScript)
for _, dirKey: string in internal do
local dir: Folder? = root:FindFirstChild(dirKey) :: Folder?
if not dir then
warn(("❗️Internal directory '%s' not found in system '%s'! 🚫"):format(dirKey, root.Name))
warn(`❗️Internal directory {dirKey} not found in system {root.Name}! 🚫`)
continue
end

Expand All @@ -151,13 +179,9 @@ local function _loadInternal(system, rootModule: ModuleScript)
continue
end

local success, dataOrErr = pcall(require, module)
local success, dataOrErr = _safeRequire(module, "internal module")
if not success then
warn("❌ Failed to load internal module '%s' in system '%s'! 💥\nError: %s"):format(
module.Name,
root.Name,
dataOrErr
)
warn(`❌ [Galaxy] Failed to load internal module {module.Name} in system {root.Name} (skipped)! 💥\nError: {dataOrErr}`)
continue
end

Expand All @@ -170,15 +194,18 @@ end
local function _indexSystems(location: Instance)
for _, descendant: ModuleScript in location:GetDescendants() do
if descendant:IsA("ModuleScript") and descendant ~= script then
if descendant:GetAttribute("GalaxyLoaded") then
continue
end

local childIndex = descendant.Name:lower()
if childIndex:find(SystemKey) then
local activeSystem = _load(descendant)
if activeSystem then
-- Use the current ModuleScript as the root for internal modules.
_loadInternal(activeSystem, descendant)
Systems[descendant.Name] = activeSystem
-- Rename the module to avoid collisions.
descendant.Name = HttpService:GenerateGUID()
descendant:SetAttribute("GalaxyLoaded", true)
end
end
end
Expand Down Expand Up @@ -242,16 +269,27 @@ function Galaxy.Launch(launchParams: LaunchParams)
_indexSystems(systemRoot)
end

for _, system in Systems do
if system.onInit and typeof(system.onInit) == "function" then
system:onInit()
for name, system in Systems do
if typeof(system.onInit) ~= "function" then
continue
end

local initThread = task.spawn(function()
system.onInit(system)
end)

if coroutine.status(initThread) == "suspended" then
warn(
`⏳ [Galaxy][IMPORTANT] {name}.onInit yielded, continuing without waiting for it. Move WaitForChild / :Wait() / network calls into onStart so init order stays deterministic.`
)
end
end

for _, system in Systems do
if system.onStart and typeof(system.onStart) == "function" then
task.spawn(system.onStart, system)
if typeof(system.onStart) ~= "function" then
continue
end
task.spawn(system.onStart, system)
end
end

Expand Down
2 changes: 1 addition & 1 deletion src/wally.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "artisticvampyre/galaxy"
version = "1.0.0"
version = "1.2.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"

Expand Down