diff --git a/docs/tutorials/getting-started/basic-fragment.md b/docs/tutorials/getting-started/basic-fragment.md new file mode 100644 index 0000000..6a19031 --- /dev/null +++ b/docs/tutorials/getting-started/basic-fragment.md @@ -0,0 +1,206 @@ +# Basic Fragment + +Lets go into a basic fragment, and explain how the model works. + +## Importing Catwork + +Each Fragment should first import Catwork. + +```lua +local ReplicatedFirst = game:GetService("ReplicatedFirst") +local Catwork = require(ReplicatedFirst.Catwork) +``` + +!!! note + This tutorial expects Catwork to be in `ReplicatedFirst`, make sure its + there before following this tutorial + + Future tutorials will assume you're importing this. + +## A clock counter + +For this tutorial, we're going to slowly build an API for interfacing with a +simple real-time clock system. + +Add `Lighting` as an import after your `Catwork` import: + +```lua +local Lighting = game:GetService("Lighting") -- required for this tutorial +``` + +### Adding Basic Logic + +Lets create a basic Fragment that iterates the current Lighting time. To do +this, we call the `Catwork.Fragment` constructor: + +```lua +return Catwork.Fragment { + Name = "LightingClockTime", + + Init = function(self) + while true do + task.wait(1) + Lighting.ClockTime += 1/60 + end + end, +} +``` + +If you run this script, you'll notice the clock slowly ticks forward. + +??? bug "Not Working?" + Here's some possible reasons for your code not working. + + 1. You've not imported Catwork, Lighting or ReplicatedFirst. + 2. You didn't include a Runtime, refer to Installation for that. + 3. There's a typo in your script. + +Lets explain how this constructor works. + +First, we give it the name `LightingClockTime`, this doesn't do much internally +as Fragments use a different method for being uniquely identified, however, it +helps us identify which Fragment we're working with. + +```lua + Name = "LightingClockTime +``` + +After that, we add an Init callback, which indicates what should happen when the +Fragment is ready to go. + +```lua + Init = function(self) + while true do + task.wait(1) + Lighting.ClockTime += 1/60 + end + end, +``` + +### Adding an API + +The `Catwork.Fragment` constructor lets you define any logic you want to on the +object, and have it passed to other code using it. + +Lets add a API to get the current clock time, and set a timezone offset. To do +this, add two new methods directly to the constructor + +```lua hl_lines="11-17" +return Catwork.Fragment { + Name = "LightingClockTime", + + Init = function(self) + while true do + task.wait(1) + Lighting.ClockTime += 1/60 + end + end, + + GetClockTime = function(self) + + end, + + SetTimeZoneOffset = function(self, offset) + + end +} +``` + +We're also going to add an in-built timer, and offset, to the fragment as a property: + +```lua hl_lines="4 5" +return Catwork.Fragment { + Name = "LightingClockTime", + + Time = os.time(), + TimeZoneOffset = 0 +``` + +Now, lets implement our two new methods, firstly, `GetClockTime`. This method +should just return `self.Time`: + +```lua + GetClockTime = function(self) + return self.Time + end +``` + +And, for `SetTimeZoneOffset`, this should change `self.TimeZoneOffset` + +```lua + SetTimeZoneOffset = function(self, offset) + self.TimeZoneOffset = offset + end +``` + +### Updating Init + +If you run the project, and interface with the Fragment, you may notice that nothing +happens. This is because we haven't updated our `Init` callback. + +Here's the updated Init callback: +```lua + Init = function(self) + while true do + Lighting.ClockTime = ((self.Time / 3600) % 24) + self.TimeZoneOffset + task.wait(1) + end + end +``` + +If you run the game now, the time in-game should match near to your local +computer time. (If you computer is set to UTC.). + +## Using the API + +If you used a ModuleScript, you can now require your script within *another* script, +and interface with the clock counter. Here's a script that requires in the ClockCounter +script, and prints out the clock time every second: + +```lua +local ClockCounter = require(path.to.ClockCounter) + +while true do + print(ClockCounter:GetClockTime()) + task.wait(1) +end +``` + +## Asynchronous design + +Catwork utilises a simple asynchronous dependency system, through `Fragment:Await` +and `Fragment:HandleAsync`. + +This works by waiting until a Fragment's `Init` function completes (returns) then +resumes any code waiting for it complete. You may notice an issue with our +`Init` callback, in that it **never** returns, and so any code waiting on it will +also never resume. + +Lets fix our Init callback to address this. We're going to use `task.spawn` to +create a new thread within our `Init` callback, that runs independently of any +code waiting upon it. + +```lua + Init = function(self) + task.spawn(function() + while true do + Lighting.ClockTime = ((self.Time / 3600) % 24) + self.TimeZoneOffset + task.wait(1) + end + end) + end +``` + +You should always `Await`/`HandleAsync` on Fragments, because you cannot guarantee +that they are ready. The Service tutorial explains the Fragment:Spawn lifecycle +more in depth. + +```lua hl_lines="2" +local ClockCounter = require(path.to.ClockCounter) +ClockCounter:Await() + +while true do + print(ClockCounter:GetClockTime()) + task.wait(1) +end +``` \ No newline at end of file diff --git a/docs/tutorials/getting-started/installation.md b/docs/tutorials/getting-started/installation.md new file mode 100644 index 0000000..6ee62ae --- /dev/null +++ b/docs/tutorials/getting-started/installation.md @@ -0,0 +1,110 @@ +# Installation + +Catwork can be installed either as a Roblox model, or within a larger project +inside your editor of choice. + + +--- + +The following guide is different for within Roblox Studio, and within external editors. + +=== "Roblox Studio" + + The RBXM can be obtained below. Its best to drag the module into `ReplicatedFirst`, + unless you intend to use the tool as a plugin (which has its own section.) + + The tutorials expect Catwork to be placed in `ReplicatedFirst`. + + [:fontawesome-solid-cat: Download Catwork](https://github.com/metatablecatgames/catwork/releases/download/v0.4.4/catwork.rbxm){ .md-button .md-button--primary} + +=== "External" + + If you intend to use Catwork externally, the sourcecode can be found on the + Releases page on the GitHub repository + + [GitHub Releases](https://github.com/metatablecatgames/catwork/releases/){ .md-button .md-button--primary} + + !!! info "Expected for Studio" + The tutorials expect you to use Catwork within Studio, but can somewhat + be followed in your editor of choice, if you setup the project correctly. + +## Obtaining a Runtime + +Catwork does not come bundled with a Runtime, which is an intentional choice for the time being. + +!!! abstract "CollectionService Runtime (RECOMENDED)" + This runtime loads ModuleScripts with a given tag based on the context it is running + in. + + === "Game Context" + + ```lua + local CollectionService = game:GetService("CollectionService") + local RunService = game:GetService("RunService") + + local TAG_SHARED = "SharedFragment" + local TAG_LOCAL = if RunService:IsClient() then "ClientFragment" else "ServerFragment" + local passed, failed = 0, 0 + + local function safeRequire(module) + local success, result = pcall(require, module) + if success then + passed += 1 + return result + else + warn("Error when requiring", module, ":", result) + failed += 1 + return nil + end + end + + local function loadGroup(tag) + local m = CollectionService:GetTagged(tag) + for _, mod in m do + if mod:IsA("ModuleScript") then + safeRequire(mod) + end + end + end + + local t = os.clock() + loadGroup(TAG_LOCAL) + loadGroup(TAG_SHARED) + local f = os.clock() - t + + print(`🐈 CatworkRun. {passed} modules required, {failed} modules failed. Load time: {math.round(f * 1000)}ms`) + ``` + + === "Plugin Context" + + ```lua + local CollectionService = game:GetService("CollectionService") + if not plugin then return end + + local TAG = "PluginFragment" + + local function safeRequire(module) + local success, result = pcall(require, module) + if success then + return result + else + warn("Error when requiring", module, ":", result) + return nil + end + end + + local function loadGroup(tag) + local m = CollectionService:GetTagged(tag) + for _, mod in m do + if mod:IsDescendentOf(plugin) and mod:IsA("ModuleScript") then + safeRequire(mod) + end + end + end + + loadGroup(TAG) + ``` + + You should generally use this runtime where possible as it's configured from CollectionService tags, and doesn't + require you to fiddle with the script. + diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md new file mode 100644 index 0000000..a7a7864 --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,12 @@ +# Tutorials + +The following section introduces Catwork in an easy-to-digest and learn manner. + +!!! note + These tutorials are designed so you can jump around as you need to for different + concepts, but build on ideas learnt in previous sections. + + If you get stuck, feel free to send me a + [message on the DevForum](https://devforum.roblox.com/u/metatablecatmaid), or + get help from the [Roblox Open Source community](https://discord.gg/Qm3JNyEc32) + discord server! \ No newline at end of file diff --git a/docs/tutorials/services/example.md b/docs/tutorials/services/example.md new file mode 100644 index 0000000..88f4a6a --- /dev/null +++ b/docs/tutorials/services/example.md @@ -0,0 +1,87 @@ +# Example Service + +The following guide explains how to create a simple RemoteEventHandler service, +and how to use it. + +## Remote Handler + +First, lets define our Service. + +```lua +return Catwork.Service { + Name = "RemoteEventHandler" +} +``` + +### Building the Fragment + +Next, we'll create the `Fragment` constructor: + +```lua + Fragment = function(self, params) + if not params.ID then + error("Fragment requires a static identifier") + end + + if not params.Event then + error("Fragment requires a connectable event") + end + + return Catwork:CreateFragmentForService(params, self) + end +``` + +This `Fragment` constructor simply enforces that the Fragment has an ID, and an +Event callback. + +We dont need to define any special logic for `FragmentAdded` here, since all +remaining logic escapes the declarative phase. Though, for example, if you +wanted to defer Spawning so another system can take over, you can simply add this: + +```lua + FragmentAdded = function(self, Fragment) + RemoteDispatcher:queue(Fragment) + end +``` + +Because we're overloading the default `FragmentAdded`, the Fragment wont spawn +unless we tell it to. + +### Setting up the Remote + +Within our `Spawning` callback, we create the remote, and hook it to the event: + +```lua + Spawning = function(self, Fragment) + local remote = Instance.new("RemoteEvent") + + remote.Event = function(...) + Fragment:Event(...) + end + + remote.Name = Fragment.ID + remote.Parent = ReplicatedStorage + end +``` + +!!! note + This assumes you've made a reference to `ReplicatedStorage` + +### Using the Remote Handler + +In another script, lets require in the RemoteHandler service, and create a Fragment: + +```lua +local RemoteEventHandler = require(path.to.RemoteEventHandler) + +RemoteEventHandler:Fragment { + ID = "Meowitzer", + + Event = function(self, plr) + print(`meows at {plr.Name} cutely`) + end +} +``` + +If you run the game, a new Event should appear called `Meowitzer`, if you +`FireServer` it, it'll print the requested message. \ No newline at end of file diff --git a/docs/tutorials/services/index.md b/docs/tutorials/services/index.md new file mode 100644 index 0000000..9eb9edb --- /dev/null +++ b/docs/tutorials/services/index.md @@ -0,0 +1,111 @@ +# Services + +Services are singleton objects that control Fragments under them. For this guide, +we're going to explain how Fragments work under Services, then make a simple +`RemoteEvent` handler. + +## Defining a Service + +`Service`s are defined using `Catwork.Service` + +```lua +Catwork.Service { + Name = "RemoteHandler" +} +``` + +Each service requires a unique name, that is not taken up by another Service. + +## Fragments + +### Creating Fragment + +`Fragment` objects are created through the `Fragment` callback, at this step, +you can manipulate the Fragment by adding new methods, or validating parameters. + +```lua + Fragment = function(self, params) + return Catwork:CreateFragmentForService(params, self) + end +``` + +!!! warning "You must call `CreateFragmentForService`" + This is required as this tells Catwork it can create the Fragment internally. + From this point, you should assume the Fragment is ready and shouldn't be touched + further (outside of Spawning.) + +For example, here, we add a simple method to the Fragment to print `meow`: + +```lua hl_lines="2-4" + Fragment = function(self, params) + function params:Meow() + print("meow!") + end + + return Catwork:CreateFragmentForService(params, self) + end +``` + +!!! tip "Created fragment is the same table as params" + This means you can operate upon `params` as if it were the Fragment, though + you should really do this in `FragmentAdded` + +### Reacting to new Fragments + +`FragmentAdded` is the callback that is invoked straight after `CreateFragmentForService`, +this defines behaviour that should react around the Fragment. Please note that +we still assume the Fragment is in a declarative phase at this point, so avoid +runtime logic. + +!!! note + `Fragment:Spawn` is the intended way for Fragments to escape the declarative + state phase. + +You can either `Spawn` directly from this callback, or defer it to another system. + +```lua + FragmentAdded = function(self, Fragment) + print(`New Fragment: {fragment.Name}`) + Fragment:Spawn() + end +``` + +### Changing `Spawn` logic + +When `Fragment:Spawn` is called, the internal Dispatcher looks for the Service's +spawning callback, which tells the Service that it should act upon this Fragment. + +This function is asynchronous, and wont block the operation of other code. By +default, this simply just calls Init on the fragment, but at this point, the +Fragment has escaped its declarative state, and runtime code can now be operated +upon it. + +```lua + Spawning = function(self, Fragment) + if Fragment.Init then + Fragment:Init() + end + end +``` + +### Basic Lifecycle Graph + +The following graph explains the default behaviour of a Fragment, from +`Service.Fragment` to `Fragment:Init` + +``` mermaid + flowchart TB + A["Service.Fragment"] + B["CreateFragmentForService"] + C["FragmentAdded"] + D["Fragment:Spawn"] + E["Spawning"] + F["Fragment:Init"] + + + A--->B + B---|Internal Fragment Constructor|C + C--->D + D---|Internal Dispatcher|E + E--->F +``` \ No newline at end of file diff --git a/docs/tutorials/services/template-services.md b/docs/tutorials/services/template-services.md new file mode 100644 index 0000000..f2c8cd1 --- /dev/null +++ b/docs/tutorials/services/template-services.md @@ -0,0 +1,100 @@ +# TemplateService + +??? tip "TemplateServices are now implicit" + Catwork previously had an explicit method called `Catwork.TemplateService`, + this has since been removed. You now simply need to use one of two methods + to enable templates + + === "Explicit" + ```lua + return Catwork.Service { + EnableTemplates = true + } + ``` + === "Implicit" + ```lua + return Catwork.Service { + TemplateAdded = function(self, template) + + end + } + ``` + +TemplateServices are an extension to Services that allow them to create Templates. +Templates are small objects that can be used to create lots of Fragments with +a similar shape. + +!!! note + The name `TemplateService` refers to an older implementation of this feature, + Template methods are now simply mounted directly to a Service object if it + detects that it is one. + +## Template +### Defining a Template + +Templates can be defined with the `Service:Template` constructor + +```lua +Service:Template { + Name = "Template", + CreateFragment = function(_, self) + + end +} +``` + +This creates a unique `Template` for the service, the `CreateFragment` callback +is fired when creating a new `Fragment` against the `Template`. + +### Creating a Template + +To create a Template, you use `Service:CreateFragmentFromTemplate`, although, many +Services omit this externally, opting to use an abstraction. + +```lua +Service:CreateFragmentFromTemplate(template, { + -- initial parameters +}) +``` + +!!! danger "Dont give this templates from other services" + This creates undefined behaviour, keep templates to their own service. + +The following graph explains the lifecycle of Template construction: + +``` mermaid + flowchart LR + A["Service:CreateFragmentFromTemplate"] + B["CreateFragment"] + C["Service.Fragment"] + D["Fragment Construction"] + + A--->B + B--->C + C-.->D +``` + +## TemplateAdded + +To react to when new Templates are created, you can add a `TemplateAdded` callback +to your Service: + +```lua hl_lines="4-6" +Catwork.Service { + ... + + TemplateAdded = function(self, Template) + print(`new template: {Template.Name}`) + end +} +``` + +The existence of this callback tells Catwork this is a TemplateService, though +you can also explicitly tell it that the Service is one through `EnableTemplates` + +```lua hl_lines="3" +Catwork.Service { + Name = "SomeService", + EnableTemplates = true +} +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 9929d24..72f6c17 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,6 +51,15 @@ extra_css: nav: - Home: index.md + - Tutorials: + - tutorials/index.md + - Getting Started: + - Installation: tutorials/getting-started/installation.md + - Basic Fragment: tutorials/getting-started/basic-fragment.md + - Services: + - tutorials/services/index.md + - Example Service: tutorials/services/example.md + - Template Services: tutorials/services/template-services.md - Reference: - reference/index.md - Catwork: