mirror of
https://github.com/imezx/Warp.git
synced 2025-04-24 15:10:03 +00:00
Compare commits
48 commits
Author | SHA1 | Date | |
---|---|---|---|
|
0b1304f4c5 | ||
|
d8526c7e25 | ||
|
d065bc2e50 | ||
|
925df47d4c | ||
|
f3b674b377 | ||
|
677d3fa675 | ||
|
91862a65fe | ||
|
8ba9540550 | ||
|
1d67954ef9 | ||
|
43c4a1594f | ||
|
22996c9357 | ||
|
a377788f22 | ||
|
3354324c5b | ||
|
20b97eeb54 | ||
|
dbed984eea | ||
|
064075fbd9 | ||
|
77de85b6b8 | ||
|
10de54608a | ||
|
5b2e36b7bb | ||
|
eba9f79655 | ||
|
7309840005 | ||
|
aa693aee4f | ||
|
44fa07df85 | ||
|
839a7af667 | ||
|
acb08a385c | ||
|
6acf92d913 | ||
|
b4ee5dc1e3 | ||
|
598c30c147 | ||
|
0fb349fe0f | ||
|
4cd0f3f2cf | ||
|
8fd9573b8a | ||
|
739e13537d | ||
|
46bbe5feb0 | ||
|
0bc8f56e11 | ||
|
69fc64d134 | ||
|
c8199d5744 | ||
|
ef4b741f85 | ||
|
e7b4ae2f61 | ||
|
9aaa382278 | ||
|
5cf44bd21f | ||
|
5b0d4ee690 | ||
|
7519839dd1 | ||
|
01c5533394 | ||
|
4b568356c8 | ||
|
ae1754d616 | ||
|
563af05c62 | ||
|
f5bb1df4b0 | ||
|
8cad93f2f5 |
50 changed files with 3436 additions and 666 deletions
10
.github/workflows/deploy.yml
vendored
10
.github/workflows/deploy.yml
vendored
|
@ -18,16 +18,16 @@ jobs:
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: npm
|
cache: npm
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v3
|
uses: actions/configure-pages@v4
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Build
|
- name: Build
|
||||||
|
@ -35,9 +35,9 @@ jobs:
|
||||||
npm run docs:build
|
npm run docs:build
|
||||||
touch docs/.vitepress/dist
|
touch docs/.vitepress/dist
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v2
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: docs/.vitepress/dist
|
path: docs/.vitepress/dist
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v2
|
uses: actions/deploy-pages@v4
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,3 +2,5 @@ node_modules
|
||||||
docs/.vitepress/dist
|
docs/.vitepress/dist
|
||||||
docs/.vitepress/cache
|
docs/.vitepress/cache
|
||||||
wally.lock
|
wally.lock
|
||||||
|
TestEZ
|
||||||
|
test.project.json
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"stylua.targetReleaseVersion": "latest"
|
||||||
|
}
|
26
TestEZ/Context.lua
Normal file
26
TestEZ/Context.lua
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
--[[
|
||||||
|
The Context object implements a write-once key-value store. It also allows
|
||||||
|
for a new Context object to inherit the entries from an existing one.
|
||||||
|
]]
|
||||||
|
local Context = {}
|
||||||
|
|
||||||
|
function Context.new(parent)
|
||||||
|
local meta = {}
|
||||||
|
local index = {}
|
||||||
|
meta.__index = index
|
||||||
|
|
||||||
|
if parent then
|
||||||
|
for key, value in pairs(getmetatable(parent).__index) do
|
||||||
|
index[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function meta.__newindex(_obj, key, value)
|
||||||
|
assert(index[key] == nil, string.format("Cannot reassign %s in context", tostring(key)))
|
||||||
|
index[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable({}, meta)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Context
|
311
TestEZ/Expectation.lua
Normal file
311
TestEZ/Expectation.lua
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
--[[
|
||||||
|
Allows creation of expectation statements designed for behavior-driven
|
||||||
|
testing (BDD). See Chai (JS) or RSpec (Ruby) for examples of other BDD
|
||||||
|
frameworks.
|
||||||
|
|
||||||
|
The Expectation class is exposed to tests as a function called `expect`:
|
||||||
|
|
||||||
|
expect(5).to.equal(5)
|
||||||
|
expect(foo()).to.be.ok()
|
||||||
|
|
||||||
|
Expectations can be negated using .never:
|
||||||
|
|
||||||
|
expect(true).never.to.equal(false)
|
||||||
|
|
||||||
|
Expectations throw errors when their conditions are not met.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local Expectation = {}
|
||||||
|
|
||||||
|
--[[
|
||||||
|
These keys don't do anything except make expectations read more cleanly
|
||||||
|
]]
|
||||||
|
local SELF_KEYS = {
|
||||||
|
to = true,
|
||||||
|
be = true,
|
||||||
|
been = true,
|
||||||
|
have = true,
|
||||||
|
was = true,
|
||||||
|
at = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
--[[
|
||||||
|
These keys invert the condition expressed by the Expectation.
|
||||||
|
]]
|
||||||
|
local NEGATION_KEYS = {
|
||||||
|
never = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Extension of Lua's 'assert' that lets you specify an error level.
|
||||||
|
]]
|
||||||
|
local function assertLevel(condition, message, level)
|
||||||
|
message = message or "Assertion failed!"
|
||||||
|
level = level or 1
|
||||||
|
|
||||||
|
if not condition then
|
||||||
|
error(message, level + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Returns a version of the given method that can be called with either . or :
|
||||||
|
]]
|
||||||
|
local function bindSelf(self, method)
|
||||||
|
return function(firstArg, ...)
|
||||||
|
if firstArg == self then
|
||||||
|
return method(self, ...)
|
||||||
|
else
|
||||||
|
return method(self, firstArg, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function formatMessage(result, trueMessage, falseMessage)
|
||||||
|
if result then
|
||||||
|
return trueMessage
|
||||||
|
else
|
||||||
|
return falseMessage
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Create a new expectation
|
||||||
|
]]
|
||||||
|
function Expectation.new(value)
|
||||||
|
local self = {
|
||||||
|
value = value,
|
||||||
|
successCondition = true,
|
||||||
|
condition = false,
|
||||||
|
matchers = {},
|
||||||
|
_boundMatchers = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(self, Expectation)
|
||||||
|
|
||||||
|
self.a = bindSelf(self, self.a)
|
||||||
|
self.an = self.a
|
||||||
|
self.ok = bindSelf(self, self.ok)
|
||||||
|
self.equal = bindSelf(self, self.equal)
|
||||||
|
self.throw = bindSelf(self, self.throw)
|
||||||
|
self.near = bindSelf(self, self.near)
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Expectation.checkMatcherNameCollisions(name)
|
||||||
|
if SELF_KEYS[name] or NEGATION_KEYS[name] or Expectation[name] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function Expectation:extend(matchers)
|
||||||
|
self.matchers = matchers or {}
|
||||||
|
|
||||||
|
for name, implementation in pairs(self.matchers) do
|
||||||
|
self._boundMatchers[name] = bindSelf(self, function(_self, ...)
|
||||||
|
local result = implementation(self.value, ...)
|
||||||
|
local pass = result.pass == self.successCondition
|
||||||
|
|
||||||
|
assertLevel(pass, result.message, 3)
|
||||||
|
self:_resetModifiers()
|
||||||
|
return self
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Expectation.__index(self, key)
|
||||||
|
-- Keys that don't do anything except improve readability
|
||||||
|
if SELF_KEYS[key] then
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Invert your assertion
|
||||||
|
if NEGATION_KEYS[key] then
|
||||||
|
local newExpectation = Expectation.new(self.value):extend(self.matchers)
|
||||||
|
newExpectation.successCondition = not self.successCondition
|
||||||
|
|
||||||
|
return newExpectation
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._boundMatchers[key] then
|
||||||
|
return self._boundMatchers[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fall back to methods provided by Expectation
|
||||||
|
return Expectation[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Called by expectation terminators to reset modifiers in a statement.
|
||||||
|
|
||||||
|
This makes chains like:
|
||||||
|
|
||||||
|
expect(5)
|
||||||
|
.never.to.equal(6)
|
||||||
|
.to.equal(5)
|
||||||
|
|
||||||
|
Work as expected.
|
||||||
|
]]
|
||||||
|
function Expectation:_resetModifiers()
|
||||||
|
self.successCondition = true
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Assert that the expectation value is the given type.
|
||||||
|
|
||||||
|
expect(5).to.be.a("number")
|
||||||
|
]]
|
||||||
|
function Expectation:a(typeName)
|
||||||
|
local result = (type(self.value) == typeName) == self.successCondition
|
||||||
|
|
||||||
|
local message = formatMessage(self.successCondition,
|
||||||
|
("Expected value of type %q, got value %q of type %s"):format(
|
||||||
|
typeName,
|
||||||
|
tostring(self.value),
|
||||||
|
type(self.value)
|
||||||
|
),
|
||||||
|
("Expected value not of type %q, got value %q of type %s"):format(
|
||||||
|
typeName,
|
||||||
|
tostring(self.value),
|
||||||
|
type(self.value)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertLevel(result, message, 3)
|
||||||
|
self:_resetModifiers()
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Make alias public on class
|
||||||
|
Expectation.an = Expectation.a
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Assert that our expectation value is truthy
|
||||||
|
]]
|
||||||
|
function Expectation:ok()
|
||||||
|
local result = (self.value ~= nil) == self.successCondition
|
||||||
|
|
||||||
|
local message = formatMessage(self.successCondition,
|
||||||
|
("Expected value %q to be non-nil"):format(
|
||||||
|
tostring(self.value)
|
||||||
|
),
|
||||||
|
("Expected value %q to be nil"):format(
|
||||||
|
tostring(self.value)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertLevel(result, message, 3)
|
||||||
|
self:_resetModifiers()
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Assert that our expectation value is equal to another value
|
||||||
|
]]
|
||||||
|
function Expectation:equal(otherValue)
|
||||||
|
local result = (self.value == otherValue) == self.successCondition
|
||||||
|
|
||||||
|
local message = formatMessage(self.successCondition,
|
||||||
|
("Expected value %q (%s), got %q (%s) instead"):format(
|
||||||
|
tostring(otherValue),
|
||||||
|
type(otherValue),
|
||||||
|
tostring(self.value),
|
||||||
|
type(self.value)
|
||||||
|
),
|
||||||
|
("Expected anything but value %q (%s)"):format(
|
||||||
|
tostring(otherValue),
|
||||||
|
type(otherValue)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertLevel(result, message, 3)
|
||||||
|
self:_resetModifiers()
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Assert that our expectation value is equal to another value within some
|
||||||
|
inclusive limit.
|
||||||
|
]]
|
||||||
|
function Expectation:near(otherValue, limit)
|
||||||
|
assert(type(self.value) == "number", "Expectation value must be a number to use 'near'")
|
||||||
|
assert(type(otherValue) == "number", "otherValue must be a number")
|
||||||
|
assert(type(limit) == "number" or limit == nil, "limit must be a number or nil")
|
||||||
|
|
||||||
|
limit = limit or 1e-7
|
||||||
|
|
||||||
|
local result = (math.abs(self.value - otherValue) <= limit) == self.successCondition
|
||||||
|
|
||||||
|
local message = formatMessage(self.successCondition,
|
||||||
|
("Expected value to be near %f (within %f) but got %f instead"):format(
|
||||||
|
otherValue,
|
||||||
|
limit,
|
||||||
|
self.value
|
||||||
|
),
|
||||||
|
("Expected value to not be near %f (within %f) but got %f instead"):format(
|
||||||
|
otherValue,
|
||||||
|
limit,
|
||||||
|
self.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertLevel(result, message, 3)
|
||||||
|
self:_resetModifiers()
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Assert that our functoid expectation value throws an error when called.
|
||||||
|
An optional error message can be passed to assert that the error message
|
||||||
|
contains the given value.
|
||||||
|
]]
|
||||||
|
function Expectation:throw(messageSubstring)
|
||||||
|
local ok, err = pcall(self.value)
|
||||||
|
local result = ok ~= self.successCondition
|
||||||
|
|
||||||
|
if messageSubstring and not ok then
|
||||||
|
if self.successCondition then
|
||||||
|
result = err:find(messageSubstring, 1, true) ~= nil
|
||||||
|
else
|
||||||
|
result = err:find(messageSubstring, 1, true) == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local message
|
||||||
|
|
||||||
|
if messageSubstring then
|
||||||
|
message = formatMessage(self.successCondition,
|
||||||
|
("Expected function to throw an error containing %q, but it %s"):format(
|
||||||
|
messageSubstring,
|
||||||
|
err and ("threw: %s"):format(err) or "did not throw."
|
||||||
|
),
|
||||||
|
("Expected function to never throw an error containing %q, but it threw: %s"):format(
|
||||||
|
messageSubstring,
|
||||||
|
tostring(err)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
message = formatMessage(self.successCondition,
|
||||||
|
"Expected function to throw an error, but it did not throw.",
|
||||||
|
("Expected function to succeed, but it threw an error: %s"):format(
|
||||||
|
tostring(err)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
assertLevel(result, message, 3)
|
||||||
|
self:_resetModifiers()
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
return Expectation
|
38
TestEZ/ExpectationContext.lua
Normal file
38
TestEZ/ExpectationContext.lua
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
local Expectation = require(script.Parent.Expectation)
|
||||||
|
local checkMatcherNameCollisions = Expectation.checkMatcherNameCollisions
|
||||||
|
|
||||||
|
local function copy(t)
|
||||||
|
local result = {}
|
||||||
|
|
||||||
|
for key, value in pairs(t) do
|
||||||
|
result[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local ExpectationContext = {}
|
||||||
|
ExpectationContext.__index = ExpectationContext
|
||||||
|
|
||||||
|
function ExpectationContext.new(parent)
|
||||||
|
local self = {
|
||||||
|
_extensions = parent and copy(parent._extensions) or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
return setmetatable(self, ExpectationContext)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ExpectationContext:startExpectationChain(...)
|
||||||
|
return Expectation.new(...):extend(self._extensions)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ExpectationContext:extend(config)
|
||||||
|
for key, value in pairs(config) do
|
||||||
|
assert(self._extensions[key] == nil, string.format("Cannot reassign %q in expect.extend", key))
|
||||||
|
assert(checkMatcherNameCollisions(key), string.format("Cannot overwrite matcher %q; it already exists", key))
|
||||||
|
|
||||||
|
self._extensions[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ExpectationContext
|
89
TestEZ/LifecycleHooks.lua
Normal file
89
TestEZ/LifecycleHooks.lua
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
local TestEnum = require(script.Parent.TestEnum)
|
||||||
|
|
||||||
|
local LifecycleHooks = {}
|
||||||
|
LifecycleHooks.__index = LifecycleHooks
|
||||||
|
|
||||||
|
function LifecycleHooks.new()
|
||||||
|
local self = {
|
||||||
|
_stack = {},
|
||||||
|
}
|
||||||
|
return setmetatable(self, LifecycleHooks)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Returns an array of `beforeEach` hooks in FIFO order
|
||||||
|
]]
|
||||||
|
function LifecycleHooks:getBeforeEachHooks()
|
||||||
|
local key = TestEnum.NodeType.BeforeEach
|
||||||
|
local hooks = {}
|
||||||
|
|
||||||
|
for _, level in ipairs(self._stack) do
|
||||||
|
for _, hook in ipairs(level[key]) do
|
||||||
|
table.insert(hooks, hook)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return hooks
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Returns an array of `afterEach` hooks in FILO order
|
||||||
|
]]
|
||||||
|
function LifecycleHooks:getAfterEachHooks()
|
||||||
|
local key = TestEnum.NodeType.AfterEach
|
||||||
|
local hooks = {}
|
||||||
|
|
||||||
|
for _, level in ipairs(self._stack) do
|
||||||
|
for _, hook in ipairs(level[key]) do
|
||||||
|
table.insert(hooks, 1, hook)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return hooks
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Pushes uncalled beforeAll and afterAll hooks back up the stack
|
||||||
|
]]
|
||||||
|
function LifecycleHooks:popHooks()
|
||||||
|
table.remove(self._stack, #self._stack)
|
||||||
|
end
|
||||||
|
|
||||||
|
function LifecycleHooks:pushHooksFrom(planNode)
|
||||||
|
assert(planNode ~= nil)
|
||||||
|
|
||||||
|
table.insert(self._stack, {
|
||||||
|
[TestEnum.NodeType.BeforeAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeAll),
|
||||||
|
[TestEnum.NodeType.AfterAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterAll),
|
||||||
|
[TestEnum.NodeType.BeforeEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeEach),
|
||||||
|
[TestEnum.NodeType.AfterEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterEach),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Get the beforeAll hooks from the current level.
|
||||||
|
]]
|
||||||
|
function LifecycleHooks:getBeforeAllHooks()
|
||||||
|
return self._stack[#self._stack][TestEnum.NodeType.BeforeAll]
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Get the afterAll hooks from the current level.
|
||||||
|
]]
|
||||||
|
function LifecycleHooks:getAfterAllHooks()
|
||||||
|
return self._stack[#self._stack][TestEnum.NodeType.AfterAll]
|
||||||
|
end
|
||||||
|
|
||||||
|
function LifecycleHooks:_getHooksOfType(nodes, key)
|
||||||
|
local hooks = {}
|
||||||
|
|
||||||
|
for _, node in ipairs(nodes) do
|
||||||
|
if node.type == key then
|
||||||
|
table.insert(hooks, node.callback)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return hooks
|
||||||
|
end
|
||||||
|
|
||||||
|
return LifecycleHooks
|
102
TestEZ/Reporters/TeamCityReporter.lua
Normal file
102
TestEZ/Reporters/TeamCityReporter.lua
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
local TestService = game:GetService("TestService")
|
||||||
|
|
||||||
|
local TestEnum = require(script.Parent.Parent.TestEnum)
|
||||||
|
|
||||||
|
local TeamCityReporter = {}
|
||||||
|
|
||||||
|
local function teamCityEscape(str)
|
||||||
|
str = string.gsub(str, "([]|'[])","|%1")
|
||||||
|
str = string.gsub(str, "\r", "|r")
|
||||||
|
str = string.gsub(str, "\n", "|n")
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
local function teamCityEnterSuite(suiteName)
|
||||||
|
return string.format("##teamcity[testSuiteStarted name='%s']", teamCityEscape(suiteName))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function teamCityLeaveSuite(suiteName)
|
||||||
|
return string.format("##teamcity[testSuiteFinished name='%s']", teamCityEscape(suiteName))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function teamCityEnterCase(caseName)
|
||||||
|
return string.format("##teamcity[testStarted name='%s']", teamCityEscape(caseName))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function teamCityLeaveCase(caseName)
|
||||||
|
return string.format("##teamcity[testFinished name='%s']", teamCityEscape(caseName))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function teamCityFailCase(caseName, errorMessage)
|
||||||
|
return string.format("##teamcity[testFailed name='%s' message='%s']",
|
||||||
|
teamCityEscape(caseName), teamCityEscape(errorMessage))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reportNode(node, buffer, level)
|
||||||
|
buffer = buffer or {}
|
||||||
|
level = level or 0
|
||||||
|
if node.status == TestEnum.TestStatus.Skipped then
|
||||||
|
return buffer
|
||||||
|
end
|
||||||
|
if node.planNode.type == TestEnum.NodeType.Describe then
|
||||||
|
table.insert(buffer, teamCityEnterSuite(node.planNode.phrase))
|
||||||
|
for _, child in ipairs(node.children) do
|
||||||
|
reportNode(child, buffer, level + 1)
|
||||||
|
end
|
||||||
|
table.insert(buffer, teamCityLeaveSuite(node.planNode.phrase))
|
||||||
|
else
|
||||||
|
table.insert(buffer, teamCityEnterCase(node.planNode.phrase))
|
||||||
|
if node.status == TestEnum.TestStatus.Failure then
|
||||||
|
table.insert(buffer, teamCityFailCase(node.planNode.phrase, table.concat(node.errors,"\n")))
|
||||||
|
end
|
||||||
|
table.insert(buffer, teamCityLeaveCase(node.planNode.phrase))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reportRoot(node)
|
||||||
|
local buffer = {}
|
||||||
|
|
||||||
|
for _, child in ipairs(node.children) do
|
||||||
|
reportNode(child, buffer, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
local function report(root)
|
||||||
|
local buffer = reportRoot(root)
|
||||||
|
|
||||||
|
return table.concat(buffer, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TeamCityReporter.report(results)
|
||||||
|
local resultBuffer = {
|
||||||
|
"Test results:",
|
||||||
|
report(results),
|
||||||
|
("%d passed, %d failed, %d skipped"):format(
|
||||||
|
results.successCount,
|
||||||
|
results.failureCount,
|
||||||
|
results.skippedCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
print(table.concat(resultBuffer, "\n"))
|
||||||
|
|
||||||
|
if results.failureCount > 0 then
|
||||||
|
print(("%d test nodes reported failures."):format(results.failureCount))
|
||||||
|
end
|
||||||
|
|
||||||
|
if #results.errors > 0 then
|
||||||
|
print("Errors reported by tests:")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
for _, message in ipairs(results.errors) do
|
||||||
|
TestService:Error(message)
|
||||||
|
|
||||||
|
-- Insert a blank line after each error
|
||||||
|
print("")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return TeamCityReporter
|
106
TestEZ/Reporters/TextReporter.lua
Normal file
106
TestEZ/Reporters/TextReporter.lua
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
--[[
|
||||||
|
The TextReporter uses the results from a completed test to output text to
|
||||||
|
standard output and TestService.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local TestService = game:GetService("TestService")
|
||||||
|
|
||||||
|
local TestEnum = require(script.Parent.Parent.TestEnum)
|
||||||
|
|
||||||
|
local INDENT = (" "):rep(3)
|
||||||
|
local STATUS_SYMBOLS = {
|
||||||
|
[TestEnum.TestStatus.Success] = "+",
|
||||||
|
[TestEnum.TestStatus.Failure] = "-",
|
||||||
|
[TestEnum.TestStatus.Skipped] = "~"
|
||||||
|
}
|
||||||
|
local UNKNOWN_STATUS_SYMBOL = "?"
|
||||||
|
|
||||||
|
local TextReporter = {}
|
||||||
|
|
||||||
|
local function compareNodes(a, b)
|
||||||
|
return a.planNode.phrase:lower() < b.planNode.phrase:lower()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reportNode(node, buffer, level)
|
||||||
|
buffer = buffer or {}
|
||||||
|
level = level or 0
|
||||||
|
|
||||||
|
if node.status == TestEnum.TestStatus.Skipped then
|
||||||
|
return buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
local line
|
||||||
|
|
||||||
|
if node.status then
|
||||||
|
local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL
|
||||||
|
|
||||||
|
line = ("%s[%s] %s"):format(
|
||||||
|
INDENT:rep(level),
|
||||||
|
symbol,
|
||||||
|
node.planNode.phrase
|
||||||
|
)
|
||||||
|
else
|
||||||
|
line = ("%s%s"):format(
|
||||||
|
INDENT:rep(level),
|
||||||
|
node.planNode.phrase
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(buffer, line)
|
||||||
|
table.sort(node.children, compareNodes)
|
||||||
|
|
||||||
|
for _, child in ipairs(node.children) do
|
||||||
|
reportNode(child, buffer, level + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reportRoot(node)
|
||||||
|
local buffer = {}
|
||||||
|
table.sort(node.children, compareNodes)
|
||||||
|
|
||||||
|
for _, child in ipairs(node.children) do
|
||||||
|
reportNode(child, buffer, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
local function report(root)
|
||||||
|
local buffer = reportRoot(root)
|
||||||
|
|
||||||
|
return table.concat(buffer, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextReporter.report(results)
|
||||||
|
local resultBuffer = {
|
||||||
|
"Test results:",
|
||||||
|
report(results),
|
||||||
|
("%d passed, %d failed, %d skipped"):format(
|
||||||
|
results.successCount,
|
||||||
|
results.failureCount,
|
||||||
|
results.skippedCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
print(table.concat(resultBuffer, "\n"))
|
||||||
|
|
||||||
|
if results.failureCount > 0 then
|
||||||
|
print(("%d test nodes reported failures."):format(results.failureCount))
|
||||||
|
end
|
||||||
|
|
||||||
|
if #results.errors > 0 then
|
||||||
|
print("Errors reported by tests:")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
for _, message in ipairs(results.errors) do
|
||||||
|
TestService:Error(message)
|
||||||
|
|
||||||
|
-- Insert a blank line after each error
|
||||||
|
print("")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return TextReporter
|
97
TestEZ/Reporters/TextReporterQuiet.lua
Normal file
97
TestEZ/Reporters/TextReporterQuiet.lua
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
--[[
|
||||||
|
Copy of TextReporter that doesn't output successful tests.
|
||||||
|
|
||||||
|
This should be temporary, it's just a workaround to make CI environments
|
||||||
|
happy in the short-term.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local TestService = game:GetService("TestService")
|
||||||
|
|
||||||
|
local TestEnum = require(script.Parent.Parent.TestEnum)
|
||||||
|
|
||||||
|
local INDENT = (" "):rep(3)
|
||||||
|
local STATUS_SYMBOLS = {
|
||||||
|
[TestEnum.TestStatus.Success] = "+",
|
||||||
|
[TestEnum.TestStatus.Failure] = "-",
|
||||||
|
[TestEnum.TestStatus.Skipped] = "~"
|
||||||
|
}
|
||||||
|
local UNKNOWN_STATUS_SYMBOL = "?"
|
||||||
|
|
||||||
|
local TextReporterQuiet = {}
|
||||||
|
|
||||||
|
local function reportNode(node, buffer, level)
|
||||||
|
buffer = buffer or {}
|
||||||
|
level = level or 0
|
||||||
|
|
||||||
|
if node.status == TestEnum.TestStatus.Skipped then
|
||||||
|
return buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
local line
|
||||||
|
|
||||||
|
if node.status ~= TestEnum.TestStatus.Success then
|
||||||
|
local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL
|
||||||
|
|
||||||
|
line = ("%s[%s] %s"):format(
|
||||||
|
INDENT:rep(level),
|
||||||
|
symbol,
|
||||||
|
node.planNode.phrase
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(buffer, line)
|
||||||
|
|
||||||
|
for _, child in ipairs(node.children) do
|
||||||
|
reportNode(child, buffer, level + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reportRoot(node)
|
||||||
|
local buffer = {}
|
||||||
|
|
||||||
|
for _, child in ipairs(node.children) do
|
||||||
|
reportNode(child, buffer, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
local function report(root)
|
||||||
|
local buffer = reportRoot(root)
|
||||||
|
|
||||||
|
return table.concat(buffer, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextReporterQuiet.report(results)
|
||||||
|
local resultBuffer = {
|
||||||
|
"Test results:",
|
||||||
|
report(results),
|
||||||
|
("%d passed, %d failed, %d skipped"):format(
|
||||||
|
results.successCount,
|
||||||
|
results.failureCount,
|
||||||
|
results.skippedCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
print(table.concat(resultBuffer, "\n"))
|
||||||
|
|
||||||
|
if results.failureCount > 0 then
|
||||||
|
print(("%d test nodes reported failures."):format(results.failureCount))
|
||||||
|
end
|
||||||
|
|
||||||
|
if #results.errors > 0 then
|
||||||
|
print("Errors reported by tests:")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
for _, message in ipairs(results.errors) do
|
||||||
|
TestService:Error(message)
|
||||||
|
|
||||||
|
-- Insert a blank line after each error
|
||||||
|
print("")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return TextReporterQuiet
|
147
TestEZ/TestBootstrap.lua
Normal file
147
TestEZ/TestBootstrap.lua
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
--[[
|
||||||
|
Provides an interface to quickly run and report tests from a given object.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local TestPlanner = require(script.Parent.TestPlanner)
|
||||||
|
local TestRunner = require(script.Parent.TestRunner)
|
||||||
|
local TextReporter = require(script.Parent.Reporters.TextReporter)
|
||||||
|
|
||||||
|
local TestBootstrap = {}
|
||||||
|
|
||||||
|
local function stripSpecSuffix(name)
|
||||||
|
return (name:gsub("%.spec$", ""))
|
||||||
|
end
|
||||||
|
local function isSpecScript(aScript)
|
||||||
|
return aScript:IsA("ModuleScript") and aScript.Name:match("%.spec$")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getPath(module, root)
|
||||||
|
root = root or game
|
||||||
|
|
||||||
|
local path = {}
|
||||||
|
local last = module
|
||||||
|
|
||||||
|
if last.Name == "init.spec" then
|
||||||
|
-- Use the directory's node for init.spec files.
|
||||||
|
last = last.Parent
|
||||||
|
end
|
||||||
|
|
||||||
|
while last ~= nil and last ~= root do
|
||||||
|
table.insert(path, stripSpecSuffix(last.Name))
|
||||||
|
last = last.Parent
|
||||||
|
end
|
||||||
|
table.insert(path, stripSpecSuffix(root.Name))
|
||||||
|
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
local function toStringPath(tablePath)
|
||||||
|
local stringPath = ""
|
||||||
|
local first = true
|
||||||
|
for _, element in ipairs(tablePath) do
|
||||||
|
if first then
|
||||||
|
stringPath = element
|
||||||
|
first = false
|
||||||
|
else
|
||||||
|
stringPath = element .. " " .. stringPath
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return stringPath
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestBootstrap:getModulesImpl(root, modules, current)
|
||||||
|
modules = modules or {}
|
||||||
|
current = current or root
|
||||||
|
|
||||||
|
if isSpecScript(current) then
|
||||||
|
local method = require(current)
|
||||||
|
local path = getPath(current, root)
|
||||||
|
local pathString = toStringPath(path)
|
||||||
|
|
||||||
|
table.insert(modules, {
|
||||||
|
method = method,
|
||||||
|
path = path,
|
||||||
|
pathStringForSorting = pathString:lower()
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Find all the ModuleScripts in this tree that are tests.
|
||||||
|
]]
|
||||||
|
function TestBootstrap:getModules(root)
|
||||||
|
local modules = {}
|
||||||
|
|
||||||
|
self:getModulesImpl(root, modules)
|
||||||
|
|
||||||
|
for _, child in ipairs(root:GetDescendants()) do
|
||||||
|
self:getModulesImpl(root, modules, child)
|
||||||
|
end
|
||||||
|
|
||||||
|
return modules
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Runs all test and reports the results using the given test reporter.
|
||||||
|
|
||||||
|
If no reporter is specified, a reasonable default is provided.
|
||||||
|
|
||||||
|
This function demonstrates the expected workflow with this testing system:
|
||||||
|
1. Locate test modules
|
||||||
|
2. Generate test plan
|
||||||
|
3. Run test plan
|
||||||
|
4. Report test results
|
||||||
|
|
||||||
|
This means we could hypothetically present a GUI to the developer that shows
|
||||||
|
the test plan before we execute it, allowing them to toggle specific tests
|
||||||
|
before they're run, but after they've been identified!
|
||||||
|
]]
|
||||||
|
function TestBootstrap:run(roots, reporter, otherOptions)
|
||||||
|
reporter = reporter or TextReporter
|
||||||
|
|
||||||
|
otherOptions = otherOptions or {}
|
||||||
|
local showTimingInfo = otherOptions["showTimingInfo"] or false
|
||||||
|
local testNamePattern = otherOptions["testNamePattern"]
|
||||||
|
local extraEnvironment = otherOptions["extraEnvironment"] or {}
|
||||||
|
|
||||||
|
if type(roots) ~= "table" then
|
||||||
|
error(("Bad argument #1 to TestBootstrap:run. Expected table, got %s"):format(typeof(roots)), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local startTime = tick()
|
||||||
|
|
||||||
|
local modules = {}
|
||||||
|
for _, subRoot in ipairs(roots) do
|
||||||
|
local newModules = self:getModules(subRoot)
|
||||||
|
|
||||||
|
for _, newModule in ipairs(newModules) do
|
||||||
|
table.insert(modules, newModule)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local afterModules = tick()
|
||||||
|
|
||||||
|
local plan = TestPlanner.createPlan(modules, testNamePattern, extraEnvironment)
|
||||||
|
local afterPlan = tick()
|
||||||
|
|
||||||
|
local results = TestRunner.runPlan(plan)
|
||||||
|
local afterRun = tick()
|
||||||
|
|
||||||
|
reporter.report(results)
|
||||||
|
local afterReport = tick()
|
||||||
|
|
||||||
|
if showTimingInfo then
|
||||||
|
local timing = {
|
||||||
|
("Took %f seconds to locate test modules"):format(afterModules - startTime),
|
||||||
|
("Took %f seconds to create test plan"):format(afterPlan - afterModules),
|
||||||
|
("Took %f seconds to run tests"):format(afterRun - afterPlan),
|
||||||
|
("Took %f seconds to report tests"):format(afterReport - afterRun),
|
||||||
|
}
|
||||||
|
|
||||||
|
print(table.concat(timing, "\n"))
|
||||||
|
end
|
||||||
|
|
||||||
|
return results
|
||||||
|
end
|
||||||
|
|
||||||
|
return TestBootstrap
|
28
TestEZ/TestEnum.lua
Normal file
28
TestEZ/TestEnum.lua
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
--[[
|
||||||
|
Constants used throughout the testing framework.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local TestEnum = {}
|
||||||
|
|
||||||
|
TestEnum.TestStatus = {
|
||||||
|
Success = "Success",
|
||||||
|
Failure = "Failure",
|
||||||
|
Skipped = "Skipped"
|
||||||
|
}
|
||||||
|
|
||||||
|
TestEnum.NodeType = {
|
||||||
|
Describe = "Describe",
|
||||||
|
It = "It",
|
||||||
|
BeforeAll = "BeforeAll",
|
||||||
|
AfterAll = "AfterAll",
|
||||||
|
BeforeEach = "BeforeEach",
|
||||||
|
AfterEach = "AfterEach"
|
||||||
|
}
|
||||||
|
|
||||||
|
TestEnum.NodeModifier = {
|
||||||
|
None = "None",
|
||||||
|
Skip = "Skip",
|
||||||
|
Focus = "Focus"
|
||||||
|
}
|
||||||
|
|
||||||
|
return TestEnum
|
304
TestEZ/TestPlan.lua
Normal file
304
TestEZ/TestPlan.lua
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
--[[
|
||||||
|
Represents a tree of tests that have been loaded but not necessarily
|
||||||
|
executed yet.
|
||||||
|
|
||||||
|
TestPlan objects are produced by TestPlanner.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local TestEnum = require(script.Parent.TestEnum)
|
||||||
|
local Expectation = require(script.Parent.Expectation)
|
||||||
|
|
||||||
|
local function newEnvironment(currentNode, extraEnvironment)
|
||||||
|
local env = {}
|
||||||
|
|
||||||
|
if extraEnvironment then
|
||||||
|
if type(extraEnvironment) ~= "table" then
|
||||||
|
error(("Bad argument #2 to newEnvironment. Expected table, got %s"):format(
|
||||||
|
typeof(extraEnvironment)), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
for key, value in pairs(extraEnvironment) do
|
||||||
|
env[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function addChild(phrase, callback, nodeType, nodeModifier)
|
||||||
|
local node = currentNode:addChild(phrase, nodeType, nodeModifier)
|
||||||
|
node.callback = callback
|
||||||
|
if nodeType == TestEnum.NodeType.Describe then
|
||||||
|
node:expand()
|
||||||
|
end
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
|
||||||
|
function env.describeFOCUS(phrase, callback)
|
||||||
|
addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Focus)
|
||||||
|
end
|
||||||
|
|
||||||
|
function env.describeSKIP(phrase, callback)
|
||||||
|
addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Skip)
|
||||||
|
end
|
||||||
|
|
||||||
|
function env.describe(phrase, callback, nodeModifier)
|
||||||
|
addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.None)
|
||||||
|
end
|
||||||
|
|
||||||
|
function env.itFOCUS(phrase, callback)
|
||||||
|
addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Focus)
|
||||||
|
end
|
||||||
|
|
||||||
|
function env.itSKIP(phrase, callback)
|
||||||
|
addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)
|
||||||
|
end
|
||||||
|
|
||||||
|
function env.itFIXME(phrase, callback)
|
||||||
|
local node = addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)
|
||||||
|
warn("FIXME: broken test", node:getFullName())
|
||||||
|
end
|
||||||
|
|
||||||
|
function env.it(phrase, callback, nodeModifier)
|
||||||
|
addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.None)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Incrementing counter used to ensure that beforeAll, afterAll, beforeEach, afterEach have unique phrases
|
||||||
|
local lifecyclePhaseId = 0
|
||||||
|
|
||||||
|
local lifecycleHooks = {
|
||||||
|
[TestEnum.NodeType.BeforeAll] = "beforeAll",
|
||||||
|
[TestEnum.NodeType.AfterAll] = "afterAll",
|
||||||
|
[TestEnum.NodeType.BeforeEach] = "beforeEach",
|
||||||
|
[TestEnum.NodeType.AfterEach] = "afterEach"
|
||||||
|
}
|
||||||
|
|
||||||
|
for nodeType, name in pairs(lifecycleHooks) do
|
||||||
|
env[name] = function(callback)
|
||||||
|
addChild(name .. "_" .. tostring(lifecyclePhaseId), callback, nodeType, TestEnum.NodeModifier.None)
|
||||||
|
lifecyclePhaseId = lifecyclePhaseId + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function env.FIXME(optionalMessage)
|
||||||
|
warn("FIXME: broken test", currentNode:getFullName(), optionalMessage or "")
|
||||||
|
|
||||||
|
currentNode.modifier = TestEnum.NodeModifier.Skip
|
||||||
|
end
|
||||||
|
|
||||||
|
function env.FOCUS()
|
||||||
|
currentNode.modifier = TestEnum.NodeModifier.Focus
|
||||||
|
end
|
||||||
|
|
||||||
|
function env.SKIP()
|
||||||
|
currentNode.modifier = TestEnum.NodeModifier.Skip
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
This function is deprecated. Calling it is a no-op beyond generating a
|
||||||
|
warning.
|
||||||
|
]]
|
||||||
|
function env.HACK_NO_XPCALL()
|
||||||
|
warn("HACK_NO_XPCALL is deprecated. It is now safe to yield in an " ..
|
||||||
|
"xpcall, so this is no longer necessary. It can be safely deleted.")
|
||||||
|
end
|
||||||
|
|
||||||
|
env.fit = env.itFOCUS
|
||||||
|
env.xit = env.itSKIP
|
||||||
|
env.fdescribe = env.describeFOCUS
|
||||||
|
env.xdescribe = env.describeSKIP
|
||||||
|
|
||||||
|
env.expect = setmetatable({
|
||||||
|
extend = function(...)
|
||||||
|
error("Cannot call \"expect.extend\" from within a \"describe\" node.")
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
__call = function(_self, ...)
|
||||||
|
return Expectation.new(...)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
return env
|
||||||
|
end
|
||||||
|
|
||||||
|
local TestNode = {}
|
||||||
|
TestNode.__index = TestNode
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Create a new test node. A pointer to the test plan, a phrase to describe it
|
||||||
|
and the type of node it is are required. The modifier is optional and will
|
||||||
|
be None if left blank.
|
||||||
|
]]
|
||||||
|
function TestNode.new(plan, phrase, nodeType, nodeModifier)
|
||||||
|
nodeModifier = nodeModifier or TestEnum.NodeModifier.None
|
||||||
|
|
||||||
|
local node = {
|
||||||
|
plan = plan,
|
||||||
|
phrase = phrase,
|
||||||
|
type = nodeType,
|
||||||
|
modifier = nodeModifier,
|
||||||
|
children = {},
|
||||||
|
callback = nil,
|
||||||
|
parent = nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
node.environment = newEnvironment(node, plan.extraEnvironment)
|
||||||
|
return setmetatable(node, TestNode)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getModifier(name, pattern, modifier)
|
||||||
|
if pattern and (modifier == nil or modifier == TestEnum.NodeModifier.None) then
|
||||||
|
if name:match(pattern) then
|
||||||
|
return TestEnum.NodeModifier.Focus
|
||||||
|
else
|
||||||
|
return TestEnum.NodeModifier.Skip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return modifier
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestNode:addChild(phrase, nodeType, nodeModifier)
|
||||||
|
if nodeType == TestEnum.NodeType.It then
|
||||||
|
for _, child in pairs(self.children) do
|
||||||
|
if child.phrase == phrase then
|
||||||
|
error("Duplicate it block found: " .. child:getFullName())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local childName = self:getFullName() .. " " .. phrase
|
||||||
|
nodeModifier = getModifier(childName, self.plan.testNamePattern, nodeModifier)
|
||||||
|
local child = TestNode.new(self.plan, phrase, nodeType, nodeModifier)
|
||||||
|
child.parent = self
|
||||||
|
table.insert(self.children, child)
|
||||||
|
return child
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Join the names of all the nodes back to the parent.
|
||||||
|
]]
|
||||||
|
function TestNode:getFullName()
|
||||||
|
if self.parent then
|
||||||
|
local parentPhrase = self.parent:getFullName()
|
||||||
|
if parentPhrase then
|
||||||
|
return parentPhrase .. " " .. self.phrase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return self.phrase
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Expand a node by setting its callback environment and then calling it. Any
|
||||||
|
further it and describe calls within the callback will be added to the tree.
|
||||||
|
]]
|
||||||
|
function TestNode:expand()
|
||||||
|
local originalEnv = getfenv(self.callback)
|
||||||
|
local callbackEnv = setmetatable({}, { __index = originalEnv })
|
||||||
|
for key, value in pairs(self.environment) do
|
||||||
|
callbackEnv[key] = value
|
||||||
|
end
|
||||||
|
-- Copy 'script' directly to new env to make Studio debugger happy.
|
||||||
|
-- Studio debugger does not look into __index, because of security reasons
|
||||||
|
callbackEnv.script = originalEnv.script
|
||||||
|
setfenv(self.callback, callbackEnv)
|
||||||
|
|
||||||
|
local success, result = xpcall(self.callback, function(message)
|
||||||
|
return debug.traceback(tostring(message), 2)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
self.loadError = result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local TestPlan = {}
|
||||||
|
TestPlan.__index = TestPlan
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Create a new, empty TestPlan.
|
||||||
|
]]
|
||||||
|
function TestPlan.new(testNamePattern, extraEnvironment)
|
||||||
|
local plan = {
|
||||||
|
children = {},
|
||||||
|
testNamePattern = testNamePattern,
|
||||||
|
extraEnvironment = extraEnvironment,
|
||||||
|
}
|
||||||
|
|
||||||
|
return setmetatable(plan, TestPlan)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Add a new child under the test plan's root node.
|
||||||
|
]]
|
||||||
|
function TestPlan:addChild(phrase, nodeType, nodeModifier)
|
||||||
|
nodeModifier = getModifier(phrase, self.testNamePattern, nodeModifier)
|
||||||
|
local child = TestNode.new(self, phrase, nodeType, nodeModifier)
|
||||||
|
table.insert(self.children, child)
|
||||||
|
return child
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Add a new describe node with the given method as a callback. Generates or
|
||||||
|
reuses all the describe nodes along the path.
|
||||||
|
]]
|
||||||
|
function TestPlan:addRoot(path, method)
|
||||||
|
local curNode = self
|
||||||
|
for i = #path, 1, -1 do
|
||||||
|
local nextNode = nil
|
||||||
|
|
||||||
|
for _, child in ipairs(curNode.children) do
|
||||||
|
if child.phrase == path[i] then
|
||||||
|
nextNode = child
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if nextNode == nil then
|
||||||
|
nextNode = curNode:addChild(path[i], TestEnum.NodeType.Describe)
|
||||||
|
end
|
||||||
|
|
||||||
|
curNode = nextNode
|
||||||
|
end
|
||||||
|
|
||||||
|
curNode.callback = method
|
||||||
|
curNode:expand()
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Calls the given callback on all nodes in the tree, traversed depth-first.
|
||||||
|
]]
|
||||||
|
function TestPlan:visitAllNodes(callback, root, level)
|
||||||
|
root = root or self
|
||||||
|
level = level or 0
|
||||||
|
|
||||||
|
for _, child in ipairs(root.children) do
|
||||||
|
callback(child, level)
|
||||||
|
|
||||||
|
self:visitAllNodes(callback, child, level + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Visualizes the test plan in a simple format, suitable for debugging the test
|
||||||
|
plan's structure.
|
||||||
|
]]
|
||||||
|
function TestPlan:visualize()
|
||||||
|
local buffer = {}
|
||||||
|
self:visitAllNodes(function(node, level)
|
||||||
|
table.insert(buffer, (" "):rep(3 * level) .. node.phrase)
|
||||||
|
end)
|
||||||
|
return table.concat(buffer, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Gets a list of all nodes in the tree for which the given callback returns
|
||||||
|
true.
|
||||||
|
]]
|
||||||
|
function TestPlan:findNodes(callback)
|
||||||
|
local results = {}
|
||||||
|
self:visitAllNodes(function(node)
|
||||||
|
if callback(node) then
|
||||||
|
table.insert(results, node)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return results
|
||||||
|
end
|
||||||
|
|
||||||
|
return TestPlan
|
40
TestEZ/TestPlanner.lua
Normal file
40
TestEZ/TestPlanner.lua
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
--[[
|
||||||
|
Turns a series of specification functions into a test plan.
|
||||||
|
|
||||||
|
Uses a TestPlanBuilder to keep track of the state of the tree being built.
|
||||||
|
]]
|
||||||
|
local TestPlan = require(script.Parent.TestPlan)
|
||||||
|
|
||||||
|
local TestPlanner = {}
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Create a new TestPlan from a list of specification functions.
|
||||||
|
|
||||||
|
These functions should call a combination of `describe` and `it` (and their
|
||||||
|
variants), which will be turned into a test plan to be executed.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- modulesList - list of tables describing test modules {
|
||||||
|
method, -- specification function described above
|
||||||
|
path, -- array of parent entires, first element is the leaf that owns `method`
|
||||||
|
pathStringForSorting -- a string representation of `path`, used for sorting of the test plan
|
||||||
|
}
|
||||||
|
- testNamePattern - Only tests matching this Lua pattern string will run. Pass empty or nil to run all tests
|
||||||
|
- extraEnvironment - Lua table holding additional functions and variables to be injected into the specification
|
||||||
|
function during execution
|
||||||
|
]]
|
||||||
|
function TestPlanner.createPlan(modulesList, testNamePattern, extraEnvironment)
|
||||||
|
local plan = TestPlan.new(testNamePattern, extraEnvironment)
|
||||||
|
|
||||||
|
table.sort(modulesList, function(a, b)
|
||||||
|
return a.pathStringForSorting < b.pathStringForSorting
|
||||||
|
end)
|
||||||
|
|
||||||
|
for _, module in ipairs(modulesList) do
|
||||||
|
plan:addRoot(module.path, module.method)
|
||||||
|
end
|
||||||
|
|
||||||
|
return plan
|
||||||
|
end
|
||||||
|
|
||||||
|
return TestPlanner
|
112
TestEZ/TestResults.lua
Normal file
112
TestEZ/TestResults.lua
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
--[[
|
||||||
|
Represents a tree of test results.
|
||||||
|
|
||||||
|
Each node in the tree corresponds directly to a node in a corresponding
|
||||||
|
TestPlan, accessible via the 'planNode' field.
|
||||||
|
|
||||||
|
TestResults objects are produced by TestRunner using TestSession as state.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local TestEnum = require(script.Parent.TestEnum)
|
||||||
|
|
||||||
|
local STATUS_SYMBOLS = {
|
||||||
|
[TestEnum.TestStatus.Success] = "+",
|
||||||
|
[TestEnum.TestStatus.Failure] = "-",
|
||||||
|
[TestEnum.TestStatus.Skipped] = "~"
|
||||||
|
}
|
||||||
|
|
||||||
|
local TestResults = {}
|
||||||
|
|
||||||
|
TestResults.__index = TestResults
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Create a new TestResults tree that's linked to the given TestPlan.
|
||||||
|
]]
|
||||||
|
function TestResults.new(plan)
|
||||||
|
local self = {
|
||||||
|
successCount = 0,
|
||||||
|
failureCount = 0,
|
||||||
|
skippedCount = 0,
|
||||||
|
planNode = plan,
|
||||||
|
children = {},
|
||||||
|
errors = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(self, TestResults)
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Create a new result node that can be inserted into a TestResult tree.
|
||||||
|
]]
|
||||||
|
function TestResults.createNode(planNode)
|
||||||
|
local node = {
|
||||||
|
planNode = planNode,
|
||||||
|
children = {},
|
||||||
|
errors = {},
|
||||||
|
status = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Visit all test result nodes, depth-first.
|
||||||
|
]]
|
||||||
|
function TestResults:visitAllNodes(callback, root)
|
||||||
|
root = root or self
|
||||||
|
|
||||||
|
for _, child in ipairs(root.children) do
|
||||||
|
callback(child)
|
||||||
|
|
||||||
|
self:visitAllNodes(callback, child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Creates a debug visualization of the test results.
|
||||||
|
]]
|
||||||
|
function TestResults:visualize(root, level)
|
||||||
|
root = root or self
|
||||||
|
level = level or 0
|
||||||
|
|
||||||
|
local buffer = {}
|
||||||
|
|
||||||
|
for _, child in ipairs(root.children) do
|
||||||
|
if child.planNode.type == TestEnum.NodeType.It then
|
||||||
|
local symbol = STATUS_SYMBOLS[child.status] or "?"
|
||||||
|
local str = ("%s[%s] %s"):format(
|
||||||
|
(" "):rep(3 * level),
|
||||||
|
symbol,
|
||||||
|
child.planNode.phrase
|
||||||
|
)
|
||||||
|
|
||||||
|
if child.messages and #child.messages > 0 then
|
||||||
|
str = str .. "\n " .. (" "):rep(3 * level) .. table.concat(child.messages, "\n " .. (" "):rep(3 * level))
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(buffer, str)
|
||||||
|
else
|
||||||
|
local str = ("%s%s"):format(
|
||||||
|
(" "):rep(3 * level),
|
||||||
|
child.planNode.phrase or ""
|
||||||
|
)
|
||||||
|
|
||||||
|
if child.status then
|
||||||
|
str = str .. (" (%s)"):format(child.status)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(buffer, str)
|
||||||
|
|
||||||
|
if #child.children > 0 then
|
||||||
|
local text = self:visualize(child, level + 1)
|
||||||
|
table.insert(buffer, text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(buffer, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
return TestResults
|
188
TestEZ/TestRunner.lua
Normal file
188
TestEZ/TestRunner.lua
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
--[[
|
||||||
|
Contains the logic to run a test plan and gather test results from it.
|
||||||
|
|
||||||
|
TestRunner accepts a TestPlan object, executes the planned tests, and
|
||||||
|
produces a TestResults object. While the tests are running, the system's
|
||||||
|
state is contained inside a TestSession object.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local TestEnum = require(script.Parent.TestEnum)
|
||||||
|
local TestSession = require(script.Parent.TestSession)
|
||||||
|
local LifecycleHooks = require(script.Parent.LifecycleHooks)
|
||||||
|
|
||||||
|
local RUNNING_GLOBAL = "__TESTEZ_RUNNING_TEST__"
|
||||||
|
|
||||||
|
local TestRunner = {
|
||||||
|
environment = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
local function wrapExpectContextWithPublicApi(expectationContext)
|
||||||
|
return setmetatable({
|
||||||
|
extend = function(...)
|
||||||
|
expectationContext:extend(...)
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
__call = function(_self, ...)
|
||||||
|
return expectationContext:startExpectationChain(...)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Runs the given TestPlan and returns a TestResults object representing the
|
||||||
|
results of the run.
|
||||||
|
]]
|
||||||
|
function TestRunner.runPlan(plan)
|
||||||
|
local session = TestSession.new(plan)
|
||||||
|
local lifecycleHooks = LifecycleHooks.new()
|
||||||
|
|
||||||
|
local exclusiveNodes = plan:findNodes(function(node)
|
||||||
|
return node.modifier == TestEnum.NodeModifier.Focus
|
||||||
|
end)
|
||||||
|
|
||||||
|
session.hasFocusNodes = #exclusiveNodes > 0
|
||||||
|
|
||||||
|
TestRunner.runPlanNode(session, plan, lifecycleHooks)
|
||||||
|
|
||||||
|
return session:finalize()
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Run the given test plan node and its descendants, using the given test
|
||||||
|
session to store all of the results.
|
||||||
|
]]
|
||||||
|
function TestRunner.runPlanNode(session, planNode, lifecycleHooks)
|
||||||
|
local function runCallback(callback, messagePrefix)
|
||||||
|
local success = true
|
||||||
|
local errorMessage
|
||||||
|
-- Any code can check RUNNING_GLOBAL to fork behavior based on
|
||||||
|
-- whether a test is running. We use this to avoid accessing
|
||||||
|
-- protected APIs; it's a workaround that will go away someday.
|
||||||
|
_G[RUNNING_GLOBAL] = true
|
||||||
|
|
||||||
|
messagePrefix = messagePrefix or ""
|
||||||
|
|
||||||
|
local testEnvironment = getfenv(callback)
|
||||||
|
|
||||||
|
for key, value in pairs(TestRunner.environment) do
|
||||||
|
testEnvironment[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
testEnvironment.fail = function(message)
|
||||||
|
if message == nil then
|
||||||
|
message = "fail() was called."
|
||||||
|
end
|
||||||
|
|
||||||
|
success = false
|
||||||
|
errorMessage = messagePrefix .. debug.traceback(tostring(message), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
testEnvironment.expect = wrapExpectContextWithPublicApi(session:getExpectationContext())
|
||||||
|
|
||||||
|
local context = session:getContext()
|
||||||
|
|
||||||
|
local nodeSuccess, nodeResult = xpcall(
|
||||||
|
function()
|
||||||
|
callback(context)
|
||||||
|
end,
|
||||||
|
function(message)
|
||||||
|
return messagePrefix .. debug.traceback(tostring(message), 2)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
-- If a node threw an error, we prefer to use that message over
|
||||||
|
-- one created by fail() if it was set.
|
||||||
|
if not nodeSuccess then
|
||||||
|
success = false
|
||||||
|
errorMessage = nodeResult
|
||||||
|
end
|
||||||
|
|
||||||
|
_G[RUNNING_GLOBAL] = nil
|
||||||
|
|
||||||
|
return success, errorMessage
|
||||||
|
end
|
||||||
|
|
||||||
|
local function runNode(childPlanNode)
|
||||||
|
-- Errors can be set either via `error` propagating upwards or
|
||||||
|
-- by a test calling fail([message]).
|
||||||
|
|
||||||
|
for _, hook in ipairs(lifecycleHooks:getBeforeEachHooks()) do
|
||||||
|
local success, errorMessage = runCallback(hook, "beforeEach hook: ")
|
||||||
|
if not success then
|
||||||
|
return false, errorMessage
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local testSuccess, testErrorMessage = runCallback(childPlanNode.callback)
|
||||||
|
|
||||||
|
for _, hook in ipairs(lifecycleHooks:getAfterEachHooks()) do
|
||||||
|
local success, errorMessage = runCallback(hook, "afterEach hook: ")
|
||||||
|
if not success then
|
||||||
|
if not testSuccess then
|
||||||
|
return false, testErrorMessage .. "\nWhile cleaning up the failed test another error was found:\n" .. errorMessage
|
||||||
|
end
|
||||||
|
return false, errorMessage
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not testSuccess then
|
||||||
|
return false, testErrorMessage
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
lifecycleHooks:pushHooksFrom(planNode)
|
||||||
|
|
||||||
|
local halt = false
|
||||||
|
for _, hook in ipairs(lifecycleHooks:getBeforeAllHooks()) do
|
||||||
|
local success, errorMessage = runCallback(hook, "beforeAll hook: ")
|
||||||
|
if not success then
|
||||||
|
session:addDummyError("beforeAll", errorMessage)
|
||||||
|
halt = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not halt then
|
||||||
|
for _, childPlanNode in ipairs(planNode.children) do
|
||||||
|
if childPlanNode.type == TestEnum.NodeType.It then
|
||||||
|
session:pushNode(childPlanNode)
|
||||||
|
if session:shouldSkip() then
|
||||||
|
session:setSkipped()
|
||||||
|
else
|
||||||
|
local success, errorMessage = runNode(childPlanNode)
|
||||||
|
|
||||||
|
if success then
|
||||||
|
session:setSuccess()
|
||||||
|
else
|
||||||
|
session:setError(errorMessage)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
session:popNode()
|
||||||
|
elseif childPlanNode.type == TestEnum.NodeType.Describe then
|
||||||
|
session:pushNode(childPlanNode)
|
||||||
|
TestRunner.runPlanNode(session, childPlanNode, lifecycleHooks)
|
||||||
|
|
||||||
|
-- Did we have an error trying build a test plan?
|
||||||
|
if childPlanNode.loadError then
|
||||||
|
local message = "Error during planning: " .. childPlanNode.loadError
|
||||||
|
session:setError(message)
|
||||||
|
else
|
||||||
|
session:setStatusFromChildren()
|
||||||
|
end
|
||||||
|
session:popNode()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, hook in ipairs(lifecycleHooks:getAfterAllHooks()) do
|
||||||
|
local success, errorMessage = runCallback(hook, "afterAll hook: ")
|
||||||
|
if not success then
|
||||||
|
session:addDummyError("afterAll", errorMessage)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
lifecycleHooks:popHooks()
|
||||||
|
end
|
||||||
|
|
||||||
|
return TestRunner
|
243
TestEZ/TestSession.lua
Normal file
243
TestEZ/TestSession.lua
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
--[[
|
||||||
|
Represents the state relevant while executing a test plan.
|
||||||
|
|
||||||
|
Used by TestRunner to produce a TestResults object.
|
||||||
|
|
||||||
|
Uses the same tree building structure as TestPlanBuilder; TestSession keeps
|
||||||
|
track of a stack of nodes that represent the current path through the tree.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local TestEnum = require(script.Parent.TestEnum)
|
||||||
|
local TestResults = require(script.Parent.TestResults)
|
||||||
|
local Context = require(script.Parent.Context)
|
||||||
|
local ExpectationContext = require(script.Parent.ExpectationContext)
|
||||||
|
|
||||||
|
local TestSession = {}
|
||||||
|
|
||||||
|
TestSession.__index = TestSession
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Create a TestSession related to the given TestPlan.
|
||||||
|
|
||||||
|
The resulting TestResults object will be linked to this TestPlan.
|
||||||
|
]]
|
||||||
|
function TestSession.new(plan)
|
||||||
|
local self = {
|
||||||
|
results = TestResults.new(plan),
|
||||||
|
nodeStack = {},
|
||||||
|
contextStack = {},
|
||||||
|
expectationContextStack = {},
|
||||||
|
hasFocusNodes = false
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(self, TestSession)
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Calculate success, failure, and skipped test counts in the tree at the
|
||||||
|
current point in the execution.
|
||||||
|
]]
|
||||||
|
function TestSession:calculateTotals()
|
||||||
|
local results = self.results
|
||||||
|
|
||||||
|
results.successCount = 0
|
||||||
|
results.failureCount = 0
|
||||||
|
results.skippedCount = 0
|
||||||
|
|
||||||
|
results:visitAllNodes(function(node)
|
||||||
|
local status = node.status
|
||||||
|
local nodeType = node.planNode.type
|
||||||
|
|
||||||
|
if nodeType == TestEnum.NodeType.It then
|
||||||
|
if status == TestEnum.TestStatus.Success then
|
||||||
|
results.successCount = results.successCount + 1
|
||||||
|
elseif status == TestEnum.TestStatus.Failure then
|
||||||
|
results.failureCount = results.failureCount + 1
|
||||||
|
elseif status == TestEnum.TestStatus.Skipped then
|
||||||
|
results.skippedCount = results.skippedCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Gathers all of the errors reported by tests and puts them at the top level
|
||||||
|
of the TestResults object.
|
||||||
|
]]
|
||||||
|
function TestSession:gatherErrors()
|
||||||
|
local results = self.results
|
||||||
|
|
||||||
|
results.errors = {}
|
||||||
|
|
||||||
|
results:visitAllNodes(function(node)
|
||||||
|
if #node.errors > 0 then
|
||||||
|
for _, message in ipairs(node.errors) do
|
||||||
|
table.insert(results.errors, message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Calculates test totals, verifies the tree is valid, and returns results.
|
||||||
|
]]
|
||||||
|
function TestSession:finalize()
|
||||||
|
if #self.nodeStack ~= 0 then
|
||||||
|
error("Cannot finalize TestResults with nodes still on the stack!", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:calculateTotals()
|
||||||
|
self:gatherErrors()
|
||||||
|
|
||||||
|
return self.results
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Create a new test result node and push it onto the navigation stack.
|
||||||
|
]]
|
||||||
|
function TestSession:pushNode(planNode)
|
||||||
|
local node = TestResults.createNode(planNode)
|
||||||
|
local lastNode = self.nodeStack[#self.nodeStack] or self.results
|
||||||
|
table.insert(lastNode.children, node)
|
||||||
|
table.insert(self.nodeStack, node)
|
||||||
|
|
||||||
|
local lastContext = self.contextStack[#self.contextStack]
|
||||||
|
local context = Context.new(lastContext)
|
||||||
|
table.insert(self.contextStack, context)
|
||||||
|
|
||||||
|
local lastExpectationContext = self.expectationContextStack[#self.expectationContextStack]
|
||||||
|
local expectationContext = ExpectationContext.new(lastExpectationContext)
|
||||||
|
table.insert(self.expectationContextStack, expectationContext)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Pops a node off of the navigation stack.
|
||||||
|
]]
|
||||||
|
function TestSession:popNode()
|
||||||
|
assert(#self.nodeStack > 0, "Tried to pop from an empty node stack!")
|
||||||
|
table.remove(self.nodeStack, #self.nodeStack)
|
||||||
|
table.remove(self.contextStack, #self.contextStack)
|
||||||
|
table.remove(self.expectationContextStack, #self.expectationContextStack)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Gets the Context object for the current node.
|
||||||
|
]]
|
||||||
|
function TestSession:getContext()
|
||||||
|
assert(#self.contextStack > 0, "Tried to get context from an empty stack!")
|
||||||
|
return self.contextStack[#self.contextStack]
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function TestSession:getExpectationContext()
|
||||||
|
assert(#self.expectationContextStack > 0, "Tried to get expectationContext from an empty stack!")
|
||||||
|
return self.expectationContextStack[#self.expectationContextStack]
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Tells whether the current test we're in should be skipped.
|
||||||
|
]]
|
||||||
|
function TestSession:shouldSkip()
|
||||||
|
-- If our test tree had any exclusive tests, then normal tests are skipped!
|
||||||
|
if self.hasFocusNodes then
|
||||||
|
for i = #self.nodeStack, 1, -1 do
|
||||||
|
local node = self.nodeStack[i]
|
||||||
|
|
||||||
|
-- Skipped tests are still skipped
|
||||||
|
if node.planNode.modifier == TestEnum.NodeModifier.Skip then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Focused tests are the only ones that aren't skipped
|
||||||
|
if node.planNode.modifier == TestEnum.NodeModifier.Focus then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
for i = #self.nodeStack, 1, -1 do
|
||||||
|
local node = self.nodeStack[i]
|
||||||
|
|
||||||
|
if node.planNode.modifier == TestEnum.NodeModifier.Skip then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Set the current node's status to Success.
|
||||||
|
]]
|
||||||
|
function TestSession:setSuccess()
|
||||||
|
assert(#self.nodeStack > 0, "Attempting to set success status on empty stack")
|
||||||
|
self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Success
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Set the current node's status to Skipped.
|
||||||
|
]]
|
||||||
|
function TestSession:setSkipped()
|
||||||
|
assert(#self.nodeStack > 0, "Attempting to set skipped status on empty stack")
|
||||||
|
self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Skipped
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Set the current node's status to Failure and adds a message to its list of
|
||||||
|
errors.
|
||||||
|
]]
|
||||||
|
function TestSession:setError(message)
|
||||||
|
assert(#self.nodeStack > 0, "Attempting to set error status on empty stack")
|
||||||
|
local last = self.nodeStack[#self.nodeStack]
|
||||||
|
last.status = TestEnum.TestStatus.Failure
|
||||||
|
table.insert(last.errors, message)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Add a dummy child node to the current node to hold the given error. This
|
||||||
|
allows an otherwise empty describe node to report an error in a more natural
|
||||||
|
way.
|
||||||
|
]]
|
||||||
|
function TestSession:addDummyError(phrase, message)
|
||||||
|
self:pushNode({type = TestEnum.NodeType.It, phrase = phrase})
|
||||||
|
self:setError(message)
|
||||||
|
self:popNode()
|
||||||
|
self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Failure
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Set the current node's status based on that of its children. If all children
|
||||||
|
are skipped, mark it as skipped. If any are fails, mark it as failed.
|
||||||
|
Otherwise, mark it as success.
|
||||||
|
]]
|
||||||
|
function TestSession:setStatusFromChildren()
|
||||||
|
assert(#self.nodeStack > 0, "Attempting to set status from children on empty stack")
|
||||||
|
|
||||||
|
local last = self.nodeStack[#self.nodeStack]
|
||||||
|
local status = TestEnum.TestStatus.Success
|
||||||
|
local skipped = true
|
||||||
|
|
||||||
|
-- If all children were skipped, then we were skipped
|
||||||
|
-- If any child failed, then we failed!
|
||||||
|
for _, child in ipairs(last.children) do
|
||||||
|
if child.status ~= TestEnum.TestStatus.Skipped then
|
||||||
|
skipped = false
|
||||||
|
|
||||||
|
if child.status == TestEnum.TestStatus.Failure then
|
||||||
|
status = TestEnum.TestStatus.Failure
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if skipped then
|
||||||
|
status = TestEnum.TestStatus.Skipped
|
||||||
|
end
|
||||||
|
|
||||||
|
last.status = status
|
||||||
|
end
|
||||||
|
|
||||||
|
return TestSession
|
42
TestEZ/init.lua
Normal file
42
TestEZ/init.lua
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
|
local Expectation = require(script.Expectation)
|
||||||
|
local TestBootstrap = require(script.TestBootstrap)
|
||||||
|
local TestEnum = require(script.TestEnum)
|
||||||
|
local TestPlan = require(script.TestPlan)
|
||||||
|
local TestPlanner = require(script.TestPlanner)
|
||||||
|
local TestResults = require(script.TestResults)
|
||||||
|
local TestRunner = require(script.TestRunner)
|
||||||
|
local TestSession = require(script.TestSession)
|
||||||
|
local TextReporter = require(script.Reporters.TextReporter)
|
||||||
|
local TextReporterQuiet = require(script.Reporters.TextReporterQuiet)
|
||||||
|
local TeamCityReporter = require(script.Reporters.TeamCityReporter)
|
||||||
|
|
||||||
|
local function run(testRoot, callback)
|
||||||
|
local modules = TestBootstrap:getModules(testRoot)
|
||||||
|
local plan = TestPlanner.createPlan(modules)
|
||||||
|
local results = TestRunner.runPlan(plan)
|
||||||
|
|
||||||
|
callback(results)
|
||||||
|
end
|
||||||
|
|
||||||
|
local TestEZ = {
|
||||||
|
run = run,
|
||||||
|
|
||||||
|
Expectation = Expectation,
|
||||||
|
TestBootstrap = TestBootstrap,
|
||||||
|
TestEnum = TestEnum,
|
||||||
|
TestPlan = TestPlan,
|
||||||
|
TestPlanner = TestPlanner,
|
||||||
|
TestResults = TestResults,
|
||||||
|
TestRunner = TestRunner,
|
||||||
|
TestSession = TestSession,
|
||||||
|
|
||||||
|
Reporters = {
|
||||||
|
TextReporter = TextReporter,
|
||||||
|
TextReporterQuiet = TextReporterQuiet,
|
||||||
|
TeamCityReporter = TeamCityReporter,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return TestEZ
|
BIN
Warp.rbxm
BIN
Warp.rbxm
Binary file not shown.
|
@ -3,5 +3,5 @@
|
||||||
|
|
||||||
# To add a new tool, add an entry to this table.
|
# To add a new tool, add an entry to this table.
|
||||||
[tools]
|
[tools]
|
||||||
rojo = "rojo-rbx/rojo@7.4.0"
|
rojo = "rojo-rbx/rojo@7.4.1"
|
||||||
wally = "UpliftGames/wally@0.3.2"
|
wally = "UpliftGames/wally@0.3.2"
|
||||||
|
|
|
@ -87,7 +87,7 @@ Disconnect the event connection.
|
||||||
```lua [Variable]
|
```lua [Variable]
|
||||||
(
|
(
|
||||||
key: string
|
key: string
|
||||||
)
|
): boolean
|
||||||
```
|
```
|
||||||
|
|
||||||
```lua [Example]
|
```lua [Example]
|
||||||
|
@ -128,7 +128,7 @@ This function have rate limiting it self and configured from server.
|
||||||
|
|
||||||
## `:Invoke` <Badge type="warning" text="yield" />
|
## `:Invoke` <Badge type="warning" text="yield" />
|
||||||
|
|
||||||
Semiliar to `:InvokeServer`, its for Invoke to a server.
|
Semiliar to `:InvokeServer`, but it have timeout system that not exists on `RemoteFunction.InvokeServer`.
|
||||||
|
|
||||||
::: code-group
|
::: code-group
|
||||||
```lua [Variable]
|
```lua [Variable]
|
||||||
|
@ -139,7 +139,7 @@ Semiliar to `:InvokeServer`, its for Invoke to a server.
|
||||||
```
|
```
|
||||||
|
|
||||||
```lua [Example]
|
```lua [Example]
|
||||||
local Request = Remote:Invoke(2, "Hello World!")
|
local Request = Remote:Invoke(2, "Hello World!") -- this yield until it response
|
||||||
```
|
```
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ This function is yielded, once it timeout it will return nil.
|
||||||
Wait the event being triggered.
|
Wait the event being triggered.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
Remote:Wait()
|
Remote:Wait() -- :Wait return number value
|
||||||
```
|
```
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
|
@ -161,7 +161,7 @@ This function is yielded, Invoke might also ping this one and also causing error
|
||||||
|
|
||||||
## `:Destroy`
|
## `:Destroy`
|
||||||
|
|
||||||
Disconnect all connection of event and remove the event from Warp
|
Disconnect all connection of event and remove the event from Warp list
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
Remote:Destroy()
|
Remote:Destroy()
|
||||||
|
|
|
@ -13,8 +13,10 @@ When creating a event on Server, you can add second argument (optional) as table
|
||||||
-- Server
|
-- Server
|
||||||
-- Let's make the event have ratelimit with max 50 entrance for 2 seconds.
|
-- Let's make the event have ratelimit with max 50 entrance for 2 seconds.
|
||||||
local Remote = Warp.Server("Remote1", {
|
local Remote = Warp.Server("Remote1", {
|
||||||
maxEntrance = 50, -- maximum 50 fires.
|
rateLimit = {
|
||||||
interval = 2, -- 2 seconds
|
maxEntrance = 50, -- maximum 50 fires.
|
||||||
|
interval = 2, -- 2 seconds
|
||||||
|
}
|
||||||
})
|
})
|
||||||
-- Now the Event RateLimit is configured, and ready to use.
|
-- Now the Event RateLimit is configured, and ready to use.
|
||||||
-- No need anything to adds on client side.
|
-- No need anything to adds on client side.
|
||||||
|
|
|
@ -36,12 +36,16 @@ Create new Warp events with array.
|
||||||
```lua [Example]
|
```lua [Example]
|
||||||
local Events = Warp.fromServerArray({
|
local Events = Warp.fromServerArray({
|
||||||
["Remote1"] = {
|
["Remote1"] = {
|
||||||
maxEntrance: 50,
|
rateLimit = {
|
||||||
interval: 1,
|
maxEntrance: 50,
|
||||||
|
interval: 1,
|
||||||
|
}
|
||||||
}, -- with rateLimit configuration
|
}, -- with rateLimit configuration
|
||||||
"Remote2", -- without rateLimit configuration
|
"Remote2", -- without rateLimit configuration
|
||||||
["Remote3"] = {
|
["Remote3"] = {
|
||||||
maxEntrance: 10,
|
rateLimit = {
|
||||||
|
maxEntrance: 10,
|
||||||
|
}
|
||||||
}, -- with rateLimit configuration
|
}, -- with rateLimit configuration
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -97,7 +101,7 @@ Disconnect the event connection.
|
||||||
```lua [Variable]
|
```lua [Variable]
|
||||||
(
|
(
|
||||||
key: string
|
key: string
|
||||||
)
|
): boolean
|
||||||
```
|
```
|
||||||
|
|
||||||
```lua [Example]
|
```lua [Example]
|
||||||
|
@ -150,9 +154,27 @@ Remote:Fires(true, "Hello World!")
|
||||||
```
|
```
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## `:FireExcept` <Badge type="tip" text="Server Only" />
|
||||||
|
|
||||||
|
Fire the event to all clients but except a players.
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
```lua [Variable]
|
||||||
|
(
|
||||||
|
reliable: boolean,
|
||||||
|
except: { Player },
|
||||||
|
...: any
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua [Example]
|
||||||
|
Remote:FireExcept(true, { Players.Eternity_Devs, Players.Player2 }, "Hello World!") -- this will sent to all players except { Players.Eternity_Devs, Players.Player2 }.
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
## `:Invoke` <Badge type="warning" text="yield" />
|
## `:Invoke` <Badge type="warning" text="yield" />
|
||||||
|
|
||||||
Semiliar to `:InvokeClient`, its for Invoke to a client.
|
Semiliar to `:InvokeClient`, but it have timeout system that not exists on `RemoteFunction.InvokeClient`.
|
||||||
|
|
||||||
::: code-group
|
::: code-group
|
||||||
```lua [Variable]
|
```lua [Variable]
|
||||||
|
@ -177,7 +199,7 @@ This function is yielded, once it timeout it will return nil.
|
||||||
Wait the event being triggered.
|
Wait the event being triggered.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
Remote:Wait()
|
Remote:Wait() -- :Wait return number value
|
||||||
```
|
```
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
|
|
|
@ -102,7 +102,23 @@ Signal1:DisconnectAll()
|
||||||
|
|
||||||
## `:Fire`
|
## `:Fire`
|
||||||
|
|
||||||
Fire the signal.
|
Fire the signal (Immediate)
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
```lua [Variable]
|
||||||
|
(
|
||||||
|
...: any
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua [Example]
|
||||||
|
Signal1:Fire("Hello World!")
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
## `:DeferFire`
|
||||||
|
|
||||||
|
Fire the signal (Deferred)
|
||||||
|
|
||||||
::: code-group
|
::: code-group
|
||||||
```lua [Variable]
|
```lua [Variable]
|
||||||
|
@ -122,7 +138,7 @@ This uses `pcall`, which means it never error (safe-mode, sacrificed debugging),
|
||||||
|
|
||||||
## `:FireTo`
|
## `:FireTo`
|
||||||
|
|
||||||
Fire to other signal, this also use `:Fire`.
|
Fire to other signal, this uses `:Fire`.
|
||||||
|
|
||||||
::: code-group
|
::: code-group
|
||||||
```lua [Variable]
|
```lua [Variable]
|
||||||
|
|
|
@ -68,3 +68,4 @@ Pong:Destroy()
|
||||||
|
|
||||||
-- Yay Done!
|
-- Yay Done!
|
||||||
```
|
```
|
||||||
|
:::
|
|
@ -8,7 +8,7 @@
|
||||||
::: code-group
|
::: code-group
|
||||||
```toml [wally.toml]
|
```toml [wally.toml]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
warp = "imezx/warp@1.0.5"
|
warp = "imezx/warp@1.0.13"
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Run `wally install` in command.
|
3. Run `wally install` in command.
|
||||||
|
|
3
runTests.server.luau
Normal file
3
runTests.server.luau
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
require(script.Parent.TestEZ).TestBootstrap:run({
|
||||||
|
game:GetService("ServerScriptService").Test
|
||||||
|
})
|
|
@ -1,208 +0,0 @@
|
||||||
--!native
|
|
||||||
--!strict
|
|
||||||
local ClientProcess = {}
|
|
||||||
|
|
||||||
local RunService = game:GetService("RunService")
|
|
||||||
local Util = script.Parent.Parent.Util
|
|
||||||
|
|
||||||
local Type = require(script.Parent.Parent.Type)
|
|
||||||
local Event = require(script.Parent.Parent.Event)
|
|
||||||
local Spawn = require(Util.Spawn)
|
|
||||||
local Key = require(Util.Key)
|
|
||||||
local RateLimit = require(Util.RateLimit)
|
|
||||||
|
|
||||||
local clientRatelimit: Type.StoredRatelimit = {}
|
|
||||||
local clientQueue: Type.QueueMap = {}
|
|
||||||
local unreliableClientQueue: Type.QueueMap = {}
|
|
||||||
local clientCallback: Type.CallbackMap = {}
|
|
||||||
local clientRequestQueue: Type.QueueMap = {}
|
|
||||||
|
|
||||||
local queueIn: {
|
|
||||||
[string]: {any}
|
|
||||||
} = {}
|
|
||||||
local queueInRequest: {
|
|
||||||
[number]: {
|
|
||||||
[string]: {
|
|
||||||
any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} = {}
|
|
||||||
local queueOutRequest: {
|
|
||||||
[number]: {
|
|
||||||
[string]: {
|
|
||||||
any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} = {}
|
|
||||||
local incoming_cache: {
|
|
||||||
[string]: {
|
|
||||||
any
|
|
||||||
}
|
|
||||||
} = {}
|
|
||||||
|
|
||||||
queueInRequest[1] = {}
|
|
||||||
queueInRequest[2] = {}
|
|
||||||
queueOutRequest[1] = {}
|
|
||||||
queueOutRequest[2] = {}
|
|
||||||
|
|
||||||
local ReliableEvent = Event.Reliable
|
|
||||||
local UnreliableEvent = Event.Unreliable
|
|
||||||
local RequestEvent = Event.Request
|
|
||||||
|
|
||||||
function ClientProcess.insertQueue(Identifier: string, reliable: boolean, ...: any)
|
|
||||||
if not reliable then
|
|
||||||
table.insert(unreliableClientQueue[Identifier], { ... })
|
|
||||||
return
|
|
||||||
end
|
|
||||||
table.insert(clientQueue[Identifier], { ... })
|
|
||||||
end
|
|
||||||
|
|
||||||
function ClientProcess.insertRequest(Identifier: string, timeout: number, ...: any)
|
|
||||||
local yieldThread: thread, start = coroutine.running(), os.clock()
|
|
||||||
local cancel = task.delay(timeout, function()
|
|
||||||
task.spawn(yieldThread, nil)
|
|
||||||
end)
|
|
||||||
table.insert(clientRequestQueue[Identifier], { tostring(Key()), function(...: any)
|
|
||||||
if (os.clock() - start) > timeout then return end
|
|
||||||
task.cancel(cancel)
|
|
||||||
task.spawn(yieldThread, ...)
|
|
||||||
end :: any, { ... } :: any })
|
|
||||||
return coroutine.yield()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ClientProcess.add(Identifier: string, originId: string)
|
|
||||||
if not clientQueue[Identifier] then
|
|
||||||
clientRatelimit[Identifier] = RateLimit.create(originId)
|
|
||||||
clientQueue[Identifier] = {}
|
|
||||||
unreliableClientQueue[Identifier] = {}
|
|
||||||
clientRequestQueue[Identifier] = {}
|
|
||||||
clientCallback[Identifier] = {}
|
|
||||||
|
|
||||||
queueOutRequest[1][Identifier] = {}
|
|
||||||
queueOutRequest[2][Identifier] = {}
|
|
||||||
queueInRequest[1][Identifier] = {}
|
|
||||||
queueInRequest[2][Identifier] = {}
|
|
||||||
queueIn[Identifier] = {}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ClientProcess.addCallback(Identifier: string, key: string, callback)
|
|
||||||
clientCallback[Identifier][key] = callback
|
|
||||||
end
|
|
||||||
|
|
||||||
function ClientProcess.removeCallback(Identifier: string, key: string)
|
|
||||||
clientCallback[Identifier][key] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function ClientProcess.start()
|
|
||||||
RunService.PostSimulation:Connect(function()
|
|
||||||
for Identifier: string, data: any in unreliableClientQueue do
|
|
||||||
if #data > 0 then
|
|
||||||
if clientRatelimit[Identifier](#data) then
|
|
||||||
UnreliableEvent:FireServer(Identifier, data)
|
|
||||||
end
|
|
||||||
table.clear(data)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for Identifier: string, data: any in clientQueue do
|
|
||||||
if #data > 0 then
|
|
||||||
if clientRatelimit[Identifier](#data) then
|
|
||||||
ReliableEvent:FireServer(Identifier, data)
|
|
||||||
end
|
|
||||||
table.clear(data)
|
|
||||||
end
|
|
||||||
if #clientRequestQueue[Identifier] > 0 then
|
|
||||||
for _, requestData in clientRequestQueue[Identifier] do
|
|
||||||
if not requestData[3] then continue end
|
|
||||||
table.insert(queueOutRequest[1][Identifier], { requestData[1], requestData[3] })
|
|
||||||
table.remove(requestData, #requestData)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if incoming_cache[Identifier] then
|
|
||||||
for _, packet in incoming_cache[Identifier] do
|
|
||||||
if not queueIn[Identifier] then continue end
|
|
||||||
table.insert(queueIn[Identifier], table.clone(packet))
|
|
||||||
table.clear(incoming_cache[Identifier])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if clientCallback[Identifier] then
|
|
||||||
if #queueIn[Identifier] > 0 then
|
|
||||||
for _, packedDatas: any in queueIn[Identifier] do
|
|
||||||
if #packedDatas == 0 then continue end
|
|
||||||
for _, v: any in packedDatas do
|
|
||||||
for _, fn: any in clientCallback[Identifier] do
|
|
||||||
Spawn(fn, table.unpack(v))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.clear(queueIn[Identifier])
|
|
||||||
end
|
|
||||||
if #queueInRequest[1][Identifier] > 0 then
|
|
||||||
for idx, packetDatas: any in queueInRequest[1][Identifier] do
|
|
||||||
if #packetDatas == 0 then continue end
|
|
||||||
for _, packetData in packetDatas do
|
|
||||||
for _, fn: any in clientCallback[Identifier] do
|
|
||||||
Spawn(function()
|
|
||||||
local requestReturn = { fn(table.unpack(packetData[2])) }
|
|
||||||
table.insert(queueOutRequest[2][Identifier], { packetData[1], requestReturn })
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.clear(queueInRequest[1][Identifier])
|
|
||||||
end
|
|
||||||
if #queueInRequest[2][Identifier] > 0 then
|
|
||||||
for _, packetDatas: any in queueInRequest[2][Identifier] do
|
|
||||||
for _, packetData in packetDatas do
|
|
||||||
if #packetData == 1 then continue end
|
|
||||||
for idx, clientRequest in clientRequestQueue[Identifier] do
|
|
||||||
if clientRequest[1] == packetData[1] then
|
|
||||||
Spawn(clientRequest[2], table.unpack(packetData[2]))
|
|
||||||
table.remove(clientRequestQueue[Identifier], idx)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.clear(queueInRequest[2][Identifier])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for Identifier: string, requestsData in queueOutRequest[1] do
|
|
||||||
if #requestsData == 0 then continue end
|
|
||||||
RequestEvent:FireServer(Identifier, "\1", requestsData)
|
|
||||||
table.clear(queueOutRequest[1][Identifier])
|
|
||||||
end
|
|
||||||
for Identifier: string, requestsData in queueOutRequest[2] do
|
|
||||||
if #requestsData == 0 then continue end
|
|
||||||
RequestEvent:FireServer(Identifier, "\0", requestsData)
|
|
||||||
table.clear(queueOutRequest[2][Identifier])
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
local function onClientNetworkReceive(Identifier: string, data: any)
|
|
||||||
if not Identifier or not data then return end
|
|
||||||
if not queueIn[Identifier] then
|
|
||||||
queueIn[Identifier] = {}
|
|
||||||
end
|
|
||||||
if not clientCallback[Identifier] then
|
|
||||||
if not incoming_cache[Identifier] then
|
|
||||||
incoming_cache[Identifier] = {}
|
|
||||||
end
|
|
||||||
table.insert(incoming_cache[Identifier], data)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
table.insert(queueIn[Identifier], data)
|
|
||||||
end
|
|
||||||
ReliableEvent.OnClientEvent:Connect(onClientNetworkReceive)
|
|
||||||
UnreliableEvent.OnClientEvent:Connect(onClientNetworkReceive)
|
|
||||||
RequestEvent.OnClientEvent:Connect(function(Identifier: string, action: string, returnDatas)
|
|
||||||
if not Identifier or not returnDatas then return end
|
|
||||||
if action == "\1" then
|
|
||||||
table.insert(queueInRequest[1][Identifier], returnDatas)
|
|
||||||
else
|
|
||||||
table.insert(queueInRequest[2][Identifier], returnDatas)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return ClientProcess
|
|
264
src/Index/Client/ClientProcess/init.luau
Normal file
264
src/Index/Client/ClientProcess/init.luau
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
--!native
|
||||||
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
|
local ClientProcess = {}
|
||||||
|
|
||||||
|
local RunService = game:GetService("RunService")
|
||||||
|
local Util = script.Parent.Parent.Util
|
||||||
|
|
||||||
|
local Type = require(script.Parent.Parent.Type)
|
||||||
|
local Event = require(script.Parent.Parent.Event)
|
||||||
|
local Spawn = require(Util.Spawn)
|
||||||
|
local Key = require(Util.Key)
|
||||||
|
local RateLimit = require(Util.RateLimit)
|
||||||
|
local Buffer = require(Util.Buffer)
|
||||||
|
|
||||||
|
local clientRatelimit: Type.StoredRatelimit = {}
|
||||||
|
local clientQueue: Type.QueueMap = {}
|
||||||
|
local unreliableClientQueue: Type.QueueMap = {}
|
||||||
|
local clientCallback: Type.CallbackMap = {}
|
||||||
|
local clientRequestQueue: Type.QueueMap = {}
|
||||||
|
local registeredIdentifier: { [string]: boolean } = {}
|
||||||
|
|
||||||
|
local queueInRequest: {
|
||||||
|
[number]: {
|
||||||
|
[string]: {
|
||||||
|
any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = {}
|
||||||
|
local queueOutRequest: {
|
||||||
|
[number]: {
|
||||||
|
[string]: {
|
||||||
|
any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
queueInRequest[1] = {}
|
||||||
|
queueInRequest[2] = {}
|
||||||
|
queueOutRequest[1] = {}
|
||||||
|
queueOutRequest[2] = {}
|
||||||
|
|
||||||
|
local ReliableEvent = Event.Reliable
|
||||||
|
local UnreliableEvent = Event.Unreliable
|
||||||
|
local RequestEvent = Event.Request
|
||||||
|
|
||||||
|
function ClientProcess.insertQueue(Identifier: string, reliable: boolean, ...: any)
|
||||||
|
if not reliable then
|
||||||
|
if not unreliableClientQueue[Identifier] then
|
||||||
|
unreliableClientQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
table.insert(unreliableClientQueue[Identifier], { ... })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not clientQueue[Identifier] then
|
||||||
|
clientQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
table.insert(clientQueue[Identifier], { ... })
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClientProcess.insertRequest(Identifier: string, timeout: number, ...: any)
|
||||||
|
if not clientRequestQueue[Identifier] then
|
||||||
|
clientRequestQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
local yieldThread: thread, start = coroutine.running(), os.clock()
|
||||||
|
local cancel = task.delay(timeout, function()
|
||||||
|
task.spawn(yieldThread, nil)
|
||||||
|
end)
|
||||||
|
table.insert(clientRequestQueue[Identifier], { tostring(Key()), function(...: any)
|
||||||
|
if (os.clock() - start) > timeout then return end
|
||||||
|
task.cancel(cancel)
|
||||||
|
task.spawn(yieldThread, ...)
|
||||||
|
end :: any, { ... } :: any })
|
||||||
|
return coroutine.yield()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClientProcess.add(Identifier: any, originId: string, conf: Type.ClientConf)
|
||||||
|
if not registeredIdentifier[Identifier] then
|
||||||
|
registeredIdentifier[Identifier] = true
|
||||||
|
|
||||||
|
if not clientRatelimit[Identifier] then
|
||||||
|
clientRatelimit[Identifier] = RateLimit.create(originId)
|
||||||
|
end
|
||||||
|
if not clientQueue[Identifier] then
|
||||||
|
clientQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
if not unreliableClientQueue[Identifier] then
|
||||||
|
unreliableClientQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
if not clientRequestQueue[Identifier] then
|
||||||
|
clientRequestQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
if not clientCallback[Identifier] then
|
||||||
|
clientCallback[Identifier] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
if not queueOutRequest[1][Identifier] then
|
||||||
|
queueOutRequest[1][Identifier] = {}
|
||||||
|
end
|
||||||
|
if not queueOutRequest[2][Identifier] then
|
||||||
|
queueOutRequest[2][Identifier] = {}
|
||||||
|
end
|
||||||
|
if not queueInRequest[1][Identifier] then
|
||||||
|
queueInRequest[1][Identifier] = {}
|
||||||
|
end
|
||||||
|
if not queueInRequest[2][Identifier] then
|
||||||
|
queueInRequest[2][Identifier] = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClientProcess.remove(Identifier: string)
|
||||||
|
if not registeredIdentifier[Identifier] then return end
|
||||||
|
registeredIdentifier[Identifier] = nil
|
||||||
|
clientQueue[Identifier] = nil
|
||||||
|
unreliableClientQueue[Identifier] = nil
|
||||||
|
clientRequestQueue[Identifier] = nil
|
||||||
|
clientCallback[Identifier] = nil
|
||||||
|
clientRatelimit[Identifier] = nil
|
||||||
|
queueOutRequest[1][Identifier] = nil
|
||||||
|
queueOutRequest[2][Identifier] = nil
|
||||||
|
queueInRequest[1][Identifier] = nil
|
||||||
|
queueInRequest[2][Identifier] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClientProcess.addCallback(Identifier: string, key: string, callback)
|
||||||
|
clientCallback[Identifier][key] = callback
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClientProcess.removeCallback(Identifier: string, key: string)
|
||||||
|
clientCallback[Identifier][key] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClientProcess.start()
|
||||||
|
debug.setmemorycategory("Warp")
|
||||||
|
RunService.PostSimulation:Connect(function()
|
||||||
|
-- Unreliable
|
||||||
|
for Identifier: string, data: any in unreliableClientQueue do
|
||||||
|
if #data == 0 then continue end
|
||||||
|
if clientRatelimit[Identifier](#data) then
|
||||||
|
for _, unpacked in data do
|
||||||
|
UnreliableEvent:FireServer(Buffer.revert(Identifier), Buffer.write(unpacked))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
unreliableClientQueue[Identifier] = nil
|
||||||
|
end
|
||||||
|
-- Reliable
|
||||||
|
for Identifier: string, data: any in clientQueue do
|
||||||
|
if #data > 0 then
|
||||||
|
if clientRatelimit[Identifier](#data) then
|
||||||
|
for _, unpacked in data do
|
||||||
|
ReliableEvent:FireServer(Buffer.revert(Identifier), Buffer.write(unpacked))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
clientQueue[Identifier] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Sent new invokes
|
||||||
|
for Identifier: string, requestsData in queueOutRequest[1] do
|
||||||
|
if #requestsData == 0 then continue end
|
||||||
|
RequestEvent:FireServer(Buffer.revert(Identifier), "\1", requestsData)
|
||||||
|
queueOutRequest[1][Identifier] = nil
|
||||||
|
end
|
||||||
|
-- Sent returning invokes
|
||||||
|
for Identifier: string, toReturnDatas in queueOutRequest[2] do
|
||||||
|
if #toReturnDatas == 0 then continue end
|
||||||
|
RequestEvent:FireServer(Buffer.revert(Identifier), "\0", toReturnDatas)
|
||||||
|
queueOutRequest[2][Identifier] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
for Identifier: string in registeredIdentifier do
|
||||||
|
if clientRequestQueue[Identifier] then
|
||||||
|
for _, requestData in clientRequestQueue[Identifier] do
|
||||||
|
if not requestData[3] then continue end
|
||||||
|
if not queueOutRequest[1][Identifier] then
|
||||||
|
queueOutRequest[1][Identifier] = {}
|
||||||
|
end
|
||||||
|
table.insert(queueOutRequest[1][Identifier], { requestData[1], requestData[3] })
|
||||||
|
requestData[3] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Unreliable & Reliable
|
||||||
|
local callback = clientCallback[Identifier] or nil
|
||||||
|
if not callback then continue end
|
||||||
|
|
||||||
|
-- Return Invoke
|
||||||
|
if queueInRequest[1][Identifier] then
|
||||||
|
for _, packetDatas: any in queueInRequest[1][Identifier] do
|
||||||
|
if #packetDatas == 0 then continue end
|
||||||
|
for _, fn: any in callback do
|
||||||
|
for i=1,#packetDatas do
|
||||||
|
if not packetDatas[i] then continue end
|
||||||
|
local packetData1 = packetDatas[i][1]
|
||||||
|
local packetData2 = packetDatas[i][2]
|
||||||
|
Spawn(function()
|
||||||
|
local requestReturn = { fn(table.unpack(packetData2)) }
|
||||||
|
if not queueOutRequest[2][Identifier] then
|
||||||
|
queueOutRequest[2][Identifier] = {}
|
||||||
|
end
|
||||||
|
table.insert(queueOutRequest[2][Identifier], { packetData1, requestReturn })
|
||||||
|
packetData1 = nil
|
||||||
|
packetData2 = nil
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
queueInRequest[1][Identifier] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Call to Invoke
|
||||||
|
if queueInRequest[2][Identifier] then
|
||||||
|
if clientRequestQueue[Identifier] then
|
||||||
|
for _, packetDatas: any in queueInRequest[2][Identifier] do
|
||||||
|
for _, packetData in packetDatas do
|
||||||
|
if #packetData == 1 then continue end
|
||||||
|
for y=1,#clientRequestQueue[Identifier] do
|
||||||
|
local clientRequest = clientRequestQueue[Identifier][y]
|
||||||
|
if not clientRequest then continue end
|
||||||
|
if clientRequest[1] == packetData[1] then
|
||||||
|
Spawn(clientRequest[2], table.unpack(packetData[2]))
|
||||||
|
table.remove(clientRequestQueue[Identifier], y)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
queueInRequest[2][Identifier] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
local function onClientNetworkReceive(Identifier: buffer | string, data: buffer, ref: { any }?)
|
||||||
|
if not Identifier or typeof(Identifier) ~= "buffer" or not data or typeof(data) ~= "buffer" then return end
|
||||||
|
Identifier = Buffer.convert(Identifier)
|
||||||
|
if not registeredIdentifier[Identifier :: string] then return end
|
||||||
|
local read = Buffer.read(data, ref)
|
||||||
|
if not read then return end
|
||||||
|
local callback = clientCallback[Identifier :: string]
|
||||||
|
if not callback then return end
|
||||||
|
for _, fn: any in callback do
|
||||||
|
Spawn(fn, table.unpack(read))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ReliableEvent.OnClientEvent:Connect(onClientNetworkReceive)
|
||||||
|
UnreliableEvent.OnClientEvent:Connect(onClientNetworkReceive)
|
||||||
|
RequestEvent.OnClientEvent:Connect(function(Identifier: any, action: string, data)
|
||||||
|
if not Identifier or not data then return end
|
||||||
|
Identifier = Buffer.convert(Identifier)
|
||||||
|
if action == "\1" then
|
||||||
|
if not queueInRequest[1][Identifier] then
|
||||||
|
queueInRequest[1][Identifier] = {}
|
||||||
|
end
|
||||||
|
table.insert(queueInRequest[1][Identifier], data)
|
||||||
|
else
|
||||||
|
if not queueInRequest[2][Identifier] then
|
||||||
|
queueInRequest[2][Identifier] = {}
|
||||||
|
end
|
||||||
|
table.insert(queueInRequest[2][Identifier], data)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ClientProcess
|
|
@ -1,20 +1,32 @@
|
||||||
--!strict
|
--!strict
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
local Client = {}
|
local Client = {}
|
||||||
Client.__index = Client
|
Client.__index = Client
|
||||||
|
|
||||||
local Players = game:GetService("Players")
|
local Players = game:GetService("Players")
|
||||||
local Util = script.Parent.Parent.Util
|
local Util = script.Parent.Parent.Util
|
||||||
|
|
||||||
|
local Type = require(script.Parent.Parent.Type)
|
||||||
local ClientProcess = require(script.Parent.ClientProcess)
|
local ClientProcess = require(script.Parent.ClientProcess)
|
||||||
local Assert = require(Util.Assert)
|
local Assert = require(Util.Assert)
|
||||||
local Key = require(Util.Key)
|
local Key = require(Util.Key)
|
||||||
local Serdes = require(Util.Serdes)
|
local Serdes = require(Util.Serdes)
|
||||||
|
local Buffer = require(Util.Buffer)
|
||||||
|
|
||||||
function Client.new(Identifier: string)
|
function Client.new(Identifier: string, conf: Type.ClientConf?)
|
||||||
local self = setmetatable({}, Client)
|
local self = setmetatable({}, Client)
|
||||||
self.id = Serdes(Identifier)
|
|
||||||
|
self._buffer = Buffer.new()
|
||||||
|
self._buffer:wu8(Serdes.increment(Identifier, conf and conf.yieldWait))
|
||||||
|
self.id = Buffer.convert(self._buffer:build())
|
||||||
self.fn = {}
|
self.fn = {}
|
||||||
ClientProcess.add(self.id, Identifier)
|
self._conf = table.freeze(conf or {})
|
||||||
|
self.IsConnected = false
|
||||||
|
|
||||||
|
ClientProcess.add(self.id, Identifier, conf or { yieldWait = 10 })
|
||||||
|
self._buffer:remove()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -29,6 +41,7 @@ end
|
||||||
function Client:Connect(callback: (args: any) -> ()): string
|
function Client:Connect(callback: (args: any) -> ()): string
|
||||||
local key = tostring(Key())
|
local key = tostring(Key())
|
||||||
table.insert(self.fn, key)
|
table.insert(self.fn, key)
|
||||||
|
self.IsConnected = #self.fn > 0
|
||||||
ClientProcess.addCallback(self.id, key, callback)
|
ClientProcess.addCallback(self.id, key, callback)
|
||||||
return key
|
return key
|
||||||
end
|
end
|
||||||
|
@ -36,7 +49,8 @@ end
|
||||||
function Client:Once(callback: (args: any) -> ()): string
|
function Client:Once(callback: (args: any) -> ()): string
|
||||||
local key = tostring(Key())
|
local key = tostring(Key())
|
||||||
table.insert(self.fn, key)
|
table.insert(self.fn, key)
|
||||||
ClientProcess.addCallback(self.id, key, function(...)
|
self.IsConnected = #self.fn > 0
|
||||||
|
ClientProcess.addCallback(self.id, key, function(...: any?)
|
||||||
self:Disconnect(key)
|
self:Disconnect(key)
|
||||||
task.spawn(callback, ...)
|
task.spawn(callback, ...)
|
||||||
end)
|
end)
|
||||||
|
@ -52,19 +66,24 @@ function Client:Wait()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Client:DisconnectAll()
|
function Client:DisconnectAll()
|
||||||
for idx, key: string in self.fn do
|
for _, key: string in self.fn do
|
||||||
ClientProcess.removeCallback(self.id, key)
|
self:Disconnect(key)
|
||||||
table.remove(self.fn, idx)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Client:Disconnect(key: string)
|
function Client:Disconnect(key: string)
|
||||||
Assert(typeof(key) == "string", "Key must be a string type.")
|
Assert(typeof(key) == "string", "Key must be a string type.")
|
||||||
ClientProcess.removeCallback(self.id, key)
|
ClientProcess.removeCallback(self.id, key)
|
||||||
|
table.remove(self.fn, table.find(self.fn, key))
|
||||||
|
self.IsConnected = #self.fn > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
function Client:Destroy()
|
function Client:Destroy()
|
||||||
self:DisconnectAll()
|
self:DisconnectAll()
|
||||||
|
self._buffer:remove()
|
||||||
|
ClientProcess.remove(self.id)
|
||||||
|
Serdes.decrement()
|
||||||
|
table.clear(self)
|
||||||
setmetatable(self, nil)
|
setmetatable(self, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
--!strict
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
local RunService = game:GetService("RunService")
|
local RunService = game:GetService("RunService")
|
||||||
local Type = require(script.Parent.Type)
|
local Type = require(script.Parent.Type)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
--!strict
|
--!strict
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
local Server = {}
|
local Server = {}
|
||||||
Server.__index = Server
|
Server.__index = Server
|
||||||
|
|
||||||
|
@ -10,12 +12,21 @@ local ServerProcess = require(script.Parent.ServerProcess)
|
||||||
local Assert = require(Util.Assert)
|
local Assert = require(Util.Assert)
|
||||||
local Key = require(Util.Key)
|
local Key = require(Util.Key)
|
||||||
local Serdes = require(Util.Serdes)
|
local Serdes = require(Util.Serdes)
|
||||||
|
local Buffer = require(Util.Buffer)
|
||||||
|
|
||||||
function Server.new(Identifier: string, rateLimit: Type.rateLimitArg?)
|
function Server.new(Identifier: string, conf: Type.ServerConf?)
|
||||||
local self = setmetatable({}, Server)
|
local self = setmetatable({}, Server)
|
||||||
self.id = Serdes(Identifier)
|
|
||||||
|
self._buffer = Buffer.new()
|
||||||
|
self._buffer:wu8(Serdes.increment(Identifier))
|
||||||
|
self.id = Buffer.convert(self._buffer:build())
|
||||||
self.fn = {}
|
self.fn = {}
|
||||||
ServerProcess.add(self.id, Identifier, rateLimit or { maxEntrance = 200, interval = 2 })
|
self._conf = table.freeze(conf or {})
|
||||||
|
self.IsConnected = false
|
||||||
|
|
||||||
|
ServerProcess.add(self.id, Identifier, conf or { rateLimit = { maxEntrance = 200, interval = 2 } })
|
||||||
|
self._buffer:remove()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -29,6 +40,20 @@ function Server:Fires(reliable: boolean, ...: any)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Server:FireExcept(reliable: boolean, except: { Player }, ...: any)
|
||||||
|
for _, player: Player in ipairs(Players:GetPlayers()) do
|
||||||
|
if table.find(except, player) then continue end
|
||||||
|
ServerProcess.insertQueue(self.id, reliable, player, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Server:FireIn(reliable: boolean, range: number, from: Vector3, data: { any }, except: { Player }?)
|
||||||
|
for _, player: Player in ipairs(Players:GetPlayers()) do
|
||||||
|
if (except and table.find(except, player)) or not player.Character or not player.Character.PrimaryPart or (player.Character.PrimaryPart.Position - from).Magnitude < range then continue end
|
||||||
|
ServerProcess.insertQueue(self.id, reliable, player, table.unpack(data))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function Server:Invoke(timeout: number, player: Player, ...: any): any
|
function Server:Invoke(timeout: number, player: Player, ...: any): any
|
||||||
return ServerProcess.insertRequest(self.id, timeout, player, ...)
|
return ServerProcess.insertRequest(self.id, timeout, player, ...)
|
||||||
end
|
end
|
||||||
|
@ -37,15 +62,17 @@ function Server:Connect(callback: (plyer: Player, args: any) -> ()): string
|
||||||
local key = tostring(Key())
|
local key = tostring(Key())
|
||||||
table.insert(self.fn, key)
|
table.insert(self.fn, key)
|
||||||
ServerProcess.addCallback(self.id, key, callback)
|
ServerProcess.addCallback(self.id, key, callback)
|
||||||
|
self.IsConnected = #self.fn > 0
|
||||||
return key
|
return key
|
||||||
end
|
end
|
||||||
|
|
||||||
function Server:Once(callback: (plyer: Player, args: any) -> ()): string
|
function Server:Once(callback: (plyer: Player, args: any) -> ()): string
|
||||||
local key = tostring(Key())
|
local key = tostring(Key())
|
||||||
table.insert(self.fn, key)
|
table.insert(self.fn, key)
|
||||||
ServerProcess.addCallback(self.id, key, function(...)
|
self.IsConnected = #self.fn > 0
|
||||||
|
ServerProcess.addCallback(self.id, key, function(player: Player, ...: any?)
|
||||||
self:Disconnect(key)
|
self:Disconnect(key)
|
||||||
task.spawn(callback, ...)
|
task.spawn(callback, player, ...)
|
||||||
end)
|
end)
|
||||||
return key
|
return key
|
||||||
end
|
end
|
||||||
|
@ -59,19 +86,25 @@ function Server:Wait()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Server:DisconnectAll()
|
function Server:DisconnectAll()
|
||||||
for idx, key: string in self.fn do
|
for _, key: string in self.fn do
|
||||||
ServerProcess.removeCallback(self.id, key)
|
self:Disconnect(key)
|
||||||
table.remove(self.fn, idx)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Server:Disconnect(key: string)
|
function Server:Disconnect(key: string): boolean
|
||||||
Assert(typeof(key) == "string", "Key must be a string type.")
|
Assert(typeof(key) == "string", "Key must be a string type.")
|
||||||
ServerProcess.removeCallback(self.id, key)
|
ServerProcess.removeCallback(self.id, key)
|
||||||
|
table.remove(self.fn, table.find(self.fn, key))
|
||||||
|
self.IsConnected = #self.fn > 0
|
||||||
|
return table.find(self.fn, key) == nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function Server:Destroy()
|
function Server:Destroy()
|
||||||
self:DisconnectAll()
|
self:DisconnectAll()
|
||||||
|
self._buffer:remove()
|
||||||
|
ServerProcess.remove(self.id)
|
||||||
|
Serdes.decrement()
|
||||||
|
table.clear(self)
|
||||||
setmetatable(self, nil)
|
setmetatable(self, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,260 +0,0 @@
|
||||||
--!native
|
|
||||||
--!strict
|
|
||||||
local ServerProcess = {}
|
|
||||||
|
|
||||||
local RunService = game:GetService("RunService")
|
|
||||||
local Players = game:GetService("Players")
|
|
||||||
local Util = script.Parent.Parent.Util
|
|
||||||
|
|
||||||
local Type = require(script.Parent.Parent.Type)
|
|
||||||
local Event = require(script.Parent.Parent.Event)
|
|
||||||
local Spawn = require(Util.Spawn)
|
|
||||||
local Key = require(Util.Key)
|
|
||||||
local RateLimit = require(Util.RateLimit)
|
|
||||||
|
|
||||||
local serverQueue: Type.QueueMap = {}
|
|
||||||
local unreliableServerQueue: Type.QueueMap = {}
|
|
||||||
local serverCallback: Type.CallbackMap = {}
|
|
||||||
local serverRequestQueue: Type.QueueMap = {}
|
|
||||||
|
|
||||||
local queueOut: {
|
|
||||||
[Player]: {
|
|
||||||
[string]: {any},
|
|
||||||
}
|
|
||||||
} = {}
|
|
||||||
local queueIn: {
|
|
||||||
[string]: {
|
|
||||||
[Player]: {any},
|
|
||||||
}
|
|
||||||
} = {}
|
|
||||||
local queueInRequest: {
|
|
||||||
[number]: {
|
|
||||||
[string]: {
|
|
||||||
[Player]: {any}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} = {}
|
|
||||||
local queueOutRequest: {
|
|
||||||
[number]: {
|
|
||||||
[string]: {
|
|
||||||
[Player]: {any}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} = {}
|
|
||||||
queueInRequest[1] = {}
|
|
||||||
queueInRequest[2] = {}
|
|
||||||
queueOutRequest[1] = {}
|
|
||||||
queueOutRequest[2] = {}
|
|
||||||
|
|
||||||
local ReliableEvent = Event.Reliable
|
|
||||||
local UnreliableEvent = Event.Unreliable
|
|
||||||
local RequestEvent = Event.Request
|
|
||||||
|
|
||||||
local function initializeEachPlayer(player: Player)
|
|
||||||
if not player then return end
|
|
||||||
if not queueOut[player] then
|
|
||||||
queueOut[player] = {}
|
|
||||||
end
|
|
||||||
for Identifier: string,_ in serverQueue do
|
|
||||||
if not queueOut[player][Identifier] then
|
|
||||||
queueOut[player][Identifier] = {}
|
|
||||||
end
|
|
||||||
if not serverRequestQueue[Identifier][player] then
|
|
||||||
serverRequestQueue[Identifier][player] = {}
|
|
||||||
end
|
|
||||||
if not queueIn[Identifier][player] then
|
|
||||||
queueIn[Identifier][player] = {}
|
|
||||||
end
|
|
||||||
if not queueInRequest[1][Identifier][player] then
|
|
||||||
queueInRequest[1][Identifier][player] = {}
|
|
||||||
queueInRequest[2][Identifier][player] = {}
|
|
||||||
end
|
|
||||||
if not queueOutRequest[1][Identifier][player] then
|
|
||||||
queueOutRequest[1][Identifier][player] = {}
|
|
||||||
queueOutRequest[2][Identifier][player] = {}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Players.PlayerAdded:Connect(initializeEachPlayer)
|
|
||||||
|
|
||||||
function ServerProcess.insertQueue(Identifier: string, reliable: boolean, player: Player, ...: any)
|
|
||||||
if not reliable then
|
|
||||||
if not unreliableServerQueue[Identifier][player] then
|
|
||||||
unreliableServerQueue[Identifier][player] = {}
|
|
||||||
end
|
|
||||||
table.insert(unreliableServerQueue[Identifier][player], { ... })
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if not serverQueue[Identifier][player] then
|
|
||||||
serverQueue[Identifier][player] = {}
|
|
||||||
end
|
|
||||||
table.insert(serverQueue[Identifier][player], { ... })
|
|
||||||
end
|
|
||||||
|
|
||||||
function ServerProcess.insertRequest(Identifier: string, timeout: number, player: Player, ...: any)
|
|
||||||
if not serverQueue[Identifier][player] then
|
|
||||||
serverQueue[Identifier][player] = {}
|
|
||||||
end
|
|
||||||
local yieldThread: thread, start = coroutine.running(), os.clock()
|
|
||||||
local cancel = task.delay(timeout, function()
|
|
||||||
task.spawn(yieldThread, nil)
|
|
||||||
end)
|
|
||||||
table.insert(serverRequestQueue[Identifier][player], { tostring(Key()), function(...: any)
|
|
||||||
if (os.clock() - start) > timeout then return end
|
|
||||||
task.cancel(cancel)
|
|
||||||
task.spawn(yieldThread, ...)
|
|
||||||
end :: any, { ... } :: any })
|
|
||||||
return coroutine.yield()
|
|
||||||
end
|
|
||||||
|
|
||||||
function ServerProcess.add(Identifier: string, originId: string, ratelimit: Type.rateLimitArg)
|
|
||||||
if not serverQueue[Identifier] then
|
|
||||||
RateLimit.create(originId, ratelimit.maxEntrance or 200, ratelimit.interval or 2)
|
|
||||||
serverQueue[Identifier] = {}
|
|
||||||
unreliableServerQueue[Identifier] = {}
|
|
||||||
serverCallback[Identifier] = {}
|
|
||||||
serverRequestQueue[Identifier] = {}
|
|
||||||
|
|
||||||
queueIn[Identifier] = {}
|
|
||||||
queueInRequest[1][Identifier] = {}
|
|
||||||
queueInRequest[2][Identifier] = {}
|
|
||||||
queueOutRequest[1][Identifier] = {}
|
|
||||||
queueOutRequest[2][Identifier] = {}
|
|
||||||
|
|
||||||
for _, player: Player in ipairs(Players:GetPlayers()) do
|
|
||||||
task.spawn(initializeEachPlayer, player)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ServerProcess.addCallback(Identifier: string, key: string, callback)
|
|
||||||
serverCallback[Identifier][key] = callback
|
|
||||||
end
|
|
||||||
|
|
||||||
function ServerProcess.removeCallback(Identifier: string, key: string)
|
|
||||||
serverCallback[Identifier][key] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function ServerProcess.start()
|
|
||||||
RunService.PostSimulation:Connect(function()
|
|
||||||
for Identifier: string, players in unreliableServerQueue do
|
|
||||||
for player: Player, data: any in players do
|
|
||||||
if #data == 0 then continue end
|
|
||||||
UnreliableEvent:FireClient(player, Identifier, data)
|
|
||||||
table.clear(data)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for _, player: Player in ipairs(Players:GetPlayers()) do
|
|
||||||
if not queueOut[player] then continue end
|
|
||||||
for Identifier: string, data: any in queueOut[player] do
|
|
||||||
if #data == 0 then continue end
|
|
||||||
ReliableEvent:FireClient(player, Identifier, data)
|
|
||||||
table.clear(data)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for Identifier: string, players in serverQueue do
|
|
||||||
for player: Player, data in players do
|
|
||||||
if #data > 0 and queueOut[player] then
|
|
||||||
queueOut[player][Identifier] = table.clone(data)
|
|
||||||
table.clear(data)
|
|
||||||
end
|
|
||||||
if #serverRequestQueue[Identifier][player] > 0 then
|
|
||||||
for _, requestData in serverRequestQueue[Identifier][player] do
|
|
||||||
if not requestData[3] then continue end
|
|
||||||
table.insert(queueOutRequest[1][Identifier][player], { requestData[1], requestData[3] })
|
|
||||||
table.remove(requestData, #requestData)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if serverCallback[Identifier] then
|
|
||||||
if #queueIn[Identifier][player] > 0 then
|
|
||||||
for _, packedDatas: any in queueIn[Identifier][player] do
|
|
||||||
if #packedDatas == 0 then continue end
|
|
||||||
for _, v: any in packedDatas do
|
|
||||||
for _, fn: any in serverCallback[Identifier] do
|
|
||||||
Spawn(fn, player, table.unpack(v))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.clear(queueIn[Identifier][player])
|
|
||||||
end
|
|
||||||
if #queueInRequest[1][Identifier][player] > 0 then
|
|
||||||
for idx, packetDatas: any in queueInRequest[1][Identifier][player] do
|
|
||||||
if #packetDatas == 0 then continue end
|
|
||||||
for _, packetData in packetDatas do
|
|
||||||
for _, fn: any in serverCallback[Identifier] do
|
|
||||||
Spawn(function()
|
|
||||||
local requestReturn = { fn(player, table.unpack(packetData[2])) }
|
|
||||||
table.insert(queueOutRequest[2][Identifier][player], { packetData[1], requestReturn })
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.clear(queueInRequest[1][Identifier][player])
|
|
||||||
end
|
|
||||||
if #queueInRequest[2][Identifier][player] > 0 then
|
|
||||||
for _, packetDatas: any in queueInRequest[2][Identifier][player] do
|
|
||||||
for _, packetData in packetDatas do
|
|
||||||
if #packetData == 1 then continue end
|
|
||||||
for idx, serverRequest in serverRequestQueue[Identifier][player] do
|
|
||||||
if serverRequest[1] == packetData[1] then
|
|
||||||
Spawn(serverRequest[2], table.unpack(packetData[2]))
|
|
||||||
table.remove(serverRequestQueue[Identifier], idx)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.clear(queueInRequest[2][Identifier][player])
|
|
||||||
end
|
|
||||||
for player: Player, requestsData: any in queueOutRequest[1][Identifier] do
|
|
||||||
if #requestsData == 0 then continue end
|
|
||||||
RequestEvent:FireClient(player, Identifier, "\1", requestsData)
|
|
||||||
table.clear(requestsData)
|
|
||||||
end
|
|
||||||
for player: Player, toReturnDatas: any in queueOutRequest[2][Identifier] do
|
|
||||||
if #toReturnDatas == 0 then continue end
|
|
||||||
RequestEvent:FireClient(player, Identifier, "\0", toReturnDatas)
|
|
||||||
table.clear(toReturnDatas)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
local function onServerNetworkReceive(player: Player, Identifier: string, data: any)
|
|
||||||
if not Identifier or not data then return end
|
|
||||||
if not serverQueue[Identifier] then
|
|
||||||
serverQueue[Identifier] = {}
|
|
||||||
end
|
|
||||||
if not serverQueue[Identifier][player] then
|
|
||||||
serverQueue[Identifier][player] = {}
|
|
||||||
end
|
|
||||||
if not queueIn[Identifier][player] then
|
|
||||||
queueIn[Identifier][player] = {}
|
|
||||||
end
|
|
||||||
table.insert(queueIn[Identifier][player], data)
|
|
||||||
end
|
|
||||||
ReliableEvent.OnServerEvent:Connect(onServerNetworkReceive)
|
|
||||||
UnreliableEvent.OnServerEvent:Connect(onServerNetworkReceive)
|
|
||||||
RequestEvent.OnServerEvent:Connect(function(player: Player, Identifier: string, action: string, data: any)
|
|
||||||
if not Identifier or not data then return end
|
|
||||||
if not queueInRequest[1][Identifier][player] then
|
|
||||||
queueInRequest[1][Identifier][player] = {}
|
|
||||||
queueInRequest[2][Identifier][player] = {}
|
|
||||||
end
|
|
||||||
if not serverQueue[Identifier][player] then
|
|
||||||
serverQueue[Identifier][player] = {}
|
|
||||||
end
|
|
||||||
if action == "\1" then
|
|
||||||
table.insert(queueInRequest[1][Identifier][player], data)
|
|
||||||
else
|
|
||||||
table.insert(queueInRequest[2][Identifier][player], data)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, player: Player in ipairs(Players:GetPlayers()) do
|
|
||||||
task.spawn(initializeEachPlayer, player)
|
|
||||||
end
|
|
||||||
|
|
||||||
return ServerProcess
|
|
371
src/Index/Server/ServerProcess/init.luau
Normal file
371
src/Index/Server/ServerProcess/init.luau
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
--!native
|
||||||
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
|
local ServerProcess = {}
|
||||||
|
|
||||||
|
local RunService = game:GetService("RunService")
|
||||||
|
local Players = game:GetService("Players")
|
||||||
|
local Util = script.Parent.Parent.Util
|
||||||
|
|
||||||
|
local Type = require(script.Parent.Parent.Type)
|
||||||
|
local Event = require(script.Parent.Parent.Event)
|
||||||
|
local Spawn = require(Util.Spawn)
|
||||||
|
local Key = require(Util.Key)
|
||||||
|
local RateLimit = require(Util.RateLimit)
|
||||||
|
local Buffer = require(Util.Buffer)
|
||||||
|
|
||||||
|
local serverQueue: Type.QueueMap = {}
|
||||||
|
local unreliableServerQueue: Type.QueueMap = {}
|
||||||
|
local serverCallback: Type.CallbackMap = {}
|
||||||
|
local serverRequestQueue: Type.QueueMap = {}
|
||||||
|
local registeredIdentifier: { [string]: boolean } = {}
|
||||||
|
|
||||||
|
local queueOut: {
|
||||||
|
[Player]: {
|
||||||
|
[string]: {any},
|
||||||
|
}
|
||||||
|
} = {}
|
||||||
|
local queueInRequest: {
|
||||||
|
[number]: {
|
||||||
|
[string]: {
|
||||||
|
[Player]: {any}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = {}
|
||||||
|
local queueOutRequest: {
|
||||||
|
[number]: {
|
||||||
|
[string]: {
|
||||||
|
[Player]: {any}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
queueInRequest[1] = {}
|
||||||
|
queueInRequest[2] = {}
|
||||||
|
queueOutRequest[1] = {}
|
||||||
|
queueOutRequest[2] = {}
|
||||||
|
|
||||||
|
local ReliableEvent = Event.Reliable
|
||||||
|
local UnreliableEvent = Event.Unreliable
|
||||||
|
local RequestEvent = Event.Request
|
||||||
|
|
||||||
|
RateLimit.Protect()
|
||||||
|
|
||||||
|
local function initializeEachPlayer(player: Player)
|
||||||
|
if not player then return end
|
||||||
|
if not queueOut[player] then
|
||||||
|
queueOut[player] = {}
|
||||||
|
end
|
||||||
|
for Identifier: string in registeredIdentifier do
|
||||||
|
if not player then break end
|
||||||
|
if not queueOut[player][Identifier] then
|
||||||
|
queueOut[player][Identifier] = {}
|
||||||
|
end
|
||||||
|
if not serverRequestQueue[Identifier] then
|
||||||
|
serverRequestQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
if not serverRequestQueue[Identifier][player] then
|
||||||
|
serverRequestQueue[Identifier][player] = {}
|
||||||
|
end
|
||||||
|
if not queueOutRequest[1][Identifier] then
|
||||||
|
queueOutRequest[1][Identifier] = {}
|
||||||
|
end
|
||||||
|
if not queueOutRequest[2][Identifier] then
|
||||||
|
queueOutRequest[2][Identifier] = {}
|
||||||
|
end
|
||||||
|
if not queueInRequest[1][Identifier][player] then
|
||||||
|
queueInRequest[1][Identifier][player] = {}
|
||||||
|
queueInRequest[2][Identifier][player] = {}
|
||||||
|
end
|
||||||
|
if not queueOutRequest[1][Identifier][player] then
|
||||||
|
queueOutRequest[1][Identifier][player] = {}
|
||||||
|
queueOutRequest[2][Identifier][player] = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Players.PlayerAdded:Connect(initializeEachPlayer)
|
||||||
|
Players.PlayerRemoving:Connect(function(player: Player)
|
||||||
|
if not player then return end
|
||||||
|
if queueOut[player] then
|
||||||
|
queueOut[player] = nil
|
||||||
|
end
|
||||||
|
for _, map in { serverQueue, unreliableServerQueue, serverRequestQueue } do
|
||||||
|
for Identifier: string in map do
|
||||||
|
map[Identifier][player] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for i=1,2 do
|
||||||
|
for Identifier: string in queueInRequest[i] do
|
||||||
|
if queueInRequest[i][Identifier][player] then
|
||||||
|
queueInRequest[i][Identifier][player] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for Identifier: string in queueOutRequest[i] do
|
||||||
|
if queueOutRequest[i][Identifier][player] then
|
||||||
|
queueOutRequest[i][Identifier][player] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
function ServerProcess.insertQueue(Identifier: string, reliable: boolean, player: Player, ...: any)
|
||||||
|
if not reliable then
|
||||||
|
if not unreliableServerQueue[Identifier] then
|
||||||
|
unreliableServerQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
if not unreliableServerQueue[Identifier][player] then
|
||||||
|
unreliableServerQueue[Identifier][player] = {}
|
||||||
|
end
|
||||||
|
table.insert(unreliableServerQueue[Identifier][player], { ... })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not serverQueue[Identifier] then
|
||||||
|
serverQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
if not serverQueue[Identifier][player] then
|
||||||
|
serverQueue[Identifier][player] = {}
|
||||||
|
end
|
||||||
|
table.insert(serverQueue[Identifier][player], { ... })
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServerProcess.insertRequest(Identifier: string, timeout: number, player: Player, ...: any)
|
||||||
|
if not serverRequestQueue[Identifier] then
|
||||||
|
serverRequestQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
if not serverRequestQueue[Identifier][player] then
|
||||||
|
serverRequestQueue[Identifier][player] = {}
|
||||||
|
end
|
||||||
|
local yieldThread: thread, start = coroutine.running(), os.clock()
|
||||||
|
local cancel = task.delay(timeout, function()
|
||||||
|
task.spawn(yieldThread, nil)
|
||||||
|
end)
|
||||||
|
table.insert(serverRequestQueue[Identifier][player], { tostring(Key()), function(...: any)
|
||||||
|
if (os.clock() - start) > timeout then return end
|
||||||
|
task.cancel(cancel)
|
||||||
|
task.spawn(yieldThread, ...)
|
||||||
|
end :: any, { ... } :: any })
|
||||||
|
return coroutine.yield()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServerProcess.add(Identifier: string, originId: string, conf: Type.ServerConf)
|
||||||
|
if not registeredIdentifier[Identifier] then
|
||||||
|
registeredIdentifier[Identifier] = true
|
||||||
|
|
||||||
|
RateLimit.create(originId, conf.rateLimit and conf.rateLimit.maxEntrance or 200, conf.rateLimit and conf.rateLimit.interval or 2)
|
||||||
|
|
||||||
|
if not serverQueue[Identifier] then
|
||||||
|
serverQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
if not serverRequestQueue[Identifier] then
|
||||||
|
serverRequestQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
if not serverCallback[Identifier] then
|
||||||
|
serverCallback[Identifier] = {}
|
||||||
|
end
|
||||||
|
if not unreliableServerQueue[Identifier] then
|
||||||
|
unreliableServerQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
if not queueInRequest[1][Identifier] then
|
||||||
|
queueInRequest[1][Identifier] = {}
|
||||||
|
end
|
||||||
|
if not queueInRequest[2][Identifier] then
|
||||||
|
queueInRequest[2][Identifier] = {}
|
||||||
|
end
|
||||||
|
if not queueOutRequest[1][Identifier] then
|
||||||
|
queueOutRequest[1][Identifier] = {}
|
||||||
|
end
|
||||||
|
if not queueOutRequest[2][Identifier] then
|
||||||
|
queueOutRequest[2][Identifier] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, player: Player in ipairs(Players:GetPlayers()) do
|
||||||
|
task.spawn(initializeEachPlayer, player)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServerProcess.remove(Identifier: string)
|
||||||
|
if not registeredIdentifier[Identifier] then return end
|
||||||
|
registeredIdentifier[Identifier] = nil
|
||||||
|
serverQueue[Identifier] = nil
|
||||||
|
serverRequestQueue[Identifier] = nil
|
||||||
|
serverCallback[Identifier] = nil
|
||||||
|
unreliableServerQueue[Identifier] = nil
|
||||||
|
queueInRequest[1][Identifier] = nil
|
||||||
|
queueInRequest[2][Identifier] = nil
|
||||||
|
queueOutRequest[1][Identifier] = nil
|
||||||
|
queueOutRequest[2][Identifier] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServerProcess.addCallback(Identifier: string, key: string, callback)
|
||||||
|
serverCallback[Identifier][key] = callback
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServerProcess.removeCallback(Identifier: string, key: string)
|
||||||
|
serverCallback[Identifier][key] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServerProcess.start()
|
||||||
|
debug.setmemorycategory("Warp")
|
||||||
|
RunService.PostSimulation:Connect(function()
|
||||||
|
-- Unreliable
|
||||||
|
for Identifier: string, players in unreliableServerQueue do
|
||||||
|
for player: Player, content: any in players do
|
||||||
|
if #content == 0 then continue end
|
||||||
|
for _, unpacked in content do
|
||||||
|
UnreliableEvent:FireClient(player, Buffer.revert(Identifier), Buffer.write(unpacked))
|
||||||
|
end
|
||||||
|
unreliableServerQueue[Identifier][player] = nil
|
||||||
|
end
|
||||||
|
unreliableServerQueue[Identifier] = nil
|
||||||
|
end
|
||||||
|
-- Reliable
|
||||||
|
for Identifier: string, contents: { [Player]: { any } } in serverQueue do
|
||||||
|
for player, content: any in contents do
|
||||||
|
if #content > 0 and queueOut[player] then
|
||||||
|
for _, unpacked in content do
|
||||||
|
ReliableEvent:FireClient(player, Buffer.revert(Identifier), Buffer.write(unpacked))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
serverQueue[Identifier][player] = nil
|
||||||
|
end
|
||||||
|
serverQueue[Identifier] = nil
|
||||||
|
end
|
||||||
|
-- Sent new invokes
|
||||||
|
for Identifier: string, contents in queueOutRequest[1] do
|
||||||
|
for player: Player, requestsData: any in contents do
|
||||||
|
if #requestsData > 0 then
|
||||||
|
RequestEvent:FireClient(player, Buffer.revert(Identifier), "\1", requestsData)
|
||||||
|
end
|
||||||
|
queueOutRequest[1][Identifier][player] = nil
|
||||||
|
end
|
||||||
|
queueOutRequest[1][Identifier] = nil
|
||||||
|
end
|
||||||
|
-- Sent returning invokes
|
||||||
|
for Identifier: string, contents in queueOutRequest[2] do
|
||||||
|
for player: Player, toReturnDatas: any in contents do
|
||||||
|
if #toReturnDatas > 0 then
|
||||||
|
RequestEvent:FireClient(player, Buffer.revert(Identifier), "\0", toReturnDatas)
|
||||||
|
end
|
||||||
|
queueOutRequest[2][Identifier][player] = nil
|
||||||
|
end
|
||||||
|
queueOutRequest[2][Identifier] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
for Identifier: string in registeredIdentifier do
|
||||||
|
if serverRequestQueue[Identifier] then
|
||||||
|
for player, content in serverRequestQueue[Identifier] do
|
||||||
|
if #content == 0 then serverRequestQueue[Identifier][player] = nil continue end
|
||||||
|
for _, requestData in content do
|
||||||
|
if not requestData[3] then continue end
|
||||||
|
if not queueOutRequest[1][Identifier] then
|
||||||
|
queueOutRequest[1][Identifier] = {}
|
||||||
|
end
|
||||||
|
if not queueOutRequest[1][Identifier][player] then
|
||||||
|
queueOutRequest[1][Identifier][player] = {}
|
||||||
|
end
|
||||||
|
table.insert(queueOutRequest[1][Identifier][player], { requestData[1], requestData[3] })
|
||||||
|
requestData[3] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local callback = serverCallback[Identifier] or nil
|
||||||
|
if not callback then continue end
|
||||||
|
|
||||||
|
-- Return Invoke
|
||||||
|
for player, content in queueInRequest[1][Identifier] do
|
||||||
|
if not callback then break end
|
||||||
|
for _, packetDatas in content do
|
||||||
|
if not callback then break end
|
||||||
|
if #packetDatas == 0 then continue end
|
||||||
|
for _, fn: any in callback do
|
||||||
|
for i=1,#packetDatas do
|
||||||
|
if not packetDatas[i] then continue end
|
||||||
|
local packetData1 = packetDatas[i][1]
|
||||||
|
local packetData2 = packetDatas[i][2]
|
||||||
|
Spawn(function()
|
||||||
|
local requestReturn = { fn(player, table.unpack(packetData2)) }
|
||||||
|
if not queueOutRequest[2][Identifier] then
|
||||||
|
queueOutRequest[2][Identifier] = {}
|
||||||
|
end
|
||||||
|
if not queueOutRequest[2][Identifier][player] then
|
||||||
|
queueOutRequest[2][Identifier][player] = {}
|
||||||
|
end
|
||||||
|
table.insert(queueOutRequest[2][Identifier][player], { packetData1, requestReturn })
|
||||||
|
packetData1 = nil
|
||||||
|
packetData2 = nil
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
queueInRequest[1][Identifier][player] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Call to Invoke
|
||||||
|
for player, content in queueInRequest[2][Identifier] do
|
||||||
|
if not callback then break end
|
||||||
|
for _, packetDatas in content do
|
||||||
|
for _, packetData in packetDatas do
|
||||||
|
if not callback then break end
|
||||||
|
if #packetData == 1 then continue end
|
||||||
|
local data = serverRequestQueue[Identifier][player]
|
||||||
|
for i=1,#data do
|
||||||
|
local serverRequest = data[i]
|
||||||
|
if not serverRequest then continue end
|
||||||
|
if serverRequest[1] == packetData[1] then
|
||||||
|
Spawn(serverRequest[2], table.unpack(packetData[2]))
|
||||||
|
table.remove(data, i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
queueInRequest[2][Identifier][player] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
local function onServerNetworkReceive(player: Player, Identifier: buffer | string, data: buffer, ref: { any }?)
|
||||||
|
if not Identifier or typeof(Identifier) ~= "buffer" or not data or typeof(data) ~= "buffer" then return end
|
||||||
|
Identifier = Buffer.convert(Identifier :: buffer)
|
||||||
|
if not registeredIdentifier[Identifier :: string] then return end
|
||||||
|
local read = Buffer.read(data, ref)
|
||||||
|
if not read then return end
|
||||||
|
local callback = serverCallback[Identifier :: string]
|
||||||
|
if not callback then return end
|
||||||
|
for _, fn: any in callback do
|
||||||
|
Spawn(fn, player, table.unpack(read))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ReliableEvent.OnServerEvent:Connect(onServerNetworkReceive)
|
||||||
|
UnreliableEvent.OnServerEvent:Connect(onServerNetworkReceive)
|
||||||
|
RequestEvent.OnServerEvent:Connect(function(player: Player, Identifier: any, action: string, data: any)
|
||||||
|
if not Identifier or not data then return end
|
||||||
|
Identifier = Buffer.convert(Identifier)
|
||||||
|
if not queueInRequest[1][Identifier][player] then
|
||||||
|
queueInRequest[1][Identifier][player] = {}
|
||||||
|
end
|
||||||
|
if not queueInRequest[2][Identifier][player] then
|
||||||
|
queueInRequest[2][Identifier][player] = {}
|
||||||
|
end
|
||||||
|
if not serverQueue[Identifier] then
|
||||||
|
serverQueue[Identifier] = {}
|
||||||
|
end
|
||||||
|
if not serverQueue[Identifier][player] then
|
||||||
|
serverQueue[Identifier][player] = {}
|
||||||
|
end
|
||||||
|
if action == "\1" then
|
||||||
|
table.insert(queueInRequest[1][Identifier][player], data)
|
||||||
|
else
|
||||||
|
table.insert(queueInRequest[2][Identifier][player], data)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, player: Player in ipairs(Players:GetPlayers()) do
|
||||||
|
task.spawn(initializeEachPlayer, player)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ServerProcess
|
|
@ -1,83 +0,0 @@
|
||||||
--!strict
|
|
||||||
--!native
|
|
||||||
local Signal = {}
|
|
||||||
Signal.__index = Signal
|
|
||||||
|
|
||||||
local Util = script.Parent.Util
|
|
||||||
local Key = require(Util.Key)
|
|
||||||
local Spawn = require(Util.Spawn)
|
|
||||||
local Assert = require(Util.Assert)
|
|
||||||
|
|
||||||
local Signals = {}
|
|
||||||
|
|
||||||
function Signal.new(Identifier: string)
|
|
||||||
Assert(typeof(Identifier) == "string", `[Signal]: Identifier must be a string type, got {typeof(Identifier)}`)
|
|
||||||
if not Signals[Identifier] then
|
|
||||||
local signal = setmetatable({}, Signal)
|
|
||||||
signal.id = Identifier
|
|
||||||
signal.fn = {}
|
|
||||||
Signals[Identifier] = signal
|
|
||||||
return signal
|
|
||||||
end
|
|
||||||
return Signals[Identifier]
|
|
||||||
end
|
|
||||||
|
|
||||||
function Signal:Fire(...: any): ()
|
|
||||||
for _, fn in self.fn do
|
|
||||||
Spawn(fn, ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Signal:FireTo(Identifier: string, ...: any): ()
|
|
||||||
if not Identifier or not Signals[Identifier] then return end
|
|
||||||
Signal.Fire(Signals[Identifier], ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Signal:Invoke(key: string, ...: any): (...any?)
|
|
||||||
if not key or not self.fn[key] then return nil end
|
|
||||||
return self.fn[key](...)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Signal:InvokeTo(Identifier: string, key: string, ...: any): (...any?)
|
|
||||||
if not Identifier or not Signals[Identifier] or not key then return nil end
|
|
||||||
return Signal.Invoke(Signals[Identifier], key, ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Signal:Connect(fn: (...any) -> ()): string
|
|
||||||
local key = tostring(Key())
|
|
||||||
self.fn[key] = fn
|
|
||||||
return key
|
|
||||||
end
|
|
||||||
|
|
||||||
function Signal:Once(fn: (...any) -> ()): string
|
|
||||||
local key: string
|
|
||||||
key = self:Connect(function(...)
|
|
||||||
self:Disconnect(key)
|
|
||||||
Spawn(fn, ...)
|
|
||||||
end)
|
|
||||||
return key
|
|
||||||
end
|
|
||||||
|
|
||||||
function Signal:Wait(): number
|
|
||||||
local thread, t = coroutine.running(), os.clock()
|
|
||||||
self:Once(function()
|
|
||||||
task.spawn(thread, os.clock()-t)
|
|
||||||
end)
|
|
||||||
return coroutine.yield()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Signal:Disconnect(key: string): ()
|
|
||||||
if not key then return end
|
|
||||||
self.fn[key] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function Signal:DisconnectAll(): ()
|
|
||||||
table.clear(self.fn)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Signal:Destroy(): ()
|
|
||||||
self:DisconnectAll()
|
|
||||||
setmetatable(self, nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Signal.new :: typeof(Signal.new)
|
|
|
@ -1,5 +1,6 @@
|
||||||
--!strict
|
--!strict
|
||||||
--!native
|
--!native
|
||||||
|
--!optimize 2
|
||||||
local Dedicated = {}
|
local Dedicated = {}
|
||||||
Dedicated.__index = Dedicated
|
Dedicated.__index = Dedicated
|
||||||
|
|
||||||
|
@ -11,6 +12,7 @@ function Dedicated.new(signal: any, handler: (...any) -> ())
|
||||||
end
|
end
|
||||||
|
|
||||||
function Dedicated:Disconnect()
|
function Dedicated:Disconnect()
|
||||||
|
table.clear(self)
|
||||||
setmetatable(self, nil)
|
setmetatable(self, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
--!strict
|
--!strict
|
||||||
--!native
|
--!native
|
||||||
|
--!optimize 2
|
||||||
local Signal = {}
|
local Signal = {}
|
||||||
Signal.__index = Signal
|
Signal.__index = Signal
|
||||||
|
|
||||||
|
@ -21,15 +22,15 @@ function Signal.new(Identifier: string)
|
||||||
return Signals[Identifier]
|
return Signals[Identifier]
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal:Connect(fn: (...any) -> ()): string
|
function Signal:Connect(fn: (...any) -> (), optKey: string?): string
|
||||||
local key = tostring(Key())
|
local key: typeof(Signal) = optKey or tostring(Key()) :: any
|
||||||
self[key] = DedicatedSignal(self, fn)
|
self[key] = DedicatedSignal(self, fn)
|
||||||
return key
|
return key :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal:Once(fn: (...any) -> ()): string
|
function Signal:Once(fn: (...any) -> ()): string
|
||||||
local key: string
|
local key: string
|
||||||
key = self:Connect(function(...)
|
key = self:Connect(function(...: any)
|
||||||
self:Disconnect(key)
|
self:Disconnect(key)
|
||||||
task.spawn(fn, ...)
|
task.spawn(fn, ...)
|
||||||
end)
|
end)
|
||||||
|
@ -37,7 +38,9 @@ function Signal:Once(fn: (...any) -> ()): string
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal:Disconnect(key: string)
|
function Signal:Disconnect(key: string)
|
||||||
|
if not self[key] then return end
|
||||||
self[key]:Disconnect()
|
self[key]:Disconnect()
|
||||||
|
self[key] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal:DisconnectAll(): ()
|
function Signal:DisconnectAll(): ()
|
||||||
|
@ -45,16 +48,22 @@ function Signal:DisconnectAll(): ()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal:Wait(): number
|
function Signal:Wait(): number
|
||||||
local thread, t = coroutine.running(), os.clock()
|
local t, thread = os.clock(), coroutine.running()
|
||||||
self:Once(function()
|
self:Once(function()
|
||||||
task.spawn(thread, os.clock()-t)
|
task.spawn(thread, os.clock()-t)
|
||||||
end)
|
end)
|
||||||
return coroutine.yield()
|
return coroutine.yield()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Signal:DeferFire(...: any): ()
|
||||||
|
for _, handle in self do
|
||||||
|
task.defer(handle.fn, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function Signal:Fire(...: any): ()
|
function Signal:Fire(...: any): ()
|
||||||
for _, handle in self do
|
for _, handle in self do
|
||||||
pcall(handle.fn, ...)
|
task.spawn(handle.fn, ...)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -72,7 +81,7 @@ end
|
||||||
|
|
||||||
function Signal:InvokeTo(signal: string, key: string, ...: any): ()
|
function Signal:InvokeTo(signal: string, key: string, ...: any): ()
|
||||||
if not Signals[signal] then return end
|
if not Signals[signal] then return end
|
||||||
return Signal.Invoke(Signals[signal], ...)
|
return Signal.Invoke(Signals[signal], key, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal:Destroy(): ()
|
function Signal:Destroy(): ()
|
||||||
|
|
|
@ -1,30 +1,44 @@
|
||||||
--!strict
|
--!strict
|
||||||
export type rateLimitArg = {
|
type rateLimitArg = {
|
||||||
maxEntrance: number?,
|
maxEntrance: number?,
|
||||||
interval: number?,
|
interval: number?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ServerConf = {
|
||||||
|
rateLimit: rateLimitArg?,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientConf = {
|
||||||
|
yieldWait: number?,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Middleware = {
|
||||||
|
middleware: (self: Middleware, middleware: (...any) -> (...any)) -> (),
|
||||||
|
key: (self: Middleware) -> string,
|
||||||
|
destroy: (self: Middleware) -> (),
|
||||||
|
}
|
||||||
|
|
||||||
export type Client = {
|
export type Client = {
|
||||||
Fire: (self: Client, reliable: boolean, ...any) -> (),
|
Fire: (self: Client, reliable: boolean, ...any) -> (),
|
||||||
Invoke: (self: Client, timeout: number, ...any) -> any,
|
Invoke: (self: Client, timeout: number, ...any) -> any,
|
||||||
Connect: (self: Client, callback: (...any) -> ()) -> string,
|
Connect: (self: Client, callback: (...any) -> ()) -> Middleware,
|
||||||
Once: (self: Client, callback: (player: Player, ...any) -> ()) -> string,
|
Once: (self: Client, callback: (player: Player, ...any) -> ()) -> Middleware,
|
||||||
Disconnect: (self: Client, key: string) -> (),
|
Disconnect: (self: Client, key: string) -> (),
|
||||||
DisconnectAll: (self: Client) -> (),
|
DisconnectAll: (self: Client) -> (),
|
||||||
Destroy: (self: Client) -> (),
|
|
||||||
Wait: (self: Client) -> number,
|
Wait: (self: Client) -> number,
|
||||||
|
Destroy: (self: Client) -> (),
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Server = {
|
export type Server = {
|
||||||
Fire: (self: Server, reliable: boolean, player: Player, ...any) -> (),
|
Fire: (self: Server, reliable: boolean, player: Player, ...any) -> (),
|
||||||
Fires: (self: Server, reliable: boolean, ...any) -> (),
|
Fires: (self: Server, reliable: boolean, ...any) -> (),
|
||||||
Invoke: (self: Server, timeout: number, player: Player, ...any) -> any,
|
Invoke: (self: Server, timeout: number, player: Player, ...any) -> any,
|
||||||
Connect: (self: Server, callback: (player: Player, ...any) -> ()) -> string,
|
Connect: (self: Server, callback: (player: Player, ...any) -> ()) -> Middleware,
|
||||||
Once: (self: Server, callback: (player: Player, ...any) -> ()) -> string,
|
Once: (self: Server, callback: (player: Player, ...any) -> ()) -> Middleware,
|
||||||
Disconnect: (self: Server, key: string) -> (),
|
Disconnect: (self: Server, key: string) -> (),
|
||||||
DisconnectAll: (self: Server) -> (),
|
DisconnectAll: (self: Server) -> (),
|
||||||
Destroy: (self: Server) -> (),
|
|
||||||
Wait: (self: Server) -> number,
|
Wait: (self: Server) -> number,
|
||||||
|
Destroy: (self: Server) -> (),
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Signal = {
|
export type Signal = {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
--!strict
|
--!strict
|
||||||
return function(condition: (any), errorMessage: string?): ()
|
--!native
|
||||||
if not (condition) then error(errorMessage, 2) end
|
--!optimize 2
|
||||||
|
return function(condition: (any), errorMessage: string, level: number?): ()
|
||||||
|
if not (condition) then error(`Warp: {errorMessage}`, level or 2) end
|
||||||
end
|
end
|
265
src/Index/Util/Buffer/Dedicated.luau
Normal file
265
src/Index/Util/Buffer/Dedicated.luau
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
--!strict
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
|
local DedicatedBuffer = {}
|
||||||
|
DedicatedBuffer.__index = DedicatedBuffer
|
||||||
|
|
||||||
|
local create = buffer.create
|
||||||
|
local copy = buffer.copy
|
||||||
|
local writei8 = buffer.writei8
|
||||||
|
local writei16 = buffer.writei16
|
||||||
|
local writei32 = buffer.writei32
|
||||||
|
local writeu8 = buffer.writeu8
|
||||||
|
local writeu16 = buffer.writeu16
|
||||||
|
local writeu32 = buffer.writeu32
|
||||||
|
local writef32 = buffer.writef32
|
||||||
|
local writef64 = buffer.writef64
|
||||||
|
local writestring = buffer.writestring
|
||||||
|
|
||||||
|
local default: { [string]: number } = {
|
||||||
|
point = 0,
|
||||||
|
next = 0,
|
||||||
|
size = 128,
|
||||||
|
bufferSize = 128,
|
||||||
|
}
|
||||||
|
|
||||||
|
function DedicatedBuffer.copy(self: any, offset: number, b: buffer?, src: buffer?, srcOffset: number?, count: number?)
|
||||||
|
if not b then
|
||||||
|
copy(create(count or default.size), offset, src or self.buffer, srcOffset, count)
|
||||||
|
else
|
||||||
|
copy(b, offset, src or self.buffer, srcOffset, count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.alloc(self: any, byte: number)
|
||||||
|
local size: number = self.size
|
||||||
|
local b: buffer = self.buffer
|
||||||
|
|
||||||
|
while self.next + byte >= size do
|
||||||
|
size = math.floor(size * 1.25) -- +25% increase
|
||||||
|
end
|
||||||
|
local newBuffer: buffer = create(size)
|
||||||
|
copy(newBuffer, 0, b)
|
||||||
|
|
||||||
|
b = newBuffer
|
||||||
|
|
||||||
|
self.point = self.next
|
||||||
|
self.buffer = b
|
||||||
|
self.next += byte
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.build(self: any): buffer
|
||||||
|
local p: number = self.next > self.point and self.next or self.point
|
||||||
|
local build: buffer = create(p)
|
||||||
|
|
||||||
|
copy(build, 0, self.buffer, 0, p)
|
||||||
|
return build
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.buildAndRemove(self: any): (buffer, (any)?)
|
||||||
|
local p: number = self.next > self.point and self.next or self.point
|
||||||
|
local build: buffer = create(p)
|
||||||
|
local ref = #self.ref > 0 and table.clone(self.ref) or nil
|
||||||
|
|
||||||
|
copy(build, 0, self.buffer, 0, p)
|
||||||
|
|
||||||
|
self:remove()
|
||||||
|
return build, ref
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.wi8(self: any, val: number, alloc: number?)
|
||||||
|
if not val then return end
|
||||||
|
self:alloc(alloc or 1)
|
||||||
|
writei8(self.buffer, self.point, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.wi16(self: any, val: number, alloc: number?)
|
||||||
|
if not val then return end
|
||||||
|
self:alloc(alloc or 2)
|
||||||
|
writei16(self.buffer, self.point, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.wi32(self: any, val: number, alloc: number?)
|
||||||
|
if not val then return end
|
||||||
|
self:alloc(alloc or 4)
|
||||||
|
writei32(self.buffer, self.point, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.wu8(self: any, val: number, alloc: number?)
|
||||||
|
if not val then return end
|
||||||
|
self:alloc(alloc or 1)
|
||||||
|
writeu8(self.buffer, self.point, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.wu16(self: any, val: number, alloc: number?)
|
||||||
|
if not val then return end
|
||||||
|
self:alloc(alloc or 2)
|
||||||
|
writeu16(self.buffer, self.point, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.wu32(self: any, val: number, alloc: number?)
|
||||||
|
if not val then return end
|
||||||
|
self:alloc(alloc or 4)
|
||||||
|
writeu32(self.buffer, self.point, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.wf32(self: any, val: number, alloc: number?)
|
||||||
|
if not val then return end
|
||||||
|
self:alloc(alloc or 4)
|
||||||
|
writef32(self.buffer, self.point, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.wf64(self: any, val: number, alloc: number?)
|
||||||
|
if not val then return end
|
||||||
|
self:alloc(alloc or 8)
|
||||||
|
writef64(self.buffer, self.point, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.wstring(self: any, val: string)
|
||||||
|
if not val then return end
|
||||||
|
self:alloc(#val)
|
||||||
|
writestring(self.buffer, self.point, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.wType(self: any, ref: number)
|
||||||
|
writeu8(self.buffer, self.point, ref)
|
||||||
|
self.point += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.wRef(self: any, value: any, alloc: number?)
|
||||||
|
if not value then return end
|
||||||
|
self:alloc(alloc or 1)
|
||||||
|
table.insert(self.ref, value)
|
||||||
|
local index = #self.ref
|
||||||
|
writeu8(self.buffer, self.point, index)
|
||||||
|
self.point += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.pack(self: any, data: {any})
|
||||||
|
if typeof(data) == "nil" then
|
||||||
|
self:wi8(0)
|
||||||
|
elseif typeof(data) == "Instance" then
|
||||||
|
self:wi8(-1) -- Instance marker
|
||||||
|
self:wRef(data)
|
||||||
|
elseif typeof(data) == "table" then
|
||||||
|
--local isArray = (next(data) ~= nil and #data > 0) and true or false
|
||||||
|
local isArray = true
|
||||||
|
local count = 0
|
||||||
|
for k in data do
|
||||||
|
count += 1
|
||||||
|
if typeof(k) ~= "number" or math.floor(k) ~= k then
|
||||||
|
isArray = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if isArray then
|
||||||
|
self:wi8(-2) -- array marker
|
||||||
|
self:wu16(count) -- use 32-bit length
|
||||||
|
for _, v in data do
|
||||||
|
self:pack(v)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self:wi8(-3) -- dictionary marker
|
||||||
|
self:wu16(count) -- number of key-value pairs
|
||||||
|
for k, v in data do
|
||||||
|
self:pack(k) -- pack the key
|
||||||
|
self:pack(v) -- pack the value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif typeof(data) == "EnumItem" then
|
||||||
|
self:wi8(-4)
|
||||||
|
self:wi8(#`{data.EnumType}`)
|
||||||
|
self:wstring(`{data.EnumType}`)
|
||||||
|
self:wu8(data.Value)
|
||||||
|
elseif typeof(data) == "BrickColor" then
|
||||||
|
self:wi8(-5)
|
||||||
|
self:wi16(data.Number)
|
||||||
|
elseif typeof(data) == "Enum" then
|
||||||
|
self:wi8(-6)
|
||||||
|
self:wi8(#`{data}`)
|
||||||
|
self:wstring(`{data}`)
|
||||||
|
elseif typeof(data) == "number" then
|
||||||
|
if math.floor(data) == data then -- Integer
|
||||||
|
if data >= 0 and data <= 255 then
|
||||||
|
self:wi8(1) -- u8 marker
|
||||||
|
self:wu8(data)
|
||||||
|
elseif data >= -32768 and data <= 32767 then
|
||||||
|
self:wi8(2) -- i16 marker
|
||||||
|
self:wi16(data)
|
||||||
|
elseif data >= -2147483647 and data <= 2147483647 then
|
||||||
|
self:wi8(3) -- i32 marker
|
||||||
|
self:wi32(data)
|
||||||
|
else
|
||||||
|
self:wi8(4) -- f64 marker
|
||||||
|
self:wf64(data)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self:wi8(4) -- f64 marker
|
||||||
|
self:wf64(data)
|
||||||
|
end
|
||||||
|
elseif typeof(data) == "boolean" then
|
||||||
|
self:wi8(5) -- boolean marker
|
||||||
|
self:wu8(data and 1 or 0)
|
||||||
|
elseif typeof(data) == "string" then
|
||||||
|
local length = #data
|
||||||
|
if length <= 255 then
|
||||||
|
self:wi8(6)
|
||||||
|
self:wu8(length)
|
||||||
|
elseif length <= 65535 then
|
||||||
|
self:wi8(7)
|
||||||
|
self:wu16(length)
|
||||||
|
else
|
||||||
|
self:wi8(8)
|
||||||
|
self:wi32(length)
|
||||||
|
end
|
||||||
|
self:wstring(data)
|
||||||
|
elseif typeof(data) == "Vector3" then
|
||||||
|
self:wi8(9) -- Vector3 marker
|
||||||
|
self:wf32(data.X)
|
||||||
|
self:wf32(data.Y)
|
||||||
|
self:wf32(data.Z)
|
||||||
|
elseif typeof(data) == "Vector2" then
|
||||||
|
self:wi8(10) -- Vector2 marker
|
||||||
|
self:wf32(data.X)
|
||||||
|
self:wf32(data.Y)
|
||||||
|
elseif typeof(data) == "CFrame" then
|
||||||
|
self:wi8(11) -- CFrame marker
|
||||||
|
for _, v in {data:GetComponents()} do
|
||||||
|
self:wf32(v)
|
||||||
|
end
|
||||||
|
elseif typeof(data) == "Color3" then
|
||||||
|
self:wi8(12) -- Color3 marker
|
||||||
|
self:wu8(data.R * 255)
|
||||||
|
self:wu8(data.G * 255)
|
||||||
|
self:wu8(data.B * 255)
|
||||||
|
else
|
||||||
|
warn(`Unsupported data type: {typeof(data)} value: {data}`)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.flush(self: any)
|
||||||
|
self.point = default.point
|
||||||
|
self.next = default.next
|
||||||
|
self.size = default.size
|
||||||
|
self.buffer = create(default.bufferSize)
|
||||||
|
table.clear(self.ref)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.new()
|
||||||
|
return setmetatable({
|
||||||
|
point = default.point,
|
||||||
|
next = default.next,
|
||||||
|
size = default.size,
|
||||||
|
buffer = create(default.bufferSize),
|
||||||
|
ref = {},
|
||||||
|
}, DedicatedBuffer)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DedicatedBuffer.remove(self: any)
|
||||||
|
self:flush()
|
||||||
|
table.clear(self)
|
||||||
|
setmetatable(self, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
export type DedicatedType = typeof(DedicatedBuffer.new())
|
||||||
|
|
||||||
|
return DedicatedBuffer.new :: typeof(DedicatedBuffer.new)
|
149
src/Index/Util/Buffer/init.luau
Normal file
149
src/Index/Util/Buffer/init.luau
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
--!strict
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
|
local Buffer = {}
|
||||||
|
Buffer.__index = Buffer
|
||||||
|
|
||||||
|
local Dedicated = require(script.Dedicated)
|
||||||
|
|
||||||
|
local tostring = buffer.tostring
|
||||||
|
local fromstring = buffer.fromstring
|
||||||
|
local readu8 = buffer.readu8
|
||||||
|
local readi8 = buffer.readi8
|
||||||
|
local readu16 = buffer.readu16
|
||||||
|
local readi16 = buffer.readi16
|
||||||
|
local readi32 = buffer.readi32
|
||||||
|
local readf32 = buffer.readf32
|
||||||
|
local readf64 = buffer.readf64
|
||||||
|
local readstring = buffer.readstring
|
||||||
|
local len = buffer.len
|
||||||
|
|
||||||
|
local function readValue(b: buffer, position: number, ref: { any }?): (any, number)
|
||||||
|
local typeByte = readi8(b, position)
|
||||||
|
position += 1
|
||||||
|
if typeByte == 0 then -- nil
|
||||||
|
return nil, position
|
||||||
|
elseif typeByte == -1 then -- Instance
|
||||||
|
if not ref or #ref == 0 then
|
||||||
|
return nil, position + 1
|
||||||
|
end
|
||||||
|
local value = ref[readu8(b, position)]
|
||||||
|
if typeof(value) == "Instance" then
|
||||||
|
return value, position + 1
|
||||||
|
end
|
||||||
|
return nil, position + 1
|
||||||
|
elseif typeByte == -2 then -- array
|
||||||
|
local length = readu16(b, position)
|
||||||
|
position += 2
|
||||||
|
local array = {}
|
||||||
|
for _ = 1, length do
|
||||||
|
local value
|
||||||
|
value, position = readValue(b, position, ref)
|
||||||
|
table.insert(array, value)
|
||||||
|
end
|
||||||
|
return array, position
|
||||||
|
elseif typeByte == -3 then -- dictionary
|
||||||
|
local length = readu16(b, position)
|
||||||
|
position += 2
|
||||||
|
local dict = {}
|
||||||
|
for _ = 1, length do
|
||||||
|
local key, value
|
||||||
|
key, position = readValue(b, position, ref)
|
||||||
|
value, position = readValue(b, position, ref)
|
||||||
|
dict[key] = value
|
||||||
|
end
|
||||||
|
return dict, position
|
||||||
|
elseif typeByte == -4 then -- EnumItem
|
||||||
|
local length = readi8(b, position)
|
||||||
|
local value = readstring(b, position + 1, length)
|
||||||
|
local value2 = readu8(b, position + 1 + length)
|
||||||
|
return Enum[value]:FromValue(value2), position + 2 + length
|
||||||
|
elseif typeByte == -5 then -- BrickColor
|
||||||
|
local value = readi16(b, position)
|
||||||
|
return BrickColor.new(value), position + 2
|
||||||
|
elseif typeByte == -6 then -- Enum
|
||||||
|
local length = readi8(b, position)
|
||||||
|
local value = readstring(b, position + 1, length)
|
||||||
|
return Enum[value], position + 1 + length
|
||||||
|
elseif typeByte == 1 then -- int u8
|
||||||
|
local value = readu8(b, position)
|
||||||
|
return value, position + 1
|
||||||
|
elseif typeByte == 2 then -- int i16
|
||||||
|
local value = readi16(b, position)
|
||||||
|
return value, position + 2
|
||||||
|
elseif typeByte == 3 then -- int i32
|
||||||
|
local value = readi32(b, position)
|
||||||
|
return value, position + 4
|
||||||
|
elseif typeByte == 4 then -- f64
|
||||||
|
local value = readf64(b, position)
|
||||||
|
return value, position + 8
|
||||||
|
elseif typeByte == 5 then -- boolean
|
||||||
|
local value = readu8(b, position) == 1
|
||||||
|
return value, position + 1
|
||||||
|
elseif typeByte == 6 then -- string u8
|
||||||
|
local length = readu8(b, position)
|
||||||
|
local value = readstring(b, position + 1, length)
|
||||||
|
return value, position + length + 1
|
||||||
|
elseif typeByte == 7 then -- string u16
|
||||||
|
local length = readu16(b, position)
|
||||||
|
local value = readstring(b, position + 2, length)
|
||||||
|
return value, position + length + 2
|
||||||
|
elseif typeByte == 8 then -- string i32
|
||||||
|
local length = readi32(b, position)
|
||||||
|
local value = readstring(b, position + 4, length)
|
||||||
|
return value, position + length + 4
|
||||||
|
elseif typeByte == 9 then -- Vector3
|
||||||
|
local x = readf32(b, position)
|
||||||
|
local y = readf32(b, position + 4)
|
||||||
|
local z = readf32(b, position + 8)
|
||||||
|
return Vector3.new(x, y, z), position + 12
|
||||||
|
elseif typeByte == 10 then -- Vector2
|
||||||
|
local x = readf32(b, position)
|
||||||
|
local y = readf32(b, position + 8)
|
||||||
|
return Vector2.new(x, y), position + 8
|
||||||
|
elseif typeByte == 11 then -- CFrame
|
||||||
|
local components = {}
|
||||||
|
for i = 1, 12 do
|
||||||
|
table.insert(components, readf32(b, position + (i - 1) * 4))
|
||||||
|
end
|
||||||
|
return CFrame.new(unpack(components)), position + 48
|
||||||
|
elseif typeByte == 12 then -- Color3
|
||||||
|
local r = readu8(b, position)
|
||||||
|
local g = readu8(b, position + 1)
|
||||||
|
local b = readu8(b, position + 2)
|
||||||
|
return Color3.fromRGB(r, g, b), position + 3
|
||||||
|
end
|
||||||
|
error(`Unsupported type marker: {typeByte}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Buffer.new(): Dedicated.DedicatedType
|
||||||
|
return Dedicated()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Buffer.convert(b: buffer): string
|
||||||
|
return tostring(b)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Buffer.revert(s: string): buffer
|
||||||
|
return fromstring(s)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Buffer.write(data: { any }): (buffer, (any)?)
|
||||||
|
local newBuffer = Dedicated()
|
||||||
|
newBuffer:pack(data)
|
||||||
|
return newBuffer:buildAndRemove()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Buffer.read(b: buffer, ref: { any }?): any?
|
||||||
|
local position = 0
|
||||||
|
local result = {}
|
||||||
|
while position < len(b) do
|
||||||
|
local value
|
||||||
|
value, position = readValue(b, position, ref)
|
||||||
|
table.insert(result, value)
|
||||||
|
end
|
||||||
|
ref = nil
|
||||||
|
return table.unpack(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Buffer :: typeof(Buffer)
|
|
@ -1,4 +1,5 @@
|
||||||
--!strict
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
return function(): number?
|
return function(): number?
|
||||||
return tonumber(string.sub(tostring(Random.new():NextNumber()), 3, 6))
|
return tonumber(string.sub(tostring(Random.new():NextNumber()), 3, 8)) -- 6 digits
|
||||||
end
|
end
|
|
@ -1,23 +1,54 @@
|
||||||
--!strict
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
local RateLimit = {}
|
local RateLimit = {}
|
||||||
|
|
||||||
local RunService = game:GetService("RunService")
|
local RunService = game:GetService("RunService")
|
||||||
local Assert = require(script.Parent.Assert)
|
local Assert = require(script.Parent.Assert)
|
||||||
local Event = require(script.Parent.Parent.Event).Reliable
|
local Events = require(script.Parent.Parent.Event)
|
||||||
|
local Reliable, Unreliable, Request = Events.Reliable, Events.Unreliable, Events.Request
|
||||||
|
local Signal = require(script.Parent.Parent.Signal)("Warp_OnSpamSignal")
|
||||||
|
|
||||||
|
local map, activity, meta = {}, {}, {}
|
||||||
|
setmetatable(meta , {
|
||||||
|
__index = map,
|
||||||
|
__newindex = function(self, key, value)
|
||||||
|
if not activity[key] then
|
||||||
|
activity[key] = os.clock()
|
||||||
|
end
|
||||||
|
if (os.clock()-activity[key]) >= 1 then
|
||||||
|
activity[key] = os.clock()
|
||||||
|
map[key] = 1
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if value >= 1e2 then -- 100
|
||||||
|
Signal:Fire(key)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
map[key] = value
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
local function onReceived(player: Player)
|
||||||
|
if not meta[player] then
|
||||||
|
meta[player] = 1
|
||||||
|
return
|
||||||
|
end
|
||||||
|
meta[player] += 1
|
||||||
|
end
|
||||||
|
|
||||||
function RateLimit.create(Identifier: string, entrance: number?, interval: number?)
|
function RateLimit.create(Identifier: string, entrance: number?, interval: number?)
|
||||||
Assert(typeof(Identifier) == "string", "Identifier must a string type.")
|
Assert(typeof(Identifier) == "string", "Identifier must a string type.")
|
||||||
if RunService:IsServer() then
|
if RunService:IsServer() then
|
||||||
Assert(typeof(entrance) == "number", "entrance must a number type.")
|
Assert(typeof(entrance) == "number", "entrance must a number type.")
|
||||||
Assert(entrance :: number > 0, "entrance must above 0.")
|
Assert(entrance :: number > 0, "entrance must above 0.")
|
||||||
Event:SetAttribute(Identifier.."_ent", entrance)
|
Reliable:SetAttribute(Identifier.."_ent", entrance)
|
||||||
Event:SetAttribute(Identifier.."_int", interval)
|
Reliable:SetAttribute(Identifier.."_int", interval)
|
||||||
else
|
else
|
||||||
while (not Event:GetAttribute(Identifier.."_ent")) or (not Event:GetAttribute(Identifier.."_int")) do
|
while (not Reliable:GetAttribute(Identifier.."_ent")) or (not Reliable:GetAttribute(Identifier.."_int")) do
|
||||||
task.wait(0.5)
|
task.wait(0.1)
|
||||||
end
|
end
|
||||||
entrance = tonumber(Event:GetAttribute(Identifier.."_ent"))
|
entrance = tonumber(Reliable:GetAttribute(Identifier.."_ent"))
|
||||||
interval = tonumber(Event:GetAttribute(Identifier.."_int"))
|
interval = tonumber(Reliable:GetAttribute(Identifier.."_int"))
|
||||||
end
|
end
|
||||||
local entrances: number = 0
|
local entrances: number = 0
|
||||||
return function(incoming: number?): boolean
|
return function(incoming: number?): boolean
|
||||||
|
@ -26,9 +57,19 @@ function RateLimit.create(Identifier: string, entrance: number?, interval: numbe
|
||||||
entrances = 0
|
entrances = 0
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
entrances += incoming and incoming or 1
|
entrances += incoming or 1
|
||||||
return (entrances <= entrance :: number)
|
return (entrances <= entrance :: number)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function RateLimit.Protect()
|
||||||
|
if not RunService:IsServer() or Reliable:GetAttribute("Protected") or Unreliable:GetAttribute("Protected") or Request:GetAttribute("Protected") then return end
|
||||||
|
Reliable:SetAttribute("Protected", true)
|
||||||
|
Unreliable:SetAttribute("Protected", true)
|
||||||
|
Request:SetAttribute("Protected", true)
|
||||||
|
Reliable.OnServerEvent:Connect(onReceived)
|
||||||
|
Unreliable.OnServerEvent:Connect(onReceived)
|
||||||
|
Request.OnServerEvent:Connect(onReceived)
|
||||||
|
end
|
||||||
|
|
||||||
return RateLimit :: typeof(RateLimit)
|
return RateLimit :: typeof(RateLimit)
|
|
@ -1,22 +1,50 @@
|
||||||
--!strict
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
|
local SerDes = {}
|
||||||
local RunService = game:GetService("RunService")
|
local RunService = game:GetService("RunService")
|
||||||
local SerInt = 0
|
local SerInt = 0
|
||||||
|
|
||||||
local Event = require(script.Parent.Parent.Event).Reliable
|
local Event = require(script.Parent.Parent.Event).Reliable
|
||||||
local Assert = require(script.Parent.Assert)
|
local Assert = require(script.Parent.Assert)
|
||||||
|
|
||||||
return function(Identifier: string): string
|
function SerDes.increment(Identifier: string, timeout: number?): number
|
||||||
Assert(typeof(Identifier) == "string", "Identifier must be a string type.")
|
Assert(typeof(Identifier) == "string", "Identifier must be a string type.")
|
||||||
Assert(SerInt < 255, "reached max 255 identifiers.")
|
|
||||||
if RunService:IsServer() then
|
if RunService:IsServer() then
|
||||||
|
Assert(SerInt < 255, "reached max 255 identifiers.")
|
||||||
if not Event:GetAttribute(Identifier) then
|
if not Event:GetAttribute(Identifier) then
|
||||||
SerInt += 1
|
SerInt += 1
|
||||||
Event:SetAttribute(Identifier, string.pack("I1", SerInt)) -- I1 -> 255 max, I2 -> ~ 6.5e4 max. (SerInt)
|
Event:SetAttribute(`{SerInt}`, Identifier)
|
||||||
|
Event:SetAttribute(Identifier, SerInt)
|
||||||
|
--Event:SetAttribute(Identifier, string.pack("I1", SerInt)) -- I1 -> 255 max, I2 -> ~ 6.5e4 max. (SerInt), removed/disabled for buffer migration.
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
while not Event:GetAttribute(Identifier) do
|
local yieldThread: thread = coroutine.running()
|
||||||
task.wait(0.5)
|
local cancel = task.delay(timeout or 10, function() -- yield cancelation (timerout)
|
||||||
end
|
task.spawn(yieldThread, nil)
|
||||||
|
error(`Serdes: {Identifier} is taking too long to retrieve, seems like it's not replicated on server.`, 2)
|
||||||
|
end)
|
||||||
|
task.spawn(function()
|
||||||
|
while coroutine.status(cancel) ~= "dead" and task.wait(0.04) do -- let it loop for yields! 1/24
|
||||||
|
if Event:GetAttribute(Identifier) then
|
||||||
|
task.cancel(cancel)
|
||||||
|
task.spawn(yieldThread, Event:GetAttribute(Identifier))
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return coroutine.yield() -- yield
|
||||||
end
|
end
|
||||||
return Event:GetAttribute(Identifier)
|
return Event:GetAttribute(Identifier)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function SerDes.decrement()
|
||||||
|
if not RunService:IsServer() or SerInt <= 0 then return end
|
||||||
|
local Identifier = Event:GetAttribute(`{SerInt}`)
|
||||||
|
if not Identifier then return end
|
||||||
|
Event:SetAttribute(`{Identifier}`, nil)
|
||||||
|
Event:SetAttribute(`{SerInt}`, nil)
|
||||||
|
SerInt -= 1
|
||||||
|
Identifier = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return SerDes :: typeof(SerDes)
|
|
@ -1,29 +1,21 @@
|
||||||
--!native
|
--!native
|
||||||
--!strict
|
--!strict
|
||||||
local thread: thread? = nil
|
--!optimize 2
|
||||||
|
local thread: thread?
|
||||||
|
|
||||||
local function passer(fn, ...): ()
|
local function passer<T...>(func: (T...) -> (), ...: T...): ()
|
||||||
local hold = thread
|
local HoldThread: thread = thread :: thread
|
||||||
thread = nil
|
thread = nil
|
||||||
fn(...)
|
func(...)
|
||||||
thread = hold
|
thread = HoldThread
|
||||||
end
|
end
|
||||||
|
|
||||||
local function yield(): never
|
local function newThread(): ()
|
||||||
while true do
|
thread = coroutine.running()
|
||||||
passer(coroutine.yield())
|
while true do passer(coroutine.yield()) end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if not thread then
|
return function<T...>(func: (T...) -> (), ...: T...): ()
|
||||||
thread = coroutine.create(yield)
|
if not thread then task.spawn(newThread) end
|
||||||
coroutine.resume(thread :: any, thread)
|
task.spawn(thread :: thread, func, ...)
|
||||||
end
|
|
||||||
|
|
||||||
return function(fn: (...any) -> (), ...: any): ()
|
|
||||||
if not thread then
|
|
||||||
thread = coroutine.create(yield)
|
|
||||||
coroutine.resume(thread :: any, thread)
|
|
||||||
end
|
|
||||||
task.spawn(thread :: thread, fn, ...)
|
|
||||||
end
|
end
|
|
@ -1,4 +1,6 @@
|
||||||
--!strict
|
--!strict
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
local Index = {}
|
local Index = {}
|
||||||
|
|
||||||
local RunService = game:GetService("RunService")
|
local RunService = game:GetService("RunService")
|
||||||
|
@ -11,6 +13,7 @@ local Client = script.Client
|
||||||
local Type = require(script.Type)
|
local Type = require(script.Type)
|
||||||
local Assert = require(Util.Assert)
|
local Assert = require(Util.Assert)
|
||||||
local Signal = require(script.Signal)
|
local Signal = require(script.Signal)
|
||||||
|
local Buffer = require(Util.Buffer)
|
||||||
|
|
||||||
if IsServer then
|
if IsServer then
|
||||||
require(Server.ServerProcess).start()
|
require(Server.ServerProcess).start()
|
||||||
|
@ -18,39 +21,43 @@ else
|
||||||
require(Client.ClientProcess).start()
|
require(Client.ClientProcess).start()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Index.Server(Identifier: string, rateLimit: Type.rateLimitArg?): Type.Server
|
function Index.Server(Identifier: string, conf: Type.ServerConf?): Type.Server
|
||||||
Assert(IsServer, `[Warp]: Calling .Server({Identifier}) on client side (expected server side)`)
|
Assert(IsServer, `[Warp]: Calling .Server({Identifier}) on client side (expected server side)`)
|
||||||
Assert(typeof(Identifier) == "string", `[Warp]: Identifier must be a string type, got {typeof(Identifier)}`)
|
Assert(typeof(Identifier) == "string", `[Warp]: Identifier must be a string type, got {typeof(Identifier)}`)
|
||||||
return require(Server.Index)(Identifier, rateLimit) :: Type.Server
|
return require(Server.Index)(Identifier, conf) :: Type.Server
|
||||||
end
|
end
|
||||||
function Index.Client(Identifier: string): Type.Client
|
function Index.Client(Identifier: string, conf: Type.ClientConf?): Type.Client
|
||||||
Assert(not IsServer, `[Warp]: Calling .Client({Identifier}) on server side (expected client side)`)
|
Assert(not IsServer, `[Warp]: Calling .Client({Identifier}) on server side (expected client side)`)
|
||||||
Assert(typeof(Identifier) == "string", `[Warp]: Identifier must be a string type, got {typeof(Identifier)}`)
|
Assert(typeof(Identifier) == "string", `[Warp]: Identifier must be a string type, got {typeof(Identifier)}`)
|
||||||
return require(Client.Index)(Identifier) :: Type.Client
|
return require(Client.Index)(Identifier, conf) :: Type.Client
|
||||||
end
|
end
|
||||||
|
|
||||||
function Index.fromServerArray(arrays: { any }): Type.fromServerArray
|
function Index.fromServerArray(arrays: { string } | { [string]: Type.ServerConf }): Type.fromServerArray
|
||||||
Assert(IsServer, `[Warp]: Calling .fromServerArray({arrays}) on client side (expected server side)`)
|
Assert(IsServer, `[Warp]: Calling .fromServerArray({arrays}) on client side (expected server side)`)
|
||||||
Assert(typeof(arrays) == "table", "[Warp]: Array must be a table type")
|
Assert(typeof(arrays) == "table", "[Warp]: Array must be a table type, got {typeof(arrays)}")
|
||||||
local copy = {}
|
local copy: { [string]: Type.Server } = {}
|
||||||
for param1: any, param2: any in arrays do
|
for param1, param2: string | Type.ServerConf in arrays do
|
||||||
if typeof(param2) == "table" then
|
if typeof(param2) == "table" then
|
||||||
copy[param1] = Index.Server(param1, param2)
|
copy[param1] = Index.Server(param1, param2)
|
||||||
else
|
else
|
||||||
copy[param2] = Index.Server(param2)
|
copy[param2] = Index.Server(param2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return table.freeze(copy) :: typeof(copy)
|
return copy
|
||||||
end
|
end
|
||||||
|
|
||||||
function Index.fromClientArray(arrays: { any }): Type.fromClientArray
|
function Index.fromClientArray(arrays: { string } | { [string]: Type.ClientConf }): Type.fromClientArray
|
||||||
Assert(not IsServer, `[Warp]: Calling .fromClientArray({arrays}) on server side (expected client side)`)
|
Assert(not IsServer, `[Warp]: Calling .fromClientArray({arrays}) on server side (expected client side)`)
|
||||||
Assert(typeof(arrays) == "table", `[Warp]: Array must be a table type, got {typeof(arrays)}`)
|
Assert(typeof(arrays) == "table", `[Warp]: Array must be a table type, got {typeof(arrays)}`)
|
||||||
local copy = {}
|
local copy = {}
|
||||||
for _, identifier: string in arrays do
|
for param1, param2: string | Type.ClientConf in arrays do
|
||||||
copy[identifier] = Index.Client(identifier)
|
if typeof(param2) == "table" then
|
||||||
|
copy[param1] = Index.Client(param1, param2)
|
||||||
|
else
|
||||||
|
copy[param2] = Index.Client(param2)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return table.freeze(copy) :: typeof(copy)
|
return copy
|
||||||
end
|
end
|
||||||
|
|
||||||
function Index.Signal(Identifier: string)
|
function Index.Signal(Identifier: string)
|
||||||
|
@ -66,4 +73,8 @@ function Index.fromSignalArray(arrays: { any })
|
||||||
return copy :: typeof(copy)
|
return copy :: typeof(copy)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Index.buffer()
|
||||||
|
return Buffer.new()
|
||||||
|
end
|
||||||
|
|
||||||
return table.freeze(Index) :: typeof(Index)
|
return table.freeze(Index) :: typeof(Index)
|
|
@ -1,7 +1,8 @@
|
||||||
-- Warp Library (@Eternity_Devs)
|
-- Warp Library (@Eternity_Devs)
|
||||||
-- version 1.0.5
|
-- version 1.0.14
|
||||||
--!strict
|
--!strict
|
||||||
--!native
|
--!native
|
||||||
|
--!optimize 2
|
||||||
local Index = require(script.Index)
|
local Index = require(script.Index)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -10,6 +11,10 @@ return {
|
||||||
fromServerArray = Index.fromServerArray,
|
fromServerArray = Index.fromServerArray,
|
||||||
fromClientArray = Index.fromClientArray,
|
fromClientArray = Index.fromClientArray,
|
||||||
|
|
||||||
|
OnSpamSignal = Index.OnSpamSignal,
|
||||||
|
|
||||||
Signal = Index.Signal,
|
Signal = Index.Signal,
|
||||||
fromSignalArray = Index.fromSignalArray,
|
fromSignalArray = Index.fromSignalArray,
|
||||||
|
|
||||||
|
buffer = Index.buffer,
|
||||||
}
|
}
|
30
test.project.json
Normal file
30
test.project.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "warp-test",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
"ServerScriptService": {
|
||||||
|
"$className": "ServerScriptService",
|
||||||
|
"Test": {
|
||||||
|
"$path": "test"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"$className": "ReplicatedStorage",
|
||||||
|
"Warp": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TestService": {
|
||||||
|
"$className": "TestService",
|
||||||
|
"$properties": {
|
||||||
|
"ExecuteWithStudioRun": true
|
||||||
|
},
|
||||||
|
"TestEZ": {
|
||||||
|
"$path": "TestEZ"
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"$path": "runTests.server.luau"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
166
test/init.spec.luau
Normal file
166
test/init.spec.luau
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
return function()
|
||||||
|
local Warp = require(game:GetService("ReplicatedStorage").Warp)
|
||||||
|
|
||||||
|
describe("Warp.Server", function()
|
||||||
|
it("should be able to create a new server event", function()
|
||||||
|
local test = Warp.Server("Test")
|
||||||
|
expect(test).to.be.ok()
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should be able to create a new server event with ratelimit configuration", function()
|
||||||
|
local test = Warp.Server("Test", {
|
||||||
|
maxEntrance = 10,
|
||||||
|
interval = 1,
|
||||||
|
})
|
||||||
|
expect(test).to.be.ok()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Warp.fromServerArray", function()
|
||||||
|
it("should be able to create a new server event with arrays", function()
|
||||||
|
local test = Warp.fromServerArray({
|
||||||
|
"Test1",
|
||||||
|
"Test2",
|
||||||
|
})
|
||||||
|
expect(test).to.be.ok()
|
||||||
|
expect(test.Test1).to.be.ok()
|
||||||
|
expect(test.Test2).to.be.ok()
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should be able to create a new server event with arrays & ratelimit configuration", function()
|
||||||
|
local test = Warp.fromServerArray({
|
||||||
|
"Test1",
|
||||||
|
"Test2",
|
||||||
|
["Test3"] = {
|
||||||
|
maxEntrance = 10,
|
||||||
|
interval = 0.75,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(test).to.be.ok()
|
||||||
|
expect(test.Test1).to.be.ok()
|
||||||
|
expect(test.Test2).to.be.ok()
|
||||||
|
expect(test.Test3).to.be.ok()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Event.Connect", function()
|
||||||
|
it("should be able to connect the event", function()
|
||||||
|
local test = Warp.Server("Test")
|
||||||
|
test:Connect(function() end)
|
||||||
|
expect(test.IsConnected).to.be.ok()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Multi Event.Connect", function()
|
||||||
|
it("should be able to multiple connect the event", function()
|
||||||
|
local test = Warp.Server("Test")
|
||||||
|
test:Connect(function() end)
|
||||||
|
test:Connect(function() end)
|
||||||
|
expect(test.IsConnected).to.be.ok()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Event.DisconnectAll", function()
|
||||||
|
it("should be able to disconnect all the event connections", function()
|
||||||
|
local test = Warp.Server("Test")
|
||||||
|
test:DisconnectAll()
|
||||||
|
expect(#test.fn).to.equal(0)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Event.Disconnect", function()
|
||||||
|
it("should be able to disconnect the event connection", function()
|
||||||
|
local test = Warp.Server("Test")
|
||||||
|
local connection = test:Connect(function() end)
|
||||||
|
expect(test:Disconnect(connection)).to.be.ok()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Warp.Signal", function()
|
||||||
|
it("should be able to create a new signal", function()
|
||||||
|
local test = Warp.Signal("Test")
|
||||||
|
expect(test).to.be.ok()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Warp.fromSignalArray", function()
|
||||||
|
it("should be able to create a new signal with arrays", function()
|
||||||
|
local test = Warp.fromSignalArray({
|
||||||
|
"Test1",
|
||||||
|
"Test2"
|
||||||
|
})
|
||||||
|
expect(test).to.be.ok()
|
||||||
|
expect(test.Test1).to.be.ok()
|
||||||
|
expect(test.Test2).to.be.ok()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Signal.Connect", function()
|
||||||
|
it("should be able to connect the signal", function()
|
||||||
|
local test = Warp.Signal("Test")
|
||||||
|
expect(test:Connect(function() end)).to.be.ok()
|
||||||
|
test:DisconnectAll()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Multi Signal.Connect", function()
|
||||||
|
it("should be able to multiple connect the signal", function()
|
||||||
|
local test = Warp.Signal("Test")
|
||||||
|
expect(test:Connect(function() end)).to.be.ok()
|
||||||
|
expect(test:Connect(function() end)).to.be.ok()
|
||||||
|
test:DisconnectAll()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Signal.Fire", function()
|
||||||
|
it("should be able to fire the signal", function()
|
||||||
|
local test = Warp.Signal("Test")
|
||||||
|
test:Once(function(arg)
|
||||||
|
expect(arg).to.equal("hello world!")
|
||||||
|
end)
|
||||||
|
test:Fire("hello world!")
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Signal.Invoke", function()
|
||||||
|
it("should be able to invoke the signal", function()
|
||||||
|
local test = Warp.Signal("Test")
|
||||||
|
local connection = test:Connect(function(arg)
|
||||||
|
if arg ~= "test" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
return "hello world!"
|
||||||
|
end)
|
||||||
|
local receive = test:Invoke(connection, "test")
|
||||||
|
expect(receive).to.equal("hello world!")
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Signal.InvokeTo", function()
|
||||||
|
it("should be able to invoke to a signal", function()
|
||||||
|
local test = Warp.Signal("Test")
|
||||||
|
local test2 = Warp.Signal("Test2")
|
||||||
|
local connection = test2:Connect(function(arg)
|
||||||
|
if arg ~= "test" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
return "hello world!"
|
||||||
|
end)
|
||||||
|
local receive = test:InvokeTo("Test2", connection, "test")
|
||||||
|
expect(receive).to.equal("hello world!")
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Signal.Wait", function()
|
||||||
|
it("should be able to wait for the signal", function()
|
||||||
|
local test = Warp.Signal("Test")
|
||||||
|
test:Connect(function() end)
|
||||||
|
task.spawn(function()
|
||||||
|
local time = test:Wait()
|
||||||
|
expect(time).to.be.ok()
|
||||||
|
expect(time).to.be.a("number")
|
||||||
|
end)
|
||||||
|
test:Fire()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
|
@ -1,14 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "imezx/warp"
|
name = "imezx/warp"
|
||||||
version = "1.0.5"
|
version = "1.0.14"
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
realm = "shared"
|
realm = "shared"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
exclude = [
|
exclude = ["node_modules", "docs", ".github", "*.rbxl", "*.rbxmx", "*.rbxml", "*.rbxm", "TestEZ", "test.project.json"]
|
||||||
"node_modules",
|
|
||||||
"docs",
|
|
||||||
".github",
|
|
||||||
]
|
|
||||||
description = "A very-fast & powerful networking library for Roblox."
|
description = "A very-fast & powerful networking library for Roblox."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
Loading…
Reference in a new issue