# Contributing

selene is written in Rust, so knowledge of the ecosystem is expected.

selene uses [Full Moon](https://github.com/Kampfkarren/full-moon) to parse the Lua code losslessly, meaning whitespace and comments are preserved. You can read the full documentation for full-moon on its [docs.rs](https://docs.rs/full_moon/latest/full_moon/) page.

TODO: Upload selene-lib on crates.io and link the docs.rs page for that as well as throughout the rest of this article.

## Writing a lint

In selene, lints are created in isolated modules. To start, create a file in `selene-lib/src/lints` with the name of your lint. In this example, we're going to call the lint `cool_lint.rs`.

Let's now understand what a lint consists of. selene takes lints in the form of structs that implement the `Lint` trait. The `Lint` trait expects:

  * A `Config` associated type that defines what the configuration format is expected to be. Whatever you pass must be [deserializable](https://serde.rs/).
  * An `Error` associated type that implements [`std::error::Error`](https://doc.rust-lang.org/std/error/trait.Error.html). This is used if configurations can be invalid (such as a parameter only being a number within a range). Most of the time, configurations cannot be invalid (other than deserializing errors, which are handled by selene), and so you can set this to [`std::convert::Infallible`](https://doc.rust-lang.org/std/convert/enum.Infallible.html).
  * A `SEVERITY` constant which is either `Severity::Error` or `Severity::Warning`. Use `Error` if the code is positively impossible to be correct.
  * A `LINT_TYPE` constant which is either `Complexity`, `Correctness`, `Performance`, or `Style`. So far not used for anything.
  * A `new` function with the signature `fn new(config: Self::Config) -> Result<Self, Self::Error>`. With the selene CLI, this is called once.
  * A `pass` function with the signature `fn pass(&self, ast: &full_moon::ast::Ast, context: &Context, ast_context: &AstContext) -> Vec<Diagnostic>`. The `ast` argument is the full-moon representation of the code. The `context` argument provides optional additional information, such as the standard library being used. The `ast_context` argument provides context specific to that AST, such as its scopes. Any `Diagnostic` structs returned here are displayed to the user.

For our purposes, we're going to write:

    use super::*;
    use std::convert::Infallible;

    struct CoolLint;

    impl Lint for CoolLint {
        type Config = ();
        type Error = Infallible;

        const SEVERITY: Severity = Severity::Warning;
        const LINT_TYPE: LintType = LintType::Style;

        fn new(_: Self::Config) -> Result<Self, Self::Error> {
            Ok(CoolLint)

        fn pass(&self, ast: &Ast, _: &Context, _: &AstContext) -> Vec<Diagnostic> {
            unimplemented!()

The implementation of `pass` is completely up to you, but there are a few common patterns.

  * Creating a visitor over the ast provided and creating diagnostics based off of that. See [`divide_by_zero`](https://github.com/Kampfkarren/selene/blob/master/selene-lib/src/lints/divide_by_zero.rs) and [`suspicious_reverse_loop`](https://github.com/Kampfkarren/selene/blob/master/selene-lib/src/lints/suspicious_reverse_loop.rs) for straight forward examples.
  * Using the `ScopeManager` struct to lint based off of usage of variables and references. See [`shadowing`](https://github.com/Kampfkarren/selene/blob/master/selene-lib/src/lints/shadowing.rs) and [`global_usage`](https://github.com/Kampfkarren/selene/blob/master/selene-lib/src/lints/global_usage.rs).

### Getting selene to recognize the new lint

Now that we have our lint, we have to make sure selene actually knows to use it. There are two places you need to update.

In selene-lib/src/lib.rs, search for `use_lints!`. You will see something such as:

    use_lints! {
        almost_swapped: lints::almost_swapped::AlmostSwappedLint,
        divide_by_zero: lints::divide_by_zero::DivideByZeroLint,
        empty_if: lints::empty_if::EmptyIfLint,
        ...

Put your lint in this list (alphabetical order) as the following format:

    lint_name: lints::module_name::LintObject,

For us, this would be:

    cool_lint: lints::cool_lint::CoolLint,

Next, in `selene-lib/src/lints.rs`, search for `pub mod`, and you will see:

    pub mod almost_swapped;
    pub mod divide_by_zero;
    pub mod empty_if;
    ...

Put your module name in this list, also in alphabetical order.

    pub mod almost_swapped;
    pub mod cool_lint;
    pub mod divide_by_zero;
    pub mod empty_if;
    ...

And we're done! You should be able to `cargo build --bin selene` and be able to use your new lint.

### Writing tests

The selene codebase uses tests extensively for lints. It means we never have to actually build the CLI tool in order to test, and we can make sure we don't have any regressions. **Testing is required if you want to submit your lint to the selene codebase.**

To write a simple test, create a folder in `selene-lib/tests` with the name of your lint. Then, create as many `.lua` files as you want to test. These should contain both cases that do and do not lint. For our cases, we're going to assume our test is called `cool_lint.lua`.

Then, in your lint module, add at the bottom:

    #[cfg(test)]
    mod tests {
        use super::{super::test_util::test_lint, *};

        #[test]
        fn test_cool_lint() {
            test_lint(
                CoolLint::new(()).unwrap(),
                "cool_lint",
                "cool_lint",
            );

Let's discuss what this code means, assuming you're familiar with [the way tests are written and performed in Rust](https://doc.rust-lang.org/book/ch11-00-testing.html).

The `test_lint` function is the easiest way to test that a lint works. It'll search the source code we made before, run selene on it (only your lint), and check its results with the existing `[filename].stderr` file, or create one if it does not yet exist.

The first argument is the lint object to use. `CoolLint::new(())` just means "create `CoolLint` with a configuration of `()`". If your lint specifies a configuration, this will instead be something such as `CoolLintConfig::default()` or whatever you're specifically testing.

The `.unwrap()` is just because `CoolLint::new` returns a `Result`. If you want to test configuration errors, you can avoid `test_lint` altogether and just test `CoolLint::new(...).is_err()` directly.

The first `"cool_lint"` is the name of the folder we created. The second `"cool_lint"` is the name of the _Lua file_ we created.

Now, just run `cargo test`, and a `.stderr` file will be automatically generated! You can manipulate it however you see fit as well as modifying your lint, and so long as the file is there, selene will make sure that its accurate.

Optionally, you can add a `.std.toml` with the same name as the test next to the lua file, where you can specify a custom [standard library](https://kampfkarren.github.io/selene/usage/std.html) to use. If you do not, the Lua 5.1 standard library will be used.

### Documenting it

This step is only if you are contributing to the selene codebase, and not just writing personal lints (though I'm sure your other programmers would love if you did this).

To document a new lint, edit `docs/src/SUMMARY.md`, and add your lint to the table of contents along the rest. As with everything else, make sure it's in alphabetical order.

Then, edit the markdown file it creates (if you have `mdbook serve` on, it'll create it for you), and write it in this format:

    # lint_name
    ## What it does
    Explain what your lint does, simply.

    ## Why this is bad
    Explain why a user would want to lint this.

    ## Configuration
    Specify any configuration if it exists.

    ## Example
    ```lua
    -- Bad code here

    ...should be written as...

    ```lua
    -- Good code here

    ## Remarks
    If there's anything else a user should know when using this lint, write it here.

This isn't a strict format, and you can mess with it as appropriate. For example, `standard_library` does not have a "Why this is bad" section as not only is it a very encompassing lint, but it should be fairly obvious. Many lints don't specify a "...should be written as..." as it is either something with various potential fixes (such as [`global_usage`](https://kampfkarren.github.io/selene/lints/global_usage.html)) or because the "good code" is just removing parts entirely (such as [`unbalanced_assignments`](https://kampfkarren.github.io/selene/lints/unbalanced_assignments.html)).


I know little about this module, I do not know if it has any functionality.

All that I know is it has 3 functions:

  * `getKeyDown` it has two parameters, I do not know the returns.
  * `getMouseDown` it has two parameters, I do not know the returns.
  * `map` it has one parameter, I do not know the returns.


An incredibly simple library, exposes roblox's [`Players:GetNameFromUserIdAsync`](https://developer.roblox.com/api-reference/function/Players/GetNameFromUserIdAsync) and [`Players:GetUserIdFromNameAsync`](https://developer.roblox.com/api-reference/function/Players/GetNameFromUserIdAsync), likely with some modification.

I am unsure whether these functions error or return `nil` in the event of failure.

`players:GetUsername(userId)` returns the username a player given their user ID. `players:GetUserId(username)` returns the user ID a player given their username.

You can see an example of it being used in the [`LifeSensor`](https://arvidsilverlock.github.io/Pilot.lua-Luau-LSP/objects/LifeSensor) documentation.


An incredibly simple library, does not actually provide support for `Tween` instances, but rather exposes [`TweenService:GetValue()`](https://create.roblox.com/docs/reference/engine/classes/TweenService#GetValue), a simple function that remaps a number between `0` and `1` based on `TweenService` interpolation styles.

Here is an example program written using the `tween` library to tween a `GuiObject`'s position, given a target, duration and easing style/direction.

    local tween = require("tween")

    local function tweenPosition(object: GuiObject, targetPosition: UDim2, duration: number, easingStyle: Enum.EasingStyle?, easingDirection: Enun.EasingDirection?)
        local initialPosition = object.Position
        local alpha = 0

        repeat
            local remappedAlpha = tween:GetValue(alpha, easingStyle, easingDuration)
            object.Position = initialPosition:Lerp(targetPosition, remappedAlpha)

            local deltaTime = task.wait() / duration
            alpha = math.min(alpha + deltaTime, 1)
        until alpha == 1

        object.Position = targetPosition
    end

    -- This function *yields* (waits) until the tween completes, if you didn't want it to wait, you could do:
    task.spawn(tweenPosition, object, targetPosition, duration, easingStyle, easingDirection)

    -- Instead of:
    tweenPosition(object, targetPosition, duration, easingStyle, easingDirection)

If you do not wish to grapple directly with the `tween` library, you can use [this](https://discord.com/channels/616089055532417036/1281197116030713897) module that losely replicates the behaviour of `TweenService`.


Provides the [`FileSystemRoot`](https://arvidsilverlock.github.io/Pilot.lua-Luau-LSP/types/File System/File Types/FileSystemRoot) of the file system used by `require`. It has a metatable of type [`FileSystem`](https://arvidsilverlock.github.io/Pilot.lua-Luau-LSP/types/File System/FileSystem).


You can now create more than just rectangles and squares. You can now define your own point-constraint structures like triangles, irregular quadrilaterals and n-sided polygons and then turn them into RigidBodies! Here's how Custom RigidBodies work and how you can create your own!

In the above image, the green dots are the arbitrary points we chose for our RigidBody, if we pass in a UI element, the 4 dots are automatically placed at its corners, also keeping in mind its rotation. There are 6 lines connecting the points all together. The lines in black are the edges of the RigidBody and the lines in red are "support constraints" which are meant to hold the RigidBody's structure in place and it prevent it from collapsing into nothingness. It is worth noting that support constraints are **not** used in collision detection. All constraints are rods.

In order to create custom RigidBodies, you don't need to specify a UI element/Object when using `Engine:Create()`. You would pass in a property called "Structure". This contains a table of constraint data. Here's how a structure looks like for a rectangle:

    -- { Point1: Vector2, Point2: Vector2, Support: boolean }

    local struct = {
        { Vector2.new(0, 0), Vector2.new(0, 10) },
        { Vector2.new(0, 10), Vector2.new(10, 10) },
        { Vector2.new(10, 10), Vector2.new(10, 0) },
        { Vector2.new(10, 0), Vector2.new(0, 0) },
        { Vector2.new(0, 0), Vector2.new(10, 10), true },
        { Vector2.new(10, 0), Vector2.new(0, 10), true }

Copy

You just need to provide the positions of the points of the edges and specify if they are support constraints or not! Note that in order to create custom rigidbodies, you **must** specify a frame for the Engine's Canvas. You can then use `Engine:Create()` to turn this structure into a RigidBody. A simple example of a triangle:

    local viewport = workspace.CurrentCamera.ViewportSize

    local function MakeTriangle(a: Vector2, b: Vector2, c: Vector2)
        return {
            { a, b, false },
            { a, c, false },
            { b, c, false }
    end

    local triangle = Engine:Create("RigidBody", {
        Structure = MakeTriangle(viewport/2, viewport/2 + Vector2.new(-40, 80), viewport/2 + Vector2.new(40, 80)),
        Collidable = true,
        Anchored = false
    })

Copy

Custom RigidBodies also support Quadtree collision detection! It is worth noting that not all methods will work for custom RigidBodies since the idea is fairly new.


A canvas specifies boundaries out of which RigidBodies won't be able to go unless RigidBody.KeepInCanvas is set to false. By default this canvas is set to `workspace.CurrentCamera.ViewportSize`. In order to specify a new canvas, we use the `Engine:CreateCanvas()` method.

    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local Nature2D = require(ReplicatedStorage.Nature2D)

    local engine = Nature2D.init(screenGuiInstance)
    engine:CreateCanvas(Vector2.new(100, 100), Vector2.new(500, 300)) -- topLeft: (100, 100), size: (500, 300)

Copy

The above should specify a canvas as such:

In order to render custom constraints and points, it is important to set the "frame" of the canvas. To do that:

    engine:CreateCanvas(Vector2.new(100, 100), Vector2.new(500, 300), GuiFrameInstance)

    -- OR

    engine.canvas.frame = GuiFrameInstance

Copy


In this example, we create custom constraints, mainly a RigidBody hanging on a rope firmly. We'll also simulate the RigidBody by applying wind forces to it. The final result will look like the following:

Firstly, we require the module and initialize the engine. It is important to set a frame for the canvas if we wish to render constraints on screen.

    local canvas = path.to.canvasFrame
    local Nature2D = require(game:GetService("ReplicatedStorage").Nature2D)

    local viewport = workspace.CurrentCamera.ViewportSize

    local Engine = Nature2D.init(path.to.screengui)
    Engine:SetPhysicalProperty("CollisionMultiplier", 0.1)
    Engine:CreateCanvas(Vector2.new(0, 0), viewport, canvas)

Copy

Next, we choose an anchor point, this is the point the rope hangs on from.

    local Anchor = Engine:Create("Point", {
       Position = Vector2.new(viewport.X/2, 0),
       Visible = false,
       Snap = true, -- prevent the point from changing positions
       KeepInCanvas = false -- or use Anchor:KeepInCanvas(false) instead of the property
    })

Copy

We then create a RigidBody and connect the anchor and the RigidBody with a constraint.

    local RigidBodyFrame = path.to.UIElement
    local Body = Engine:Create("RigidBody", {
       Object = RigidBodyFrame,
       Collidable = true,
       Anchored = false
    })

    local TopLeftCorner = Body:GetVertices()[1]

    -- I set the constraint type to be a rope. You can use Rod or Spring as well!
    local Rope = Engine:Create("Constraint", {
       Type = "Rope",
       Point1 = Anchor,
       Point2 = TopLeftCorner,
       Visible = true,
       Thickness = 3,
       Color = Color.new(1, 1, 1) -- set constraint color to white (or use Rope:Stroke(Color3.new(1, 1, 1)))
    })

Copy

We now apply random wind forces to the RigidBody to complete the simulation!

    Engine:Start() -- start the simulation

    while task.wait() do
       local random = Random.new()
       Body:ApplyForce(Vector2.new(random:NextNumber(0, 0.5), 0)) -- apply a random wind force
    end

Copy

There you go! You now have a simulation with custom constraints. These constraints can also be used to connect multiple RigidBodies with each other as well as to create custom RigidBodies and SoftBodies!


Iterations provide accurate calculations for more rigid and smoother physics. Constraint iterations are applied to Constraint:Constrain() method. Constraint iterations are extremely useful of rod constraints and rope constraints. Constraint iterations do not work on spring constraints.

Collision iterations are used to provide accurate and rigid collision detection and resolution. By default both of these iterations are set to 1. Iterations can be in the range of 1-10 only. Collision iterations can be set only if quadtrees are being used in collision detection.

Keep in mind that the higher the number of iterations the more accurate results. But, having more iterations means you’ll have to sacrifice performance. The lesser the number of iterations, the better performance but we’ll have to sacrifice on accuracy. So be careful where you use them!

You can set constraint iterations and collision iterations by using `Engine:SetConstraintIterations()` and `Engine:SetCollisionIterations()`.

    local Engine = Nature2D.init(someScreenGuiInstance)
    Engine:UseQuadtrees(true)
    Engine:SetCollisionIterations(2)
    Engine:SetConstraintIterations(3)

Copy

##### **Recommended Iteration Amounts**

Constraint Iterations - 3
Collision Iterations - 4


# lune-cloud-luau-client

> If you are looking to execute tasks from your own Lune scripts, I rewrote
> this entire project as a library with a muhch more ergonomic API for use from
> lune scripts. You can find it in the [ergo-input](https://github.com/itsfrank/lune-cloud-luau-client/tree/ergo-input?tab=readme-ov-file) branch

This is a lune port of the sample client script provided by Roblox in the
[announcement](https://devforum.roblox.com/t/beta-open-cloud-engine-api-for-executing-luau/3172185?u=itsfrank17) of the Open Cloud
Engine API for Executing Luau beta.

### usage

#### Prerequisites

1. Make sure you have lune installed: [documentation](https://lune-org.github.io/docs/getting-started/1-installation)
2. Make an api key for the API: [instructions the announcement](https://devforum.roblox.com/t/beta-open-cloud-engine-api-for-executing-luau/3172185?u=itsfrank17)
    - put the key in a file and use the `-k` option
    - or set the `RBLX_OC_API_KEY` environment variable with your key as the value

#### CLI

The CLI is documented via help text:
> lune cloud-luau-task.luau -- --help

#### Example

you can find an example invocation and output in the [example](example) folder


# Nexus-Data-Store
Nexus Data Store is a successor to PlayerDataStore by stravant.
Nexus Data Store provides the same functionality of buffering
DataStore requests with the addition of cross-server communication
for changes using the `MessagingService`. Assuming proper listening
using `OnUpdate`, data of players in a game can be manipulates from
other servers within 2 seconds of the server making the change.

## Documentation
Documentation for the framework and utilities can be found
on the [GitHub pages](https://thenexusavenger.github.io/Nexus-Data-Store)
for this project. The [docs folder](docs) can also be used since it has all
of the markdown files.

## License
Nexus Instance is available under the terms of the MIT
Liscence. See [LICENSE](LICENSE) for details.


# Unofficial Sentry SDKs for Luau

A set of Sentry SDKs for various Luau environments. To simplify design and implementation, these SDKs are hand-translated
from the [Sentry JavaScript SDK](https://github.com/getsentry/sentry-javascript) (with deviations for Luau specific
constructs).

The Luau SDK is currently following vesion [`9.35`](https://github.com/getsentry/sentry-javascript/tree/9.35.0) of the
JavaScript SDK.

## Links

## Contents

- [Contributing](https://github.com/Neura-Studios/sentry-lua/blob/main/CONTRIBUTING.md)
- [Supported Platforms](#supported-platforms)
- [Installation and Usage](#installation-and-usage)
- [Other Packages](#other-packages)

## Supported Platforms

I'd like to support all major Luau runtimes with a high-level SDK, but only have the bandwidth to support the platforms
I use myself. If you'd like to add support for a Luau-based runtime that isn't already supported, please refer to
the [contribution guide](https://github.com/Neura-Studios/sentry-lua/blob/main/CONTRIBUTING.md)!

Please refer to the README and instructions of each SDK for more detailed information:

- [`sentry-roblox`](https://github.com/Neura-Studios/sentry-lua/tree/main/packages/roblox): SDK for Roblox

## Installation and Usage

To install an SDK, add the high-level package. For example, with [Wally](https://wally.run):

```toml
# wally.toml
[dependencies]
Sentry = "neura-studios/sentry-roblox@1.0.0"

```sh
wally install

Setup and usage of these SDKs always follows the same principle.

```lua
local Sentry = require(Path.To.Sentry)

Sentry.init({
  dsn = "__DSN__",
  -- ...
})

Sentry.captureMessage("Hello, world!")

## Other Packages

Besides the high-level SDKs, this repository contains shared packages, helpers and configuration used for SDK
development. If you're thinking about contributing to or creating a Luau-based SDK, have a look at the resources
below:

- [`sentry-core`](https://github.com/Neura-Studios/sentry-lua/blob/main/packages/core): The base for all
  Luau SDKs with type definitions and base classes.
- [`sentry-utils`](https://github.com/Neura-Studios/sentry-lua/blob/main/packages/utils): A set of helpers and
  utility functions useful for various SDKs.
- [`sentry-types`](https://github.com/Neura-Studios/sentry-lua/blob/main/packages/types): Types used in all
  packages.


e�������{��ٛ 3�_Z���X�mɭ�lWَ <��@��L�j�t��y~���t`��u�2w��J���L ֪�|�w�����<8 �?�` ��{�{���@h� p UD� �V�&��->��7t�ʂ�ڭ�!������"��ڙ:L�9�~��id�F: �Dbi�1 �� R:�����a�k6��̳{�O�>lCK�� ��\����ן�>�Fc���bL�EV�a��ː�I�.�3V~�����G�ӽ<�%Y����X�g��9 )���b�$��(��}�� 7������`� �f R��w逵OF�q��h�~�rd�IFZ ���g��pa�{F�>�X/u���>S��n���9� ,��D����>��1�U�j�n��ͧ��� ��ɼ��K'�9�H~�C�d�gf� �l|��t��O�IG�6�r�+L9fté|�=�-a��5�4tsy��@f�c�t�G���� �? �i�LFmո������J+Y+���z�{=� կ����ߢ���7�o���x�Ǉ����2�e�x�@�S�� �)� .�q���ֽ�EQ���ۢj�cR��I�mS��b�����,�`攡 � �����>��D�+eU@U��7���Z�=\ v<(u�w �J���*XO���ˤHƳS�,;\l=�� bєJM���n�J/ f* �R��ZW%[�]���nۮo�eA���ۂ�BW@��]� ����Y�g�ۖҮjd�ւKq�r�U˚�����jxf� }�ʢ�u'4�T. �쫦lZ^p�]ʍ�}�Kh;�Z١�P�nKݫF�� �e_v�R�muS.����oX'�R&�Xq���%��1~�3:u4���W� .�IO��s��D�� �AZ{F�Gi��,������t-��/��qD����h����tX*ۍ�>�Mh;Gt� ��3�$�F������;c�yFѼ�~����)�Ԉ b4�E�A��4��jD�4Z"�H���?B�e{�8����FIoM/��j������Pկ� \�;Z%�wFIK'-�E��Jx�D�X���D�� /�m[�֞��!��>�Î ؍a�S�-� ������0B-}��ψ�4�p�ae�G"�1;B{�2;�a�.��(�7�Q�z��a��F4 ��<� 1ʻ �m���ҫ������ �B��]�rU4@[��6�a��.t� h0f�N�k�*�� �M��~^/*���� ����g��ZU��+�Z�ʪ)AwJʶ�,*�Uն��t֨ �p�+����[����u ��zU�MY��)?� F�=l6:����;�tQz�8�7�?m8��[j {�o7�U�˰)o6{S��f z���O� A:���Vm%��MU��Y,&}��&��������m�۪���Q8���b�_a�C��y}>\hsD�����o�?�o�l���� m��h-q�>�4B �2,�M=�����>�3L|r�����t�Gq'������^h������vQ�E���f��,�Ǐ��� ��-7_�b�`�U'�}a��I��ɑ�2�}�6����()5@���� | ���s��%��R�9��/+/�H'ß�? ��7��x5�h��nכ��l��u�*J_%?�}e�O;�Jwu�;F���fӮ��ü��?�����]g�z>B �Eo����x� ���vJ�_q ��� E1:�Ƚ/�SDQ��^�Y*��;� ��{6)X5��0�O2���FO]�Y����X����S�$%�.,�+��9А�_��y�k[�Dv�6ʻ?� �|}�����]烆�;���;Ɏ�+����uf�% �8�9DY.�'�a�O�:`��J��E�tf�E�v�/�+]$(�"�Ɓ ȸ�8���V[�E,W�%&?a�L�?B�?�b��� `e2��!��'Cn�2�� ��׿x����9AD;5�� ����K�n�Ũ�rQ��V��ב�MG�.��|��nT� Ԇ�w G�j�³'�ɨ�3J~����(E��XZ���ӀS���F�Y��Q$9@���������` �nLꬠ���>�'�/֢i�rI �7h�A�\� /�j^�@��=�p]5���p��e��c%��?���~���KҸ�����佅�V ���韂��I0E��%������O��g,�jw���?9g� �2xS=�U~�m�d0[ف�g�=SF�Y�2��u��1��)�ȯ)���i4 P��w~I�8�λ���k�l�3.)��8�u�.��\����DN�a�rJ�6 � �����(��S��p��FJ,Ur4qC �B��:D��7ƽ ���������/�,ӈzc�>SK�ҵ�>dH��)x��і�U��G\���RR�cA�� + k-+ kr֒� «/-�|���oMMX��bd��N �wMM���8)�� �_Z��]S�B|)��5��_ FDy�Ԅ�o5#e�sJ��^W"g-�U^�9km��YIjE1'L�4-.H��� �l%�V9�uF4�9�kB뜓�� s�� ��4�9�-..7�� )ۼX�zMx��\�*/�6o������d9�y]�$u�~C����k\�~WJ�I!�_�_7���Ŝ�2�j%+I+rS�v�%��iJU��4o1'm���&%��[RS���m^�D�9g��9�}�=��$5����Tm�8�I%0�#DT���r.HU�@�f,H#�pA��S�#S�6�J��� "��!E}�0뽌��y��/-��������Z"�:�F��0#��+\���+\���)�e<.npK�7w�tђ��~�%)��"��M�> "��5nIÿM�QҖ���{�cMX� R���%i�����א �����4.I#����osNx%����f�)69�g������e� g����S� NX� JX{�kR���&U���� msVQ�W9o��^�:���"g���N��_��"5?OM�EKhe9i.%���-�� RT���bNZ��R�R�嗺9#U�W��KA�*7��F�x.� k +%k�� `0%�jR0�I����i������ u� qWT�ֲ@[轜.��`�`���T5n$�kR���-1�"eAx\���<¸��SRUyQVKL�aR59#���T��m��,�yA6 R7�����=V]��4/Z" )�h�ڗ/ �8�7�"��e�H����2���m<���<\tKJޥ�&� �P4\BZ�g�����;�p�zǳVhs<\l�W��bs̟�(P���)�92Fz; <���C���}"=H�9#ȏ5%, }Ԯ���ʹ8�G�i��GO�v� �M�V�ɡ���>��8m�O_??s���eg/���.(<��[�@ �'��6��z.��G ��T�X��>4&�n ��K� C,�cnx�*�_ڟ���dL��A�\ "�$��0M���)�����0��I��噎��e�y>7e��D� �)V�b����9���2 �OTF-� �N h-_��%�7�� s��1(s��!?���=ߛ����X����I����)�0EC0 r�g\�)a-����iv���e��34[\���f�)��B1�z[�<�`�1n�*mk8��!X�{m�%<�6˄,Hm܀�39��2����4�ߌ�T�?��m<����2J{j2������))��v.8�jw'<�5�����w � �ݠ� �gk�3��Y��!�� Vʂ=��� �|�y��G�pp!֢���f+� �E�|x���#��S��D+���0|��6s� ��\����s��/���x�K����)Ci��p���w{��q�|����+6�p*kG \L<� �$���*x��q?, ��u��g^r����!`J�Ϗ�3D����dSn�/�oHy�C�W��[�`����� u5$hg���Xw3�U!d㵎̰M~k��رP�ib�;s�j)��w�o]�!�d����e�\T �fk�(�4ޜo�&�K��!�M .�4 i�> �%��I���/� iP�1���Lg�a�11�� 4b����*i�Ǉ]J��G�q�1J�� Š��&���_��?�`�^�).7�v�o�� #Q��1B��A��[�*۾�Dz�Æ��u��z�� m|}��>�����*�UOۺ+���+��Fٷ��� � L�O2<�`dvæ�=m���ox�kSܴ� r�_���������׾.Q%�)G� V/,_��Ժ\ ���N��fّцwTF��Q�SO�Z9]���Ԗ/;r*�� �S��آ�E#hj����>�ʶ�7[���OQ��?�V��/@$:.絜޼���붵� ���r��~+i��纗�y)Ũd�(���~ �jy�1�VV�B�b� _��0<�2 ׫���U�az�A �W���*�3��l{x�R=���Q���T�+YƋA���Ϟy�~ĕ" E��x��pi-�v"���H�| �C�9����w��7/�ݯ�|�'#� �e�вh����XQʾ���ˢᕔLm�-4�p+�:&2|���� �S��a�!+�:&B S#�\\+���'�@���ɶ�� �Zf��� �`� E|�o������%�T��yە/��f�o�F)Z���,[)��*����Bv�<�J�c*���U�a� 1lW)+�U =��B>ٮB>y��ӝm��ࢫ�D��5��� )+E �Dt\~����Mb�>��i������.^}�}�oy��ɚռ�1�*��Zp&���+��[I%��p�x�g�f!��Y�$���J7R<��"�0���W�FK��H��O�=|�5����B��xk���P��q��7i���p�q�'aY$������� >�Wm�s�:>��e!ᗕIL=��~�vi�ռn�Ba\�gص}Y6����������h��%���p�/G�S���K���cѰ&Y������Z ��Dċ� �ߠC[t�X���T���`�<�� L�kZ#���&\}��7� ���w�:�8��0����G��9�� ����FN��T�4�w�Qvƚtd�;\�E�~�%Vγ 5�' � ��a�Nu �"V[ .-.� �q����K�����L������#H�>G��8��& ȁ!�8���5�]�[,���`"������8`�:i�P���a�~x�qsRE�U�"����������_�y�ꫣ���B�/+��$2N P�����'%,�<���A7 ˅"��J@q����1!��A}�Cj�h���9���c2_Ue�@_����+�^������A �dH�9�T��z~L�V����#��=,ö�8M�o �(=G�GOt�'!x��⬝�Ns��j���� �c��a���$�Ɔh�& ����ny�`��ĺ�U����7*q P��&�:+�"��J?kr�m�f��Ha��)�M�҈���cG&&��tkڪ ࡭�� �����x����1K��B�0�{*���vG ߉��UUG���8����ƫE�C ���m�Z��_�h��� �޶m;fX{ ��T��@F��a-�7�2� 5�*<��� N!����ͶTbk��8�v�v7��ġy�^dFC��m1i��$5F���k� �tHxY0V�.���/.� _b�<,6�k��F��� ��q��Kڑ*�-���]�q��ј��y�8-�͟�\�Y9���U�$n˛�f�� ����S�\���s��>�$��N~H�E[�$�zm�펋A��v�5}"��w1#ֻ �N s'<k��7vā�?��ю{�Ug)F}x��Ɯ!�*f ���܄ �I�|���U����!a����8����������A��1it+9�P �� �l1�m���:[D�� ��&�df|�j|+C�� Q3I) �.6�%� Ű+l���1�$�߉� 1Q��b��t�������Q�ݻ����k5�o�B�/+��$)�����HD�U�5�=��ԩ�ÍPk ����B �w]�ɹӳ�='ߑ;NT=�霗�F�T� �$:F `0� �4=C��AqT�_ţ�4h�D�⦅v7���04�1V,�{�Y$ډd�����@&>Ƽ'B/�S����5j{3�i�MZ�L�I��w��M$^��H`�u�A�%��� cB��; ]o�� %n\\}�ċ �����7���ͯ�k�n�o o� ~���$������Ҕ���ʡ(<��L�5���.����{�%�+��J77(ԡS3���m�ڼ�}����ſ�$�.�����b�s��A$:��Uy��$���>�u�H5Ht�ad�|{�B���������t2 W#de*��S#���v=)� �?z&Z'ŀ�Tc!��Uȋ�g� ��U�a� ��U�O��R �U=خB>y����c�@�1�DG !�㞧�F�K��)�y5����@���!�l�l�%�2=zq ���@"�0�Э�B� a^�y3���E�+P����S�!�����%T��X�s|`:���q���*�� ��C2n��j��x\⎒mŌ8�P���gk��b����S�� ��{�W���&�O���S����x��9Q�iآa>2�F���`�ِ�Q|�/����yf`����ȟo>4j��;�5柁 Y�x 2 �< G�d0/ ���PB�᤹W�� -�c /h� 5a@����(f} ��$bl[˗bF�����-^�ȱ�R�7]�X黝W�6 ���kڌ���8|��E�M�X1����r QF �7��D�8�gvT[Ɨ�>_ f��Ӫ��$�\6_=������B�/�r;6ߤ$�Qx��(4"��DT�dƔ|��~ ((L�F[97�v)��)�0�� �r���ބ�H�\��) w��=��gUC��ȗ83)߉]�3xÀD�:"�a���h6 �H�4xg�g�Ԣ�<��k 1*�ZF�f6��,�/[Z$��_>��)�}��uxa �ǫj!ᗕIL[^0����&��PB��3y7�6���g:�8#���:)񮝚�} ǘ��Jf�IS�6"̬3>!d�3��$���H|�8��t��6��K/��ۑ��}x��w���C���*O ���h&&��w���p��DT��"��l�O� �]�ɹꚁY�'0�z[��L��e.�����K�ʵHsu�#N_�%3�孛��S;,�8� �(�?3F=>��,��&>un�Vퟟ��2�Ќ���Ccߵ)��6 �e���[Q{�2!5�<� ��l�=�D��[W�m�]Wv���� ��H=�kd �;��L����3�K�����U�ʕ�'�g{Fo] %�⛱o`�'��\Ύ��F{�&��h�V����'���?B8���w��wo��1���盇���?}����ۦV��R�F�PRB����(�6��qJW�)�ƥె�w��RP��I�rë~�7'������\� �v�/�S�I��_����=W������OR+�6�����%��_?W ���i�*9����6��6x>��q�Nr��� T�`K�'����8�nѪ7'�� � ͧ��1G���C&�b�6�*�BkB�I(��%����71�D�qu�Q/c�7?�;�f�Z�*�z�H ��w�8�D�ap�ޡO��������L��z\\(�IgAI�]��{�4�͊đ�X ���#'ҧ���{�����ӗ���o��(�-$� �}&��M�2�/�]��]���7���~�:-�U⹳ t�>(��3�C��^��Np�a9�(�����40�:��/u�m���&�A-%�3 �Pe-�TN��!B8r���Oo�����:H�{2���B���x ���C;W���Kf͔�#�s%�����qn�����Ss�D��+��)�����<�eR��\y�f���\�S��0 �qT�{?2�� ��8¼��{���^�J�h�K�-�g����KC; ��� �X����nQ��j��s+l��� 7:؋ Fc�mČkٜ�NB�6��Do�]bR�x�gI[x��@W�/�C� e�#˯ � S���҂(�X�[ g�0��١�tf� p/��7},�Imй�9�~��{7��h#7�v��I� M5�l��#��$~��)�������<����r!.َlŏ�єp�� v�ֲ��� �a�.%wb� G�;���0� e�{�#㳶T*�3�(��.�)`�l��j�[�x���v� ��b�m��Ų79;�zk���'9�w�Z�-��%�Q�3�d �� ���9{���za*Ґ:���}�GH̓�{��w�� > ~�����^l���'y�<�eF�#- � !��h܀��'4+��2k� *5VܼG �'��v��@׌<�U���K�gD�g@lc, ������%[�>"��HU;�vD_R���^==�\Mzդw���� �fTU��9�uP �n�5ٵL0�` �J����`���(9c�����L2���4Ű��\ j�^t���!�h��)�RC�����8{M�����U�4 ��%�|�n�=�q2��!YF .�ZT� (N-;'��b^mJ�7��B�d���ؾ]��� �\\{ms��Ge\�R�D �;�ʤpء�,��7 *��H�C4�� ����~�W��rz�AD$����d:��dm��� �"0(�I"B�*I���Ŏ� �;[����iDk����# �RL� �A�Bؙ۽�ڒ/$�#���� T� ����B�G9�YG���:��q��Qޖ>l �!��yF��ܧ󀠸Zl|p���� �M�=2s���h���f���݆� \H� ��������4IL���b�_�� fI�ڴ�)���4 #xs�9`Y��z��%� 4ؿ���-5)�b�0߭K.��1~2��HU`�\6� �`h�-����+�?'t����J��, 0n'�~pL���3���L�}LN��Q����� ����vj�sb���隿�u���U��?�Q�U������@^ -�-����(;��Bu:�;�[���P�Ȟ�������&�sM-6���5��R��o��j��?d��Ѫ^�L��|_՗� o;n�a�G�� �?����ғR�a��q�_F`=�ڙ����J�H� �в�O��m܆p� k���N��}����KzF+a�d7Qt�k2m> G�ؾ`M&��l�`�1�� ̢{m�Ɣ1֓�"�P9�>M/�apΘ�� ��~��;�v��{yy��4 <�+�6�W4�j�8�{�0n�Wa�J뼫������_O~$v�% ���XU�m_��Il��c�$�p��Nw&���,�<�|�IM�d�L�fP��C��(~a��Gׄ���9!�1�$ rܯ 7�l�\ ڼ�`���u9�<�(�4�h�s��qD�B�y��� 7߲Zj�I�$ܬ ��y�}�ġ�����")JJW<���r�Ҕ߅���{'D!wu����T����/�$��U�f�&�'�O�K�����Q���;ġ��H+C aE�� Ǵ����+�Ʈ?K�a�����8Ӹ��k��A{&�cZ�r5 ]F|�AS�}W��� �P�ۀk'V *ld�B�S���[Jg9��Y��=���M��o��9d�}�T<�� ��m �L���q�1!� z �1����ٯs�V�C�I,���l�m��\B^ ����u��RB^�9I������+�q�������H���eI(7w�Vj �_'C��� MR���t��Z+l��>�� ���0y�Stc��8� �|�e(�*�,m�ZmY� g B�t���0��{�I���l\�-G�e�JGM�} �>\� |���??���\�{�Q�`�w8�Ζ�Z�-�\\} ���l�>f^ ���5v= �Iݥe�;��v���sR���=�hM�p6�˯b:-�>�V*t�3[U9� ПJ��Ѻt����l���2u�lGb�1�ȭtF��|- ٶ� ]o���@�)s�H�X���DFPo�� ��S�~^�zg� ao�k���A�� ����T�M� � �WG@A*Ó����q-ukl��{��7J���N�.��,?f��� ڣ��䨸η �)�>a������}��PK���`D+����]��yϰ3j��X��$3Yx 2�uI�Wi����rE�ŀ��{� �'����w¹a�R���.���Οޡ�Fp G���H��(ϳ=�A3f��!��4i�P���Ue�YnSj���Z�g�iu~@Q`�Y61���=BN�jǠ#��1�����L�VQp��-g^ �=�j9V'�dE�k 2W�'.��,�D�z����x@un���`�f����d|(�"�\���/��'������u�&�~3�s�]�0��^��z�\^<t<���AI�x��$Ka���r!Q����lW��<��L��!@���Ů.�n(o��rJ/� �0��@��9��0�� ����`��L#��ښW�|F �������j���� �D . V ��[�i����������.�Z�ΤW���x�ה���� �W����2vL�#JE|4�SVk��* ��d���6>�J�=�I�g��$�52�.��hi)Kߜ���G��[�̶�nӊ<����izd�����irK�2F (�D'����_����א~ɹ9�5RP/;��0�C�.�A,�A�z����띂��))��!'�0�P"z$���S�_�q(f����&�R�B����9~w�;����q� �R!��B� ,� tT B)C�澆�8���< G�H�tSN��W�T)M۳ Y�{?����P��! P������`|��_�}��ʹ�7��P^f���M ��Xװ��;M��!�óJ � !����&�7@���*6��� ��A�ꠁ����ކ[�r����:� 02����V�������Be�8ĿAAm�hA[�:Ȣ�z�^ �2.���){˴mj�O�ކ�"3�B`dY`8"� ї� 3������"VE=-9�)$�����e~QI��?����� �2C !����}����m�Ԣ:���b ���� 1 �X��m����~�oHvݝfa�Н�+9s5�X�"�t�E-bӑ�B�H��6M6>�y��@��M���A�\���b2:� :?Mn@�]�N�ad�t6Ȃ���Azq)Mʾ�X���l\z�_Ђc�3��n8l3pSo@���G��m��?����鴾�A������A��}�[�KoWd��� h z��+����(9Y��B���렄L, 3ux� <�[�%��b� ��*��< gQ%�2��,hIj�x��V�?��Э�Y W)��?@��#�? +�����{��8Ǡ��t�/h��������n�͖���0�f���B� �&�j�|���ns�~�������Ae/�:Ǹ�%�P;�O�>�Fndf���u%ؖ�7E8��f�,�=�e��98Qpe��S� �֨�g�Q헿, U��y��T�K�u�C4� �����=�{��ӻ��W�T^~��� �����[��ԕ������߿�ł��|7:�O� �;�[v��6 6�����)�u�K�J,w �*�¼e�n�f����Ov�X�d������h#gl�L2�H����}��W�`a�ˌ N�����巽�� �*��e���afwAmh�Zb�1K�y�n4B �}W,��X��I�� �N���o��E;-�=��[l�[ �C��\������_�4�^j�e������tD׉�=`�3':P��[���ڇk�� ��ٝ[B��$X� �K~1h � Z�:|� X�J��<�����-2��Kw_��N�ύԣ0�͂jE���y���8�vG��%Q�"��o�Q���n�������#'b��lx<(�ʃ�&��Z���"d* -7/<���O���u�8$��1�*2o��E��q�B�˦�0� �jx�[�t��%��=��pHAt��e :�@$M��z,�����N�]�D��#�lW�$��G/��Pl =xt�������� !b�7>J\�w� *xDl�'u ^�G��5&�#>@��Zd^X 蝣�p�N� ,�� (� ��$xe�8ȏ�ּ 4�9��/B Ф͸�..�����ڰ{W�7��_��`}w��r{�+�.I��;?W&ӣe}<��A'L �qF���Z���8�?톳v ��N>^:��䭸5]� ;���jN�h�O���c�I�?ܺ� �-�G�8<���W��h���0w��zg� ��vl/�g���;B��Ì���7���Q�u88�Q��_���o�.rr!�p�5�,�"h���/W�xYŏ/��j�v�{f�x���i�����y�9^;h*����[ ���c�&�>�l2+%���������� � �"��Cܳ�2H.�)�N�\�c�z��d�r�KZ-�1����Њr3kf5\�~ـ���X�f�z�҅�%������ ��q�,��f�R�h���ImE�ti� n�؉L��IiCD�����hozܑo����i�� ǰ�a 7���{h���Ĵb�j;��D9�JD��ɽ�m(7��y�k{A� ��Ĩ��RLDE���� _h�R������E��:��}h��&�� �G1O/n�$d0���%��y?��3�:#s���ˎ?��&7��}ϋB��c�ke�W����Y̟y#�d��!A�B�� N�mcA-bNΣ"F� B +'�z�2TIi�:)0 �F��ʠID��,��j_���2� #��o#��ӿ��/�/��%�kj��M��_U����r� Z�o��A�_^o���� �_�����˭�\ %��;}��H�t��M����l�?����(z�� S~��N��K�UdV �,9��482�+7N�8n��9A� Il���K��V��޹k�q�����(��2�.ȏj�TY�Z� )!��E�}��AOn�W|�5,��])I ��q� {N%Յ��;D*�; ���J��y��+�U~ )��5�f���N�϶�T�� ˓7�� �q��#��c� �P�W��b<�L""�i,T��5kD �S2�+�� "�~d����t���F� ��ϲ�ʋ(�r�Α���am,�k�x��go~�����@�o���*|�� .�ɭӛ���̇�}�A-K;Th�=�J� sI���@�( ���i� �/�٬�*�}�n�YtӰ|� Ni� � ��Ҙjt��\�Y� <�ǔ/z��!�'��}�1�L��+1��pZ��<"��m�� ;e� �w�i�M���B���h�.�A�w �ھ��� �s�)�DPU#�^X��$3�ￒ�����r}���F���O�VYslJ�$����v1�ښ�Tv;�l�]��@SQ蝏�DݡD:� ZOO݋M��`� '��S� )��N]j�W>�� �u�T�Gx="���b7�(���d�&����e�t��S��>kW 9v�I� ��C@L�Ȟc[$12�*[}�2�U��S5'��؀_m�.Qz@�-�t;��cE?��,3�,�gJ�=�YlN��TR�@�J�� ! ȥ2�E��vW��Mѷ1� �6���V?M�l� ��x-�4Jo�)cL�#�W�T��w����*��x�:`������o�,���ς��^fe�y�-�a9mk� �V�w��&�!fهr>"�cx��cl�X���B�)�˖��3>j_X=R�9 ��bޘ�v�ɱ�Țc!�-96��#`��#�n�n��6���5�U@�G"�c21�2�l9� /�>L&�ˀXY� _��V;�6�]P�w�Z,�׾Jeؘ4����-]�f�1 � �{�;�ѽYFy��[���pmA�z�rI���� ��T�"[��:gDб �pc^�f��-v\?�E=rH���Hϝ�Z���'��:��2˲�Q_���%��6�I۰�{�`[B�V6��7`Q ����u���-�0�i�:|`U��ڈ� j?���H���j��Ş�f�f:=5���� ��@MW�����ɍw�R*Z�dc������A���4��� q�q� $ڧ!əA�e3�)���T�U����<�lrf! �w�D�a���(��Tڟ #&@9��K�RD������s�|b����U�'J���_�0����;�Ke�k1�I�F�ehZ��R�Q�{��!��G���D��&��h��SE���2Qm$��k~����Ds�R�/!h�j-�s�q��]sBC/���'Ϭ��k4q:��> I�d� �l�{oq���0M&�UH����� �q��H�0��rE{���L�g`�LTؤښ�p }u��~��e�̔.-�u^�W2���6^{��q���HF�x���;���2�w�+FK3�M�a����=@�a{� &V^� � 4�IU�w�D��WZ��; Y�?|�H��Y��YD�S]�-{��y ������y��}PO2��N�l�:ܰ�z}��A������ucRd@�����z,Q &R^���3�IW�e�����<��� �t,÷)��ϔ��) �k8�S_l1m�S ![�j�hE0\� �w*"kՄ�_��fӅ���d/`�����Ʊ r�Ϳ�ElA�C�k�ʹ�{-��m\���8mNǰ�D�@�P� �EeZ�+��G �� �5*�V��S}� >D`z��l�0�q�H���@7�� �*���f"BtT�C��Pt �0�㴮0�� ��X�{�����S��B�@4H aٰ�N��:{�Ȃ*ՀF�;aڬ�e�^���pC,�� T=��-� @�5 �U_��NW�ؾ���3D�?��О&U�݌ �����!D"4JP�}F �� �� 6�~o"��a�e'D�%�|F�ኹw�������("����Hy:ς� �;��as �`�W�b��f��xf-2뾧ND����眛i����`���d����z�Y�s�v�E9�{R�j��F���H��s:L�m8�����ǂ=S򎁪�&Ȕ.�xB��Y#Z�ס�s�{�� ��^�BZ�e: �����7���Dbc�:�l�>sm]�� ��β�EX#�c0���mv��@q������3��|'C�؀d �X���M� ��t��ĚD��M� �Ϳ��T<얊WV��J%F2"9pI��a� ���k�T�򱷞S_2M.i���<,�G^SbJ1��\�I,�m�b��5�x�����>��F��ǂ%�T��k���w�! :Ya SDOm�iy>��=����� ӡ��@�| I&�p�d���#)�Ch%D[F�G��� <"�z�I�,[mG ��*��Y��j���5M&�|ܠ��I���t��`BLjg�CE(b D � ��&7 =�ٖS�<�n��K��h�j���|TS$Bu�?��~���Ƞ�l�����!TT���rH��e���g�ލ�S[.Gb������ ��k�VJ�Y��Ķ$BRa�.� ��� �4b�-�J�a�f]�XV��V�e[��S���� �v柪��s-q��e0�Xg;��bP�\q�(;-D� �4�~��J������\�:w{��s�� [���z�$��8��QBG���s�`�j8*��}�<��S������B��������VO��=I���y��8R��l�;#����j�����B� YV���&��qYܚY�4�O����r�PK�Jh��7�� �B�{��3+~�*x8�4��u @�ʲ@�4����C -G᠛��u�x+��oU1�c& ��6L��^d����.[%���i���gy�fsfYV�ϖF'�F�>��ţ����k�3� ���� h�S5�RQ.O>�y���j�i̲�N)�l�K��N�c:V�#uUw�6* ��[�bR��D��| ���D�A�x��u��H��|����y<��UF�i�H�cj�XC f�MV��AZ!�8�5��0[�Z-U�#��1�� �@B-�9 ��O���aZ�� U� �^ �B� p�lX"�� �N �B!*p�l�� V��DT4Zof�{�� qն��͸ֵ Tf�UaP� А S߉^�4�� &��F���g��zN� ���XP#|W�*Fs,$�0��d���f�*�sB%�$G�A�G|O��$���iR ��բ� ��Ҹ:� ����%����=����_�-12l[PŃ#�$����:����z��~�z�� m��P{����vv]��l��qz�ƽ�����Li䷉K�[x� ��%� �D���-nH=��o�rVIY���ex+Vl��� ���� �\�}���ϵ7m��㾲!Lv��~�$Q�< �l���d���Ls�Na.ߘ���Ou�7���-�Kl��g.#ed��<6�N㝺����6u�{hSs�y�)+@蔄���ܯ�Ϻ�\�,慄�u��X���mX욭|]�g$�8�Hs��ّ,+@dˡ��@�ӵ"���f��Xf�/e�3o�qÔ��wQ�K5��L�題�RO,ea�6 �U�b��r%��}՘[��4S��Xh��x���XL����j�1c�]�OfS��+ $� �A9 Sᢩ�D�\�`��I:?pSrj0�˒\��ʈ1�~DγU��c�����t0�=N�g��x�D#*b�j�٨�;G�����kcQ O'K����tX�/�2|�����Ẏޘp�r ����0?|�M�h>8� ���ibc��m��XV���ѡJ�>Y�3�!�1����M��m��w�R*�DńYbTi�ͺb���va���X� Y��Ib�\?%<Ƕ�B2dX��:�z����q��~�ei�:�{�b�B�ABsl���¯/�s�S��4l���F�%���Dm �2 �Ĕa`H�P������V�d�v�۾�B��.�n��Q�"���_��pw`as��|7�b����٣G�쓦$�]�̸�#�@���D�gM�E�) ��������������>� �M��q��&NH�w�X��{ �* � �AF�J&&���hl 遞�[ ��� O��W*�4��)�/�s�G��I踤ׅ�g������fM�_�ڜ���B2�-��y���}27��2S��G��u%����r�!$��%��!$s���b�P�� B����[vW@(���L�&�e2����� �ޓ3�p�m��8 �ʴ Zz�dL=]�Õ=�'����$Q�'{I��ٯ�KuV%���Pu?s�)���,����W:���[p�^��6M�1� �ia��7y QՖFb¹uX���h�~����Yq��X�mwa��Tm'u{"�9��H�1j ���^8��4�%�ni�0\^�P���&�+�:K%�B?�%����=|H��4���M��%$j�~�P]�9M��8�`�� N�9���KC\�@�����@�ݝd`~6(��Rk!� ��l8�� W����44�ڬ��  �(�Eg'HANm�=�F) ���n�����~ h�6̴��a�D�� ?"� Ѡ�6�7/���wV�$�r:B*����(Kb�&Q� �&�,hȆ�Ui{�&)H@� @���Uޕ�p�,z'�Ap���əDq�� �k1��� =]� ���~uvzء�� 첣OFL�*�Fl,�1�m�9ajd�j�����v�T���!��;g����]�B�I!U$8�P�`��$�ExrȻ5ScҨQ��� p�׭�� ?^�L���9���+�6��|��x�t�d��5\�z2�4��F�B ,���9~�� m� �D8�h�|T;*�^�^@bك/S0m*ZC�XPK5��cNʦ��b ����.������ 9��K�����q"*��{�Eթ� �ك{���z5���W�U���:x�� ?O������Ѓh������/����v��� 1��'7̛�c�h"1P�/ش�"ol�d1�/؎���:$iU�%Ƨ� ���ѣ���[|�p��VT�hc��G�r � �SD�)�MKC�!�t'D �G�E<%�eO����Z/����6јk8S������ �U�^������(F^c� 2�v����?��6a*�����p��`�}0G*�1x� z#�ْ� Q ���¶��)c� +"�,��+�W��a�o�at"�d �����I>���ZV�,+�kr ~�e�� e�Nr��4�9b�`�}UGT�'����&�:bh�� Ҩ�XKfl�s�N3�< 8�+I\���B_��qJ������2~����ܘ�8�8�A�tn(o� V5���i*D��Y���J) �����V,YzN����Ǖ*�ǝ0Ͱ�8�H��> 0_���h�Z$�����Yi0�D��a�W�oP���~�����W�rd�+�� c����r���� ��" ��o�%��e��Tb��l�.�����-�_bb �X�&�9�q+b���H -2C:�xu)�_�J��1��������#0:����C&��!'�-�*�n�Ter{� -�YZ7��$G��B-�~+��:� � � À X��(�N?9E(~���r�P��̟��Q8�ZMN9C���~q֮j�4=+�,�C���+g8U/KU���XrVE�6}�ZE(�4ݏx���lQ��n��:uL����� �E�&��jj�z���o�VT���ܺ����F�=&�6�lr�9�i2�Y�п�泫�sA%i0 ��*�d��8Ɵ��C� pq��5R��c��d��n�wW{d��!��� �SAk�����U)�b!Av��Tur�>���/��,�D�9K��0������cUr�a^� �r�.$Q����~_����I!�� ��ۧc �v��u,�� ˣ>l}`���'��v�l�ꗍ~6��'�|#�M��g�P��60I�QiSW����lr�$���Trҿ��3n��V�o�r�[dym��zT4_�wS%JyC&���d(׿~�� e�}�"�g�<��r�݁��_����>�M�� dy������OQ���U��i�0�����c�f�vMw��Ew � �(��k���QӃ��<����ټ��^6�ٽ


# Zappy
Zappy is a Roblox Studio Plugin implementation of [Zap's Playground](https://zap.redblox.dev/playground.html). It works by taking the WASM from the playground and translating it to Luau using [Wasynth](https://github.com/Rerumu/Wasynth).
The UI is made with a lightly modified version of [PluginEssentials](https://github.com/mvyasu/PluginEssentials/) to give it a look that's consistent with Studio's built-in plugins.

## Features
With [valid IDL](https://zap.redblox.dev/intro/getting-started.html#writing-your-first-network-description) input, you will get the client and server code emitted by Zap after clicking Generate. You also have the option of having the code be automatically sent to your game to save you the hassle of copy-pasting. Server code can be exported to `ServerScriptService` and client (+ tooling, [if you have it enabled](https://zap.redblox.dev/config/options.html#tooling)) code can be exported to `ReplicatedStorage`.

![image](https://github.com/user-attachments/assets/b09d2a1f-722a-4d1f-bab7-4a5e181a388b)

The plugin contains the same diagnostics as the playground, so you will still benefit from the same helpful error messages that Zap has to offer.

![image](https://github.com/user-attachments/assets/129d4350-e556-4986-8859-e829e6ca874d)

## Installing

1. Build or download the plugin from [GitHub Releases](https://github.com/Ultrasonic1209/Zappy/releases/latest).
2. Open your plugins folder (WIN+R then enter `%localappdata%\Roblox\Plugins`, or click **Plugins Folder** inside Studio's Plugins ribbon).
3. Copy the plugin you downloaded to your plugins folder.
4. Relaunch Studio.

Zappy should then appear along with all your other plugins.

## Building

1. Install Rojo with [Aftman](https://github.com/LPGhatguy/aftman) (`aftman install`), [Rokit](https://github.com/rojo-rbx/rokit) (`rokit install`) or [manually](https://rojo.space/docs/v7/getting-started/installation/).
2. Run `rojo build ./build.project.json --output ZappyPlugin.rbxm`.

## Logo
Zappy's logo was sourced from [Twitter](https://github.com/twitter/twemoji/blob/master/assets/svg/26a1.svg) and is under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license. The colour of the logo was changed to a blue hue.


# PlayerState

A modern player data management system for Roblox games featuring ProfileService integration, replica-based state replication, automatic leaderboards, and batched updates.

📖 **[Full Documentation](https://playerstate.netlify.app)** - Complete API reference, guides, and examples

## Features

- ✅ **ProfileService Integration** - Secure data persistence with session locking
- ✅ **Real-time Replication** - Automatic client data sync with change tracking
- ✅ **Built-in Leaderboards** - Automatic stat tracking and rankings
- ✅ **Batched Updates** - Efficient DataStore writes
- ✅ **Path-Based Access** - Access nested data with dot notation
- ✅ **Type-Safe API** - Full Luau type coverage
- ✅ **Performance Optimized** - Intelligent caching and rate limiting

## Installation

### Option 1: Download Model (.rbxm)

**Recommended for beginners** - Download the latest `.rbxm` file from [Releases](https://github.com/bellaouzo/PlayerState/releases/) and drag it into your ReplicatedStorage.

### Option 2: Manual Installation

*Clone or download this repository*

git clone https://github.com/bellaouzo/PlayerState.git

### Option 3: GitHub Cloning Plugin

Use the [GitHub Cloning Plugin](https://devforum.roblox.com/t/github-cloning-plugin/31267) to clone directly into Roblox Studio.

## Quick Start

### 1. Configure Your Data Structure

Edit `DefaultData.luau` to define your player data:

```lua
return {
    Coins = 0,
    Level = 1,

    Inventory = {
        Weapons = {},
        Items = {}
    },

    leaderstats = {
        ["💰 Coins"] = "Coins",
        ["⭐ Level"] = "Level"

### 2. Configure Settings

Edit `PlayerStateConfig.luau` for your game settings:

```lua
local Config = {
    Server = {
        DataStore = {
            Name = "PlayerData_v1",
            Scope = "Production"
        },

        Leaderboard = {
            Enabled = true,
            TrackedStats = {"Coins", "Level"}

return Config

### 3. Initialize on Server

```lua
local Players = game:GetService("Players")
local PlayerState = require(ReplicatedStorage.PlayerState.PlayerStateServer)

Players.PlayerAdded:Connect(function(player)
    local success = PlayerState.Init(player)

    if success then
        print(`[PlayerState] Loaded data for {player.Name}`)
    end
end)

Players.PlayerRemoving:Connect(function(player)
    PlayerState.SaveData(player)
end)

### 4. Use on Client

```lua
local PlayerState = require(ReplicatedStorage.PlayerState.PlayerStateClient)

-- Wait for data to load
PlayerState.IsReady() -- Check if ready

-- Listen for changes
PlayerState.OnChanged("Coins", function(newValue, oldValue)
    print(`Coins: {oldValue} → {newValue}`)
end)

## API Reference

### Server API

#### Basic Operations

```lua
-- Set a value
PlayerState.Set(player, "Coins", 100)

-- Get a value
local coins = PlayerState.Get(player, "Coins")

-- Access nested data with dot notation
PlayerState.SetPath(player, "Inventory.Weapons[1].Durability", 95)
local durability = PlayerState.GetPath(player, "Inventory.Weapons[1].Durability")

#### Increment/Decrement

```lua
-- Increment by 1
PlayerState.Increment(player, "Coins")

-- Increment by custom amount
PlayerState.Increment(player, "Level", 5)

-- Decrement
PlayerState.Decrement(player, "Coins", 10)

#### Arrays

```lua
-- Add item to array
PlayerState.AddToArray(player, "Inventory.Weapons", {
    Id = "sword_001",
    Name = "Iron Sword",
    Damage = 10
})

-- Update array item
PlayerState.UpdateArrayItem(player, "Inventory.Weapons", 1, {
    Id = "sword_001",
    Name = "Iron Sword",
    Damage = 15
})

-- Remove from array
PlayerState.RemoveFromArray(player, "Inventory.Weapons", 1)

#### Dictionaries

```lua
-- Set dictionary value
PlayerState.SetInDict(player, "Inventory.Items", "health_potion", 5)

-- Get dictionary value
local quantity = PlayerState.GetFromDict(player, "Inventory.Items", "health_potion")

-- Remove from dictionary
PlayerState.RemoveFromDict(player, "Inventory.Items", "health_potion")

#### Batch Operations

```lua
-- Update multiple values at once
PlayerState.BatchSetValues(player, {
    {path = "Coins", value = 200},
    {path = "Level", value = 5},
    {path = "Experience", value = 1000}
})

-- Manually flush batch (normally auto-flushes)
PlayerState.FlushBatch(player)

#### Leaderboards

```lua
-- Get top players
local topPlayers = PlayerState.GetLeaderboard("Coins", 10)
-- Returns: {{userId = 123, score = 5000, rank = 1}, ...}

-- Get player's rank
local rank = PlayerState.GetPlayerRank(player, "Coins")

#### Events

```lua
-- Before data is saved
PlayerState.BeforeSave:Connect(function(player, data)
    -- Validate or modify data before saving
end)

-- When player data loads
PlayerState.ProfileLoaded:Connect(function(player, data)
    print("Data loaded for", player.Name)
end)

-- When player leaves
PlayerState.ProfileUnloaded:Connect(function(player, data)
    print("Data saved for", player.Name)
end)

#### Utilities

```lua
-- Get all player data
local allData = PlayerState.GetAll(player)

-- Check if data is ready
if PlayerState.IsPlayerDataReady(player) then
    -- Safe to use data
end

-- Save data manually
PlayerState.SaveData(player)

-- Admin: Wipe player data
PlayerState.WipePlayerData(player)

### Client API

#### Reading Data

```lua
-- Get value
local coins = PlayerState.Get("Coins")

-- Get nested value
local durability = PlayerState.GetPath("Inventory.Weapons[1].Durability")

-- Get dictionary value
local quantity = PlayerState.GetFromDict("Inventory.Items", "health_potion")

-- Get all data
local allData = PlayerState.GetAll()

#### Listening for Changes

```lua
-- Listen to any path
PlayerState.OnChanged("Coins", function(newValue, oldValue)
    print(`Coins changed: {oldValue} → {newValue}`)
end)

PlayerState.OnChanged("Inventory.Weapons", function(newValue, oldValue)
    print("Weapons updated!")
end)

#### Utilities

```lua
-- Check if data is ready
if PlayerState.IsReady() then
    -- Data loaded
end

-- Clear cache (advanced)
PlayerState.ClearCache()

-- Get underlying Replica (advanced)
local replica = PlayerState.GetReplica()

## Attribution

This library uses the following third-party dependencies (included):

- **ProfileStore** by MAD STUDIO (loleris) - DataStore session management
- **Replica** by MAD STUDIO (loleris) - State replication system
- **Maid, Signal, Remote, RateLimit** by MAD STUDIO - Supporting utilities

See individual file headers for full attribution details.

## License

MIT License - See LICENSE file for details

## Support

- 💬 [DevForum Post](https://devforum.roblox.com/t/your-post-link)
- 🐛 [Report Issues](https://github.com/YOURUSERNAME/PlayerState/issues)


# PlayerState Documentation

A robust, type-safe player data management system for ROBLOX games using ProfileStore and Replica services.

> **Created by [bellaouzo](https://www.roblox.com/users/18782534/profile)** \- A powerful solution for managing player data in ROBLOX games that utilizes Replica and ProfileStore.

**Optional support** — This documentation is free. **You do not need to pay anything** to use PlayerState or these docs. If you would like to leave a tip, it is genuinely appreciated, but **completely optional** and never expected.

In PayPal, use **Send** and enter **joelbriskpro@gmail.com**. [Open PayPal](https://www.paypal.com)

## Features

🖥 Server  💻 Client

Get: 2.2M/secSet: 478K/secGetPath: 2.1M/secSetPath: 488K/sec

Get: 3.5M/secGetPath: 2.77M/secCached: 3.2M/secGetAll: 4.5M/sec

  * **Type Safety**: Full TypeScript-style type annotations
  * **Real-time Sync**: Automatic client-server data synchronization
  * **Persistent Storage**: ProfileStore integration for reliable data persistence
  * **Path-based Access**: Easy nested data manipulation with dot notation
  * **Event System**: Listen for data changes with callbacks
  * **Array & Dictionary Operations**: Built-in helpers for collections

## Quick Start

Server SetupClient Usage

lua

    local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateServer)

    -- ⚠ CRITICAL: You MUST have this PlayerAdded connection!
    game.Players.PlayerAdded:Connect(function(player)
        PlayerState.Init(player) -- ← ESSENTIAL - PlayerState won't work without this!
    end)

    -- Set player coins (after Init)
    PlayerState.Set(player, "Coins", 1000)

1
2
3
4
5
6
7
8
9

lua

    local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateClient)

    -- Get player coins
    local coins = PlayerState.Get("Coins")

    -- Listen for changes
    PlayerState.OnChanged("Coins", function(newValue, oldValue)
        print("Coins updated:", newValue)
    end)

1
2
3
4
5
6
7
8
9

Ready to Go!

The Roblox model includes all dependencies - just download and use!

## Architecture

PlayerState uses a **server-authoritative** architecture:

**Key Components:**

  * **Server**: Manages all data modifications using ProfileStore for persistence
  * **Client**: Read-only access with real-time updates via Replica
  * **Synchronization**: Automatic bi-directional sync between server and clients

## Dependencies

  * [ProfileStore](https://github.com/MadStudioRoblox/ProfileStore) \- Data persistence
  * [Replica](https://github.com/MadStudioRoblox/Replica) \- Real-time synchronization
  * DefaultData module - Data schema definition
  * PlayerStateConfig module - Centralized configuration settings

NOTE

All dependencies are included in the Roblox model! No manual installation needed.

## Getting Started

  1. **[Installation](https://playerstate.netlify.app/installation)** \- Add to your project
  2. **[Setup](https://playerstate.netlify.app/setup)** \- Configure the module and PlayerStateConfig settings
  3. **[API Reference](https://playerstate.netlify.app/api/)** \- Complete function documentation
  4. **[Examples](https://playerstate.netlify.app/examples)** \- Common usage patterns

Have existing player data?

Legacy Data Migration lets you import from your old DataStore during profile load. Opt-in, configurable, and safe to test. See the [Migration Guide](https://playerstate.netlify.app/migration).

PlayerState V1

This is **PlayerState V1** \- production-ready with active development. Updates include new features, optimizations, and community-requested improvements. Your feedback shapes future versions!

## Recent updates

  * **New**: Legacy Data Migration — import from an old DataStore during profile load. Opt-in, multiple strategies (Auto, Overwrite, KeepNew, Manual), and `MigrationResult` signal for logging.
  * **New**: Session (non-persistent) data — runtime-only roots like `session` that replicate during play but never persist.
  * **New**: `RuntimeNonPersistentRoots` config for defining which top-level keys are stripped before save.
  * **New**: `MigrationResult` event — fires when migration runs for a player (applied, keyUsed, reason, etc.).


# Client Functions

Client-side PlayerState API for data access and change listeners.

Client Only - READ-ONLY

**Client scripts only.** These functions are **READ-ONLY** \- no data modification!
Need to change data? Create your own RemoteEvents with server validation.

## Data Access Functions

### Get()

`Get(key) → any`

Gets a top-level data value. Automatically waits for data to load with built-in caching for performance.

**Parameters:** `key: string`
**Returns:** `any` \- The value, or `nil` if not found

Example

lua

    local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateClient)

    local coins = PlayerState.Get("Coins")
    local level = PlayerState.Get("Level")
    print("Player has", coins, "coins and is level", level)

1
2
3
4
5

Note

Automatically waits for server data to sync with built-in caching for performance

### GetPath()

`GetPath(path) → any`

Gets a nested data value using dot notation. Automatically waits for data to load with optimized nested value caching.

**Parameters:** `path: ValuePath`
**Returns:** `any` \- The value, or `nil` if not found

Example

lua

    local likes = PlayerState.GetPath("Plot.Likes")
    local musicEnabled = PlayerState.GetPath("Settings.MusicEnabled")
    local highScore = PlayerState.GetPath("Stats.HighScore")

    print("Plot has", likes, "likes")
    print("Music is", musicEnabled and "enabled" or "disabled")

1
2
3
4
5
6

Note

Handles nested data structures safely with optimized caching

### GetAll()

`GetAll() → PlayerData?`

Gets all player data. Automatically waits for data to load.

**Parameters:** None
**Returns:** `PlayerData?` \- Complete data table, or `nil` if not loaded

Example

lua

    local allData = PlayerState.GetAll()
    if allData then
        print("Complete player data:", allData)

        -- Iterate through top-level keys
        for key, value in pairs(allData) do
            print(key, "=", value)
        end
    end

1
2
3
4
5
6
7
8
9

Note

Returns a reference to the actual data (do not modify!)

Read-Only Data

Don't modify the returned data! It's read-only on the client.

### GetFromDict()

`GetFromDict(dictPath, key) → any`

Gets a value from a dictionary using a key with automatic type conversion and caching.

**Parameters:** `dictPath: ValuePath`, `key: string | number`
**Returns:** `any` \- The value, or `nil` if not found

Example

lua

    local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateClient)

    -- Get building data
    local houseData = PlayerState.GetFromDict("Plot.Buildings", "House")
    if houseData then
        print("House level:", houseData.Level)
        print("House material:", houseData.Material)
    end

    -- Get with numeric key (automatically converted to string)
    local equippedItem = PlayerState.GetFromDict("Inventory.Equipped", 1)

    -- Get setting value
    local quality = PlayerState.GetFromDict("Settings", "GraphicsQuality")
    print("Graphics quality:", quality or "Default")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Note

Automatically converts number keys to strings for consistency

## 🏆 Leaderboard Functions

### GetLeaderboardInfo()

`GetLeaderboardInfo(statName) → LeaderboardInfo?`

Gets your **current** leaderboard info (rank + score) for a tracked stat.

**Parameters:** `statName: string`
**Returns:** `LeaderboardInfo?` \- `{ rank: number?, score: number?, statName: string }`, or `nil` if unavailable

Example

lua

    local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateClient)

    local info = PlayerState.GetLeaderboardInfo("Coins")
    if info then
        print(`Coins leaderboard: rank={info.rank}, score={info.score}`)
    else
        print("No leaderboard info (not tracked / not enabled / not ready yet)")
    end

1
2
3
4
5
6
7
8

Notes

  * Requires server leaderboard to be enabled and the stat to be tracked in `PlayerStateConfig.Server.Leaderboard.TrackedStats`.
  * The client reads from the server-synced `_LeaderboardRanks` table (replicated to the client).
  * `_LeaderboardRanks` is **temporary display data** and is treated as non-persistent (it will not be saved into player data).
  * **Score feels instant**: when a tracked stat changes, the synced `score` updates right away.
  * **Rank updates on the normal refresh cycle** (based on the server sync interval).

### GetLeaderboard()

`GetLeaderboard(statName, limit?) → {LeaderboardEntry}`

Gets the ranked leaderboard entries for a tracked stat. Automatically waits for data to load.

**Parameters:** `statName: string`, `limit: number?` (optional — max entries to return; omit for all)
**Returns:** `{LeaderboardEntry}` \- Array of `{ userId: number, score: number, rank: number }`, or `{}` if unavailable

Example

lua

    local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateClient)

    local top10 = PlayerState.GetLeaderboard("Coins", 10)
    for _, entry in top10 do
        print(`#{entry.rank}: User {entry.userId} — {entry.score} coins`)
    end

    local allEntries = PlayerState.GetLeaderboard("Coins")
    print("Total entries:", #allEntries)

1
2
3
4
5
6
7
8
9

Notes

  * Reads from the server-synced `_Leaderboards` table (replicated to the client).
  * `limit` is clamped to the actual number of entries; invalid or non-positive `limit` returns all entries.
  * Returns an empty table if the stat is not tracked, leaderboards are disabled, or data is not ready.

## Status Functions

### IsReady()

`IsReady() → boolean`

Checks if player data is ready and replica is active.

**Parameters:** None
**Returns:** `boolean` \- True if data is ready, false otherwise

Example

lua

    if PlayerState.IsReady() then
        local coins = PlayerState.Get("Coins")
        print("Player has", coins, "coins")
    else
        print("Data not ready yet...")
    end

1
2
3
4
5
6

Note

Optional check - data access functions already wait automatically

## Cache Management Functions

### ClearCache()

`ClearCache() → void`

Manually clears all internal caches. Useful for memory management or debugging.

**Parameters:** None
**Returns:** Nothing

Example

lua

    -- Clear cache when switching players or for debugging
    PlayerState.ClearCache()

    -- Cache will rebuild automatically on next access
    local coins = PlayerState.Get("Coins")

1
2
3
4
5

Note

Manual cache management with automatic rebuild on next access

## Change Listener Functions

### OnChanged()

`OnChanged(pathOrKey, callback) → ReplicaClient.Connection?`

Listens for changes to a specific data path or key with enhanced change information.

**Parameters:** `pathOrKey: string`, `callback: (newValue: any, oldValue: any, changeInfo: ChangeInfo | {string}?) -> ()`
**Returns:** `ReplicaClient.Connection?` \- Connection to disconnect the listener

Basic ListeningNested PathsGlobal Change Listener - NEW

lua

    -- Listen to top-level changes
    local coinsConnection = PlayerState.OnChanged("Coins", function(newValue, oldValue)
        print("Coins changed from", oldValue, "to", newValue)
        updateCoinsUI(newValue)
    end)

1
2
3
4
5

lua

    -- Listen to nested changes
    local likesConnection = PlayerState.OnChanged("Plot.Likes", function(newValue, oldValue, pathInfo)
        print("Likes changed:", newValue)
        if pathInfo then
            print("Full path:", table.concat(pathInfo, "."))
        end
        updateLikesDisplay(newValue)
    end)

1
2
3
4
5
6
7
8

lua

    -- Listen to ALL changes with detailed action info
    local allChangesConnection = PlayerState.OnChanged(".", function(newValue, oldValue, changeInfo)
        if changeInfo and changeInfo.action then
            local pathString = table.concat(changeInfo.path, ".")
            print(`{changeInfo.action}: {pathString} = {newValue} (was {oldValue})`)

            -- Handle different action types
            if changeInfo.action == "TableInsert" then
                print("Item added at index:", changeInfo.index)
            elseif changeInfo.action == "TableRemove" then
                print("Item removed from index:", changeInfo.index)
            end
        end
    end)

1
2
3
4
5
6
7
8
9
10
11
12
13
14

**Enhanced Change Information:**

  * `ChangeInfo` type provides detailed action information
  * Support for `"Set"`, `"SetValues"`, `"TableInsert"`, `"TableRemove"` actions
  * Index information for array operations
  * Full path information for all changes

Note

Callbacks fire immediately when data changes with automatic cache invalidation

Managing Connections

lua

    -- Store connections for cleanup
    local connections = {}

    connections.coins = PlayerState.OnChanged("Coins", updateCoinsUI)
    connections.level = PlayerState.OnChanged("Level", updateLevelUI)

    -- NEW: Global listener with action handling
    connections.all = PlayerState.OnChanged(".", function(newValue, oldValue, info)
        if info and info.action == "TableInsert" then
            print("Array item added!")
        end
    end)

    -- Cleanup when done
    for name, connection in pairs(connections) do
        if connection then
            connection:Disconnect()
        end
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

## Advanced Functions

### GetReplica()

`GetReplica() → ReplicaInstance?`

Gets the raw Replica instance for advanced operations.

**Parameters:** None
**Returns:** `ReplicaInstance?` \- Replica instance, or `nil` if not loaded

Example

lua

    local replica = PlayerState.GetReplica()
    if replica then
        -- Access raw Replica API
        print("Replica data:", replica.Data)
        print("Replica tags:", replica.Tags)

        -- Advanced change listener
        replica:OnChange(function(action, path, param1, param2)
            print("Raw change:", action, table.concat(path, "."))
        end)
    end

1
2
3
4
5
6
7
8
9
10
11

Note

Provides access to the underlying Replica object with enhanced validation

## Usage Examples

### UI Updates with Enhanced Change Handling

Example

lua

    local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateClient)

    -- UI Elements
    local coinsLabel = gui.CoinsLabel
    local levelLabel = gui.LevelLabel
    local experienceBar = gui.ExperienceBar

    -- Initial UI setup (data access automatically waits)
    local function updateUI()
        coinsLabel.Text = tostring(PlayerState.Get("Coins") or 0)
        levelLabel.Text = "Level " .. tostring(PlayerState.Get("Level") or 1)

        local exp = PlayerState.Get("Experience") or 0
        local maxExp = (PlayerState.Get("Level") or 1) * 100
        experienceBar.Size = UDim2.new(exp / maxExp, 0, 1, 0)
    end

    -- Listen for changes with enhanced info
    local connections = {}

    connections.coins = PlayerState.OnChanged("Coins", function(newValue, oldValue, info)
        coinsLabel.Text = tostring(newValue or 0)

        -- Show gain/loss animation
        if oldValue and newValue > oldValue then
            showGainAnimation(newValue - oldValue)
        end
    end)

    connections.level = PlayerState.OnChanged("Level", function(newValue, oldValue, info)
        levelLabel.Text = "Level " .. tostring(newValue or 1)
        updateUI() -- Recalculate experience bar

        if oldValue and newValue > oldValue then
            showLevelUpEffect()
        end
    end)

    -- NEW: Global change listener for debugging
    connections.debug = PlayerState.OnChanged(".", function(newValue, oldValue, info)
        if info and info.action then
            print(`[DEBUG] {info.action} at {table.concat(info.path, ".")}`)
        end
    end)

    -- Initial update
    updateUI()

    -- Enhanced cleanup function
    local function cleanup()
        for name, connection in pairs(connections) do
            if connection and typeof(connection) == "table" and connection.Disconnect then
                connection:Disconnect()
            end
        end
        connections = {}
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

### Settings Management

Example

lua

    -- Settings UI manager with caching
    local SettingsManager = {}

    local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateClient)
    local SoundService = game:GetService("SoundService")

    function SettingsManager.init()
        -- Apply initial settings (automatically waits for data)
        local musicEnabled = PlayerState.GetPath("Settings.MusicEnabled")
        local soundEnabled = PlayerState.GetPath("Settings.SoundEnabled")

        SoundService.AmbientReverb = musicEnabled and Enum.ReverbType.City or Enum.ReverbType.NoReverb
        SoundService.Volume = soundEnabled and 1 or 0

        -- Listen for changes
        PlayerState.OnChanged("Settings.MusicEnabled", function(enabled)
            SoundService.AmbientReverb = enabled and Enum.ReverbType.City or Enum.ReverbType.NoReverb
        end)

        PlayerState.OnChanged("Settings.SoundEnabled", function(enabled)
            SoundService.Volume = enabled and 1 or 0
        end)
    end

    return SettingsManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

### Inventory Display with Array Change Handling

Example

lua

    -- Inventory UI manager with enhanced change detection
    local InventoryUI = {}

    local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateClient)

    -- UI elements
    local inventoryFrame = gui.InventoryFrame
    local itemTemplate = inventoryFrame.ItemTemplate

    function InventoryUI.updateDisplay()
        -- Clear existing items
        for _, child in pairs(inventoryFrame:GetChildren()) do
            if child ~= itemTemplate and child:IsA("Frame") then
                child:Destroy()
            end
        end

        -- Get inventory data
        local inventory = PlayerState.GetPath("Inventory") or {}

        -- Create UI elements for each item
        for index, item in ipairs(inventory) do
            local itemFrame = itemTemplate:Clone()
            itemFrame.Name = "Item" .. index
            itemFrame.Visible = true
            itemFrame.Parent = inventoryFrame

            -- Update item display
            itemFrame.ItemName.Text = item.Name or "Unknown"
            itemFrame.Rarity.Text = item.Rarity or "Common"
            itemFrame.LayoutOrder = index
        end
    end

    function InventoryUI.init()
        -- Initial display (automatically waits for data)
        InventoryUI.updateDisplay()

        -- NEW: Enhanced inventory change listener
        PlayerState.OnChanged("Inventory", function(newValue, oldValue, info)
            InventoryUI.updateDisplay()

            -- Handle specific actions
            if info and info.action == "TableInsert" then
                showItemAddedEffect(info.index)
            elseif info and info.action == "TableRemove" then
                showItemRemovedEffect(info.index)
            end
        end)
    end

    return InventoryUI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

## Error Handling

### Safe Data Access with Readiness Checks

Example

lua

    -- Safe data access (automatically waits for data)
    local function getPlayerData()
        return {
            coins = PlayerState.Get("Coins") or 0,
            level = PlayerState.Get("Level") or 1,
            likes = PlayerState.GetPath("Plot.Likes") or 0
    end

    -- Safe nested access with validation
    local function getInventoryItem(index)
        local inventory = PlayerState.GetPath("Inventory")
        if not inventory or typeof(inventory) ~= "table" then
            return nil
        end

        return inventory[index]
    end

    -- Check if specific data exists
    local function hasValidInventory()
        local inventory = PlayerState.GetPath("Inventory")
        return inventory and typeof(inventory) == "table" and #inventory > 0
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

### Enhanced Connection Management

Example

lua

    -- Advanced connection management system
    local ConnectionManager = {}
    local activeConnections = {}

    function ConnectionManager.add(name, connection)
        -- Cleanup existing connection
        if activeConnections[name] then
            if typeof(activeConnections[name]) == "table" and activeConnections[name].Disconnect then
                activeConnections[name]:Disconnect()
            end
        end

        activeConnections[name] = connection
    end

    function ConnectionManager.remove(name)
        local connection = activeConnections[name]
        if connection and typeof(connection) == "table" and connection.Disconnect then
            connection:Disconnect()
        end
        activeConnections[name] = nil
    end

    function ConnectionManager.cleanup()
        for name, connection in pairs(activeConnections) do
            if connection and typeof(connection) == "table" and connection.Disconnect then
                connection:Disconnect()
            end
        end
        activeConnections = {}
    end

    -- Usage with enhanced error handling
    ConnectionManager.add("coins", PlayerState.OnChanged("Coins", function(newValue, oldValue, info)
        if typeof(newValue) == "number" then
            updateCoinsDisplay(newValue)
        end
        else
            warn("Invalid coins value received:", newValue)
        end
    end))

    -- Cleanup when done
    ConnectionManager.cleanup()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

## Performance Tips

Caching & Optimization

**Automatic Caching:** Frequently accessed values are cached automatically for performance **Cache Management:** Use `ClearCache()` only when necessary (debugging, memory concerns) **Readiness Checks:** Use `IsReady()` before bulk operations to avoid unnecessary waits **Specific Listeners:** Use specific paths instead of global `"."` listeners when possible **Connection Cleanup:** Always disconnect listeners to prevent memory leaks

Best Practices

**Fallbacks:** Always provide fallbacks when accessing data that might not exist **Auto-Waiting:** Data access automatically waits - no manual waiting required **Conditional Logic:** Use `IsReady()` only for conditional logic or optimization **Listener Management:** Disconnect listeners when UI elements are destroyed **Path Specificity:** Use specific paths instead of listening to all changes **Action Handling:** Handle different action types in global listeners for better UX **Read-Only Data:** Don't modify returned data - it's a reference to actual data


# API Reference

PlayerState provides separate APIs for server and client environments with enhanced type-safe interfaces and advanced features.

## Overview

  * **[Server Functions](https://playerstate.netlify.app/api/server)** \- Data manipulation, batch operations, and management (server-only)
  * **[Server Events](https://playerstate.netlify.app/api/server#events)** \- BeforeSave, ProfileLoaded, ProfileUnloaded, MigrationResult
  * **[Client Functions](https://playerstate.netlify.app/api/client)** \- Data access, change listeners, and caching (client-only)
  * **[Types](https://playerstate.netlify.app/api/types)** \- Enhanced TypeScript-style type definitions

## 🆕 Latest Features

### Server Enhancements

  * **🚀 Batch Operations** \- `BatchSetValues()` and `FlushBatch()` for high-performance bulk updates
  * **🏆 Automatic Leaderstats** \- Integrated leaderstats creation and real-time synchronization
  * **🔧 Cache Management** \- `ClearPathCache()` with automatic size limits
  * **📦 Array & Dictionary Operations** \- Full CRUD operations with validation
  * **🔄 Event System** \- `BeforeSave`, `ProfileLoaded`, `ProfileUnloaded`, `MigrationResult` for data lifecycle and migration

### Client Enhancements

  * **⚡ Advanced Caching** \- Automatic value and path caching with cleanup
  * **📈 Enhanced Change Listeners** \- Detailed action information (`Set`, `TableInsert`, etc.)
  * **🎯 Global Change Monitoring** \- Listen to all changes with `"."` path
  * **✅ Readiness Checking** \- `IsReady()` for reliable data status
  * **🧹 Manual Cache Control** \- `ClearCache()` for debugging and optimization

## Architecture

PlayerState follows a **server-authoritative** model with enhanced real-time synchronization:

    ┌─────────────────┐    ┌──────────────────┐
    │   Server Module │────│ ProfileStore     │ (Data Persistence)
    │                 │    │ (Automatic Save) │
    └─────────────────┘    └──────────────────┘
             │
             ├─────────────► Replica ────────────┐
             │              (Real-time Sync)     │
             │                                   ▼
             ├─────────────► Batch System    ┌─────────────────┐
             │              (Performance)    │  Client Module  │
             │                               │                 │
             └─────────────► Leaderstats ────│  ► UI Updates   │
                            (Player List)    │  ► Game Logic   │
                                             │  ► Caching      │
                                             └─────────────────┘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

### Enhanced Data Flow

  1. **Server**: All data modifications with batch processing and validation
  2. **ProfileStore**: Enhanced persistence with automatic cleanup
  3. **Replica**: Real-time sync with detailed change information
  4. **Client**: Cached read access with advanced change detection
  5. **Leaderstats**: Automatic integration and real-time updates

No Built-in Client Requests

**PlayerState does NOT include built-in RemoteEvents or RemoteFunctions.** Clients cannot directly request data changes for security reasons. You must create your own RemoteEvents with proper server-side validation for any client-initiated modifications.

## Module Locations

lua

    -- Server Module (Server-side only)
    local PlayerStateServer = require(ServerScriptService.Libraries.PlayerState.PlayerStateServer)

    -- Client Module (Client-side only)
    local PlayerStateClient = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateClient)

    -- Default Data Schema (with leaderstats configuration)
    local DefaultData = require(ReplicatedStorage.Libraries.PlayerState.DefaultData)

    -- Configuration Module (server and client settings)
    local Config = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateConfig)

1
2
3
4
5
6
7
8
9
10
11

## Enhanced Usage Examples

Server Operations - NEWClient Operations - ENHANCED

lua

    -- Initialize player (enhanced with leaderstats)
    local success = PlayerState.Init(player)

    -- Basic operations with return values
    local success = PlayerState.Set(player, "Coins", 1000)
    local coins = PlayerState.Get(player, "Coins")

    -- Batch operations for performance
    PlayerState.BatchSetValues(player, {
        {path = "Coins", value = 2000},
        {path = "Level", value = 5},
        {path = "Plot.Likes", value = 100}
    })

    -- Array & Dictionary operations
    PlayerState.AddToArray(player, "Inventory", {Id = "sword", Name = "Iron Sword"})
    PlayerState.SetInDict(player, "Plot.Buildings", "House", {Level = 2})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

lua

    -- Check readiness before operations
    if PlayerState.IsReady() then
        -- Read data (with caching)
        local coins = PlayerState.Get("Coins")
        local likes = PlayerState.GetPath("Plot.Likes")

        -- Enhanced change listeners with action info
        local connection = PlayerState.OnChanged("Inventory", function(newValue, oldValue, info)
            if info and info.action == "TableInsert" then
                print(`Item added at index {info.index}`)
            elseif info.action == "TableRemove" then
                print(`Item removed from index {info.index}`)
            end
        end)

        -- Global change monitoring
        PlayerState.OnChanged(".", function(newValue, oldValue, info)
            if info and info.action then
                local pathString = table.concat(info.path, ".")
                print(`{info.action}: {pathString}`)
            end
        end)
    end

    -- Manual cache management
    PlayerState.ClearCache() -- For debugging or memory optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

Enhanced Performance

  * Server batch operations process up to 20 changes in 30ms for optimal performance
  * Client caching reduces repeated data access overhead
  * Automatic cache cleanup prevents memory leaks

## Leaderstats Integration

PlayerState now automatically creates and manages leaderstats:

lua

    -- In DefaultData.lua
    local DefaultData = {
        Coins = 0,
        Level = 1,
        Plot = { Likes = 0 },

        -- Automatic leaderstats configuration
        leaderstats = {
            ["💰 Coins"] = "Coins",
            ["⭐ Level"] = "Level",
            ["👍 Likes"] = "Plot.Likes"

1
2
3
4
5
6
7
8
9
10
11
12
13

**Features:**

  * 🏆 **Automatic Creation** \- Leaderstats appear when player data loads
  * 🔄 **Real-time Sync** \- Updates instantly when data changes
  * 🛡 **Protection** \- Prevents direct leaderstats modification
  * 🎯 **Path Support** \- Works with nested data paths

## Enhanced Path Notation

PlayerState supports dot notation with improved performance:

Enhanced Path Examples

lua

    -- Basic paths (cached for performance)
    PlayerState.SetPath(player, "Settings.MusicEnabled", false)
    PlayerState.SetPath(player, "Plot.Buildings.House.Level", 3)

    -- Complex batch operations
    PlayerState.BatchSetValues(player, {
        {path = "Stats.Combat.Weapons.Sword.Damage", value = 25},
        {path = "Inventory.Tools.Pickaxe.Durability", value = 100},
        {path = "Settings.Graphics.Quality", value = "High"}
    })

    -- Array operations with enhanced feedback
    PlayerState.AddToArray(player, "Achievements", "first_win") -- Returns success
    PlayerState.UpdateArrayItem(player, "Inventory", 1, newItemData) -- Index-based update

    -- Dictionary operations
    PlayerState.SetInDict(player, "Plot.Buildings", "Windmill", buildingData)
    PlayerState.RemoveFromDict(player, "Settings", "ObsoleteSetting")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

::: note Automatic Path Creation Paths automatically create nested structures when setting values, with enhanced validation! :::

## Enhanced Error Handling & Status

All functions now include comprehensive error handling:

lua

    -- Server functions return detailed success indicators
    local success = PlayerState.Init(player) -- boolean
    local added = PlayerState.AddToArray(player, "Inventory", item) -- boolean

    -- Batch operations with validation
    local success = PlayerState.BatchSetValues(player, operations) -- boolean
    PlayerState.FlushBatch(player) -- Force immediate processing

    -- Client functions with readiness checking
    if PlayerState.IsReady() then
        local coins = PlayerState.Get("Coins") -- Safe access
    end

    -- Enhanced connection management
    local connection = PlayerState.OnChanged("Coins", callback)
    -- Remember to disconnect: connection:Disconnect()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

## Performance Optimizations

### 🚀 Server Performance

  * **Batch Processing**: Intelligent grouping of operations
  * **Path Caching**: Reusable path parsing (max 1000 entries)
  * **Automatic Cleanup**: Memory management for long-running servers
  * **Leaderstats Protection**: Prevents accidental modification

### ⚡ Client Performance

  * **Value Caching**: Frequently accessed data cached with timestamps
  * **Nested Path Caching**: Complex path resolution optimization
  * **Connection Management**: Automatic cleanup and validation
  * **Cache Control**: Manual `ClearCache()` for memory management

## Migration Guide

### Legacy Data Migration

To import data from an old DataStore, configure [Legacy Data Migration](https://playerstate.netlify.app/migration) in PlayerStateConfig. Migration is opt-in and disabled by default.

### From Previous Versions

**Server Changes:**

  * Functions now return `boolean` success status
  * Batch operations recommended for multiple changes
  * Leaderstats are automatic (remove manual creation)

**Client Changes:**

  * Use `IsReady()` before data operations
  * Enhanced `OnChanged()` callbacks with action info
  * Global listeners with `"."` path for debugging
  * Manual cache management available

## Next Steps

  * **[Server Functions](https://playerstate.netlify.app/api/server)** \- Complete enhanced server API reference
  * **[Client Functions](https://playerstate.netlify.app/api/client)** \- Complete enhanced client API reference
  * **[Types](https://playerstate.netlify.app/api/types)** \- Enhanced type definitions and interfaces