Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
1651a53528 | |||
c36e812c03 | |||
954dc3c607 | |||
0ebb9e6007 | |||
e60721c110 | |||
2119809d2a | |||
89f65e6025 |
42
.woodpecker.yaml
Normal file
42
.woodpecker.yaml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
steps:
|
||||||
|
- name: build-release
|
||||||
|
image: alpine
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
commands:
|
||||||
|
- apk add openjdk21
|
||||||
|
- apk add wget
|
||||||
|
- apk add tar
|
||||||
|
- wget https://github.com/demonnic/muddler/releases/download/0.13/muddle-shadow-0.13.tar
|
||||||
|
- tar -xvf muddle-shadow-0.13.tar
|
||||||
|
- mv muddle-shadow-0.13/lib/muddle-0.13-all.jar muddle.jar
|
||||||
|
- java -jar muddle.jar
|
||||||
|
|
||||||
|
- name: deploy-build
|
||||||
|
image: alpine
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
environment:
|
||||||
|
GITEATKN:
|
||||||
|
from_secret: GITEA_TOKEN
|
||||||
|
GOTOKEN:
|
||||||
|
from_secret: GOTIFY_TOKEN
|
||||||
|
depends_on: build-release
|
||||||
|
commands:
|
||||||
|
- apk add tea
|
||||||
|
- apk add wget
|
||||||
|
- tea login add --name=temp --url=https://git.vertinext.com/ --token=$GITEATKN
|
||||||
|
- tea releases create --asset "build/lotj-ui.mpackage" --asset "build/lotj-ui.xml" --tag ${CI_COMMIT_TAG} --title "Latest Compiled Release" --note "Use .mpackage for Mudlet and .xml for Mushclient"
|
||||||
|
- wget "https://gotify.vertinext.com/message?token=$GOTOKEN" --post-data "title=${CI} - ${CI_REPO_NAME}&message=New release ${CI_COMMIT_TAG} built and deployed.&priority=0" -O /dev/null
|
||||||
|
|
||||||
|
- name: notify-on-branch-push
|
||||||
|
image: alpine
|
||||||
|
when:
|
||||||
|
- event: [push, pull_request, tag, release]
|
||||||
|
- status: [success, failure]
|
||||||
|
environment:
|
||||||
|
GOTOKEN:
|
||||||
|
from_secret: GOTIFY_TOKEN
|
||||||
|
commands:
|
||||||
|
- apk add wget
|
||||||
|
- wget "https://gotify.vertinext.com/message?token=$GOTOKEN" --post-data "title=${CI} - ${CI_REPO_NAME}&message=New ${CI_PIPELINE_EVENT} to ${CI_REPO_NAME}/${CI_COMMIT_BRANCH}.&priority=0" -O /dev/null
|
2
LICENSE
2
LICENSE
@ -199,3 +199,5 @@
|
|||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
75
README.md
75
README.md
@ -1,70 +1,9 @@
|
|||||||
# lotj-mudlet-ui
|
# New Readme
|
||||||
|
|
||||||
This is an attempt to provide a richer UI for [Legends of the Jedi MUD](https://www.legendsofthejedi.com/) in Mudlet.
|
This is a custom fork of the lotj-mudlet-ui that includes some additional features as listed below.
|
||||||
|
|
||||||
![Image of UI with ground map](https://raw.githubusercontent.com/LotJ/lotj-mudlet-ui/main/images/ground-map.png)
|
* Virtual Ship Maps
|
||||||
|
* Integrated Autostudy
|
||||||
|
* TLC Automator
|
||||||
## Features
|
* Engineering Suite
|
||||||
|
* Skill Training Suite
|
||||||
### Ground Map
|
|
||||||
|
|
||||||
This package includes a script hooking into Mudlet's mapper so you can map by (mostly) just walking around an unexplored area.
|
|
||||||
|
|
||||||
It works fairly well on all existing planets. It's based on room vnums, which means it will consider each ship to be fully unique rooms.
|
|
||||||
|
|
||||||
### Local System Map
|
|
||||||
|
|
||||||
![Image of UI with ground map](https://raw.githubusercontent.com/LotJ/lotj-mudlet-ui/main/images/system-map.png)
|
|
||||||
|
|
||||||
When flying in a system, triggers capture radar output and draw a visual representation of the radar, including zooming in/out and updating proximity of each other entity as your position changes.
|
|
||||||
|
|
||||||
### Galaxy Map
|
|
||||||
|
|
||||||
![Image of UI with ground map](https://raw.githubusercontent.com/LotJ/lotj-mudlet-ui/main/images/galaxy-map.png)
|
|
||||||
|
|
||||||
After initializing it by running various in-game commands, this map will show all publicly listed starsystems, including coloring each government's planets differently. It will also attempt to highlight your current system when known, although that only works while in space.
|
|
||||||
|
|
||||||
### Chat windows
|
|
||||||
|
|
||||||
Certain types of chat content are scraped from the main console and copied into tabbed chat windows for easier history browsing.
|
|
||||||
|
|
||||||
### Live-updating Status Bar
|
|
||||||
|
|
||||||
![Image of Status Bar](https://raw.githubusercontent.com/LotJ/lotj-mudlet-ui/main/images/stats-bar.png)
|
|
||||||
|
|
||||||
Right above your input box, you'll see a bunch of useful information which updates live. This includes:
|
|
||||||
|
|
||||||
- Your HP/Move/(Mana?)
|
|
||||||
- Opponent's name and percentage
|
|
||||||
- Current comlink channel and encryption code
|
|
||||||
- Ship speed, coordinates, hull, shield, energy
|
|
||||||
- Piloting and chaff indicators, and a countdown to the next space tick
|
|
||||||
|
|
||||||
|
|
||||||
## Installing
|
|
||||||
|
|
||||||
After creating a Mudlet profile to connect to LOTJ, do the following to add the package:
|
|
||||||
|
|
||||||
1. Download a release of this package (the `.mpackage` file) from the [releases page](https://github.com/LotJ/lotj-mudlet-ui/releases)
|
|
||||||
1. Open the **Package Manager**
|
|
||||||
1. If present, uninstall the **generic-mapper** package. It conflicts with the one provided here.
|
|
||||||
1. Select the `lotj-ui-<version>.mpackage` file you downloaded before for installation
|
|
||||||
1. Restart Mudlet and reconnect. The UI should populate fully once you log into a character.
|
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
The source for this package is structured to use [muddler](https://github.com/demonnic/muddler) to package it into a Mudlet package. Using version 0.1 is necessary at this time due to some errant behavior by later Muddler versions.
|
|
||||||
|
|
||||||
You can, of course, just modify the triggers/aliases/scripts directly within Mudlet if you want to test local changes, but they'll be overwritten if you want to update to future versions of this package.
|
|
||||||
|
|
||||||
To change the source for this package, modify the JSON files and associated Lua scripts inside the `src` directory, then run `muddler` to regenerate the package. The resulting `.mpackage` file will be inside the `build` directory.
|
|
||||||
|
|
||||||
If you have Docker set up, it can be easiest to run a command like this to regenerate the package, from the root of the repository:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run --rm -it -u $(id -u):$(id -g) -v $PWD:/$PWD -w /$PWD demonnic/muddler:0.1
|
|
||||||
```
|
|
||||||
|
|
||||||
If that's a pain, just make a pull request and someone else can generate the package with your changes to make sure they work.
|
|
2
mfile
2
mfile
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"package": "lotj-ui",
|
"package": "lotj-ui",
|
||||||
"version": "v2.3.3"
|
"version": "v2.4"
|
||||||
}
|
}
|
30
src/aliases/autostudy/aliases.json
Normal file
30
src/aliases/autostudy/aliases.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "study.add",
|
||||||
|
"regex": "^studyadd (.*)$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "study.start",
|
||||||
|
"regex": "^studystart$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "study.auto",
|
||||||
|
"regex": "^studyauto$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "study.list",
|
||||||
|
"regex": "^studylist$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "study.clear",
|
||||||
|
"regex": "^studyclear$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "study.resume",
|
||||||
|
"regex": "^studyresume$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "study.help",
|
||||||
|
"regex": "^studyhelp$"
|
||||||
|
}
|
||||||
|
]
|
3
src/aliases/autostudy/study.add.lua
Normal file
3
src/aliases/autostudy/study.add.lua
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
studyList = studyList or {}
|
||||||
|
table.insert(studyList, matches[2])
|
||||||
|
cecho(matches[2] .. "<dodger_blue> added.")
|
25
src/aliases/autostudy/study.auto.lua
Normal file
25
src/aliases/autostudy/study.auto.lua
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
studyIndex = 1
|
||||||
|
studyList = {
|
||||||
|
"camera",
|
||||||
|
"set",
|
||||||
|
"construction",
|
||||||
|
"data",
|
||||||
|
"sword",
|
||||||
|
"buffet",
|
||||||
|
"tome",
|
||||||
|
"datpad",
|
||||||
|
"bioplug",
|
||||||
|
"gloves",
|
||||||
|
"dna",
|
||||||
|
"beacon",
|
||||||
|
"terraform",
|
||||||
|
"support",
|
||||||
|
"formation",
|
||||||
|
"spice",
|
||||||
|
"models",
|
||||||
|
"bag"
|
||||||
|
}
|
||||||
|
cecho("<dodger_blue>Starting Ithor TLC Automator.")
|
||||||
|
send("bot start")
|
||||||
|
send("study " .. studyList[studyIndex])
|
||||||
|
enableTrigger("autostudy")
|
5
src/aliases/autostudy/study.clear.lua
Normal file
5
src/aliases/autostudy/study.clear.lua
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
studyList = []
|
||||||
|
studyIndex = 0
|
||||||
|
disableTrigger("autostudy")
|
||||||
|
|
||||||
|
cecho("<dodger_blue>Cleared list of study items.")
|
0
src/aliases/autostudy/study.help.lua
Normal file
0
src/aliases/autostudy/study.help.lua
Normal file
32
src/aliases/autostudy/study.list.lua
Normal file
32
src/aliases/autostudy/study.list.lua
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
local TableMaker = require("lotj-ui.MDK.ftext").TableMaker
|
||||||
|
|
||||||
|
if not studyList or #studyList == 0 then
|
||||||
|
cecho("<dodger_blue>Studylist Empty.")
|
||||||
|
else
|
||||||
|
studyTable = TableMaker:new({
|
||||||
|
title = "Studylist",
|
||||||
|
printTitle = true,
|
||||||
|
printHeaders = false,
|
||||||
|
separateRows = false,
|
||||||
|
frameColor = "<purple>"
|
||||||
|
})
|
||||||
|
|
||||||
|
studyTable:addColumn({
|
||||||
|
name = "Index",
|
||||||
|
width = "5",
|
||||||
|
textColor = "<mint_cream>"
|
||||||
|
})
|
||||||
|
studyTable:addColumn({
|
||||||
|
name = "Study Item",
|
||||||
|
width = "25",
|
||||||
|
textColor = "<medium_slate_blue>"
|
||||||
|
})
|
||||||
|
|
||||||
|
for index, value in ipairs(studyList) do
|
||||||
|
studyTable:addRow({
|
||||||
|
index,
|
||||||
|
value
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
cecho(studyTable:assemble())
|
5
src/aliases/autostudy/study.resume.lua
Normal file
5
src/aliases/autostudy/study.resume.lua
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
if #studyList > 0 and studyIndex then
|
||||||
|
send("study " .. studyList[studyIndex])
|
||||||
|
else
|
||||||
|
send("<dodger_blue>You need to add items to your study list first. See studyhelp for commands.")
|
||||||
|
end
|
8
src/aliases/autostudy/study.start.lua
Normal file
8
src/aliases/autostudy/study.start.lua
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
if #studyList > 0 then
|
||||||
|
studyIndex = studyIndex or 1
|
||||||
|
if studyIndex == 0 then studyIndex = 1 end
|
||||||
|
send("study " .. studyList[studyIndex])
|
||||||
|
enableTrigger("autostudy")
|
||||||
|
else
|
||||||
|
cecho("<dodger_blue>No study list. See studyhelp for commands.")
|
||||||
|
end
|
48
src/resources/MDK/LICENSE.lua
Executable file
48
src/resources/MDK/LICENSE.lua
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
--[===[
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2020 Damian Monogue
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
--]===]
|
||||||
|
|
||||||
|
-- schema validation provided by schema.lua, license below
|
||||||
|
--[[
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Sebastian Schoener
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
]]
|
91
src/resources/MDK/README.md
Executable file
91
src/resources/MDK/README.md
Executable file
@ -0,0 +1,91 @@
|
|||||||
|
# the demonnic MDK and You
|
||||||
|
|
||||||
|
This is a collection of Lua 'classes' and modules I wrote for Mudlet. It is largely targeted at scripters, and comes packaged in two ways depending on how you intend to use/distribute your work. Please see [Installation](#installation) for more details
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
The [MDK wiki](https://github.com/demonnic/MDK/wiki) contains an entry for each module or class, as well as examples.
|
||||||
|
|
||||||
|
Starting with alpha2 of the MDK, the ldocs generated from code are included in the zipped releases. The current release's ldocs can always be viewed at <https://demonnic.github.io/mdk/current/>
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
How you 'install' the MDK depends on how you intend to use it.
|
||||||
|
|
||||||
|
### I just want to install the MDK for my own personal use
|
||||||
|
|
||||||
|
You just want to get your hands on the goods, and aren't looking to use any MDK items in an exported package for sharing or anything like that.
|
||||||
|
Well, you are who the mdk mpackage is for! Download the MDK.mpackage from your desired release on the [Releases](https://github.com/demonnic/MDK/releases) page and install it in the package manager. The examples in the [wiki](https://demonnic.github.io/mdk/current/) are written with this in mind, and you would require the items you need as `local EMCO = require("MDK.emco")`
|
||||||
|
|
||||||
|
### I am a package author looking to include/use one of the MDK modules or classes in my package
|
||||||
|
|
||||||
|
You should download the `demonnic-MDK-<version>.zip` file for your desired release on the [Releases](https://github.com/demonnic/MDK/releases) page.
|
||||||
|
Inside are the individual .lua files for the modules and classes described in the [wiki](https://demonnic.github.io/mdk/current/) and [API docs](https://demonnic.github.io/mdk/current/).
|
||||||
|
You can include all of them if you wish, or only the ones you actually make use of. I ask that you include the LICENSE.lua or LICENSE-MDK.lua file (depending on the release) file in addition.
|
||||||
|
They should go in the root of your package, so that when your package is installed the files can be found at `getMudletHomeDir() .. "/<packagename>/emco.lua"`. You would then use `local EMCO = require("<mypackagename>.emco")`
|
||||||
|
So for example if your package name is "MySuperCoolPackage" and it installs to `getMudletHomeDir() .. "/MySuperCoolPackage/"` then you use `local EMCO = require("MySuperCoolPackage.emco")` and the emco.lua file should be at `getMudletHomeDir() .. "/MySuperCoolPackage/emco.lua"`
|
||||||
|
|
||||||
|
## Files (Modules/Classes)
|
||||||
|
|
||||||
|
These files contain the modules in the MDK. You only need to include those files which you intend to use, except as noted in the descriptions below.
|
||||||
|
If you include any of the modules from the MDK, you should also include LICENSE.lua or LICENSE-MDK.lua. It contains the licenses for my modules and for luaunit and lua-schema which are not my original works.
|
||||||
|
You should maybe also include demontools.lua, as it notes below several other of the MDK modules make use of items within it.
|
||||||
|
|
||||||
|
* aliasmgr.ua
|
||||||
|
* Object to manage tempAliases programmatically. <https://github.com/demonnic/MDK/wiki/AliasMgr>
|
||||||
|
|
||||||
|
* chyron.lua
|
||||||
|
* Label which moves a message across its face from right to left, like a stock ticker or the news chyrons. Documentation at <https://github.com/demonnic/MDK/wiki/Chyron>
|
||||||
|
|
||||||
|
* demontools.lua
|
||||||
|
* Collection of miscellaneous useful functions. You should include this file if you use the MDK, as several other modules make use of it. Include functions for converting c/d/hecho, html, and ansi colored strings between each other, mkdir_p, and some others. <https://github.com/demonnic/MDK/wiki/DemonTools>
|
||||||
|
|
||||||
|
* emco.lua
|
||||||
|
* EMCO. Documentation at <https://github.com/demonnic/MDK/wiki/EMCO> Will make use of LoggingConsole if loggingconsole.lua and demontools.lua are included
|
||||||
|
|
||||||
|
* figlet.lua
|
||||||
|
* Creates FIGlets from strings
|
||||||
|
* Reference package with multiple fonts and color gradients at <https://github.com/demonnic/figinator>
|
||||||
|
|
||||||
|
* ftext.lua
|
||||||
|
* basic fText. Documentation at <https://github.com/demonnic/MDK/wiki/fText>
|
||||||
|
* now includes TextFormatter and TableMaker as ftext.TextFormatter and ftext.TableMaker
|
||||||
|
|
||||||
|
* gradientmaker.lua
|
||||||
|
* Functions for creating color gradients for use with c/d/hecho. Documentation at <https://github.com/demonnic/MDK/wiki/GradientMaker>
|
||||||
|
|
||||||
|
* loggingconsole.lua
|
||||||
|
* Self logging extension to the mini console. Works just like a Geyser.MiniConsole but adds a templated path and fileName constraint, as well as logFormat so it can log what is echod or appended to it. Requires demontools.lua in order to work.
|
||||||
|
|
||||||
|
* loginator.lua
|
||||||
|
* Creates objects for logging messages to disk. <https://github.com/demonnic/MDK/wiki/Loginator>
|
||||||
|
|
||||||
|
* mastermindsolver.lua
|
||||||
|
* A class which will help you solve Master Mind puzzles. <https://github.com/demonnic/MDK/wiki/MasterMindSolver>
|
||||||
|
|
||||||
|
* revisionator.lua
|
||||||
|
* A class which aims to make upgrading between package versions easier by storing and running patch functions. <https://github.com/demonnic/MDK/wiki/Revisionator>
|
||||||
|
|
||||||
|
* sortbox.lua
|
||||||
|
* SortBox, an alternative to H/VBox which can be either, and also provides options for sorting its contents. Overview at <https://github.com/demonnic/MDK/wiki/SortBox>
|
||||||
|
|
||||||
|
* spinbox.lua
|
||||||
|
* SpinBox, a Geyser element for adjusting numbers with your mouse. Overview at <https://github.com/demonnic/MDK/wiki/SpinBox>
|
||||||
|
|
||||||
|
* sug.lua
|
||||||
|
* Self Updating Gauges, will watch a set of variables and update itself on a timer based on what values those variables hold. Documentation at <https://github.com/demonnic/MDK/wiki/SelfUpdatingGauge>
|
||||||
|
|
||||||
|
* textgauge.lua
|
||||||
|
* TextGauges, what it says on the tin. Documentation at <https://github.com/demonnic/MDK/wiki/TextGauge>
|
||||||
|
|
||||||
|
* timergauge.lua
|
||||||
|
* TimerGauge, an extension of Geyser.Gauge which serves as an animated countdown timer. Overview at <https://github.com/demonnic/MDK/wiki/TimerGauge>
|
||||||
|
|
||||||
|
## Others people's work I depend upon
|
||||||
|
|
||||||
|
* schema.lua
|
||||||
|
* lua-schema, for defining table schema. Documentation at <https://github.com/sschoener/lua-schema>
|
||||||
|
* will be used by Archon for ensuring configuration tables are as they should be.
|
||||||
|
|
||||||
|
* LICENSE.lua
|
||||||
|
* Contains the license information for MDK, as well as lua-schema and luaunit which have been included.
|
164
src/resources/MDK/aliasmgr.lua
Executable file
164
src/resources/MDK/aliasmgr.lua
Executable file
@ -0,0 +1,164 @@
|
|||||||
|
--- Alias Manager
|
||||||
|
-- @classmod aliasmgr
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2022 Damian Monogue
|
||||||
|
-- @license MIT, see LICENSE.lua
|
||||||
|
local aliasmgr = {}
|
||||||
|
aliasmgr.__index = aliasmgr
|
||||||
|
|
||||||
|
--- Creates a new alias manager
|
||||||
|
function aliasmgr:new()
|
||||||
|
local mgr = {
|
||||||
|
aliases = {}
|
||||||
|
}
|
||||||
|
setmetatable(mgr, self)
|
||||||
|
return mgr
|
||||||
|
end
|
||||||
|
|
||||||
|
local function argError(funcName, argument, expected, actual)
|
||||||
|
local msg = string.format("%s: %s as %s expected, got %s", funcName, argument, expected, actual)
|
||||||
|
printError(msg, true, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Registers an alias with the alias manager
|
||||||
|
-- @param name the name for the alias
|
||||||
|
-- @param regex the regular expression the alias matches against
|
||||||
|
-- @param func The code to run when the alias matches. Can wrap code in [[ ]] or pass an actual function
|
||||||
|
function aliasmgr:register(name, regex, func)
|
||||||
|
local funcName = "aliasmgr:register(name, regex, func)"
|
||||||
|
if func == nil then
|
||||||
|
printError(f"{funcName} takes 3 arguments and you have provided less than that", true, true)
|
||||||
|
end
|
||||||
|
local nameType = type(name)
|
||||||
|
if nameType ~= "string" then
|
||||||
|
argError(funcName, "name", "string", nameType)
|
||||||
|
end
|
||||||
|
local regexType = type(regex)
|
||||||
|
if regexType ~= "string" then
|
||||||
|
argError(funcName, "regex", "string", regexType)
|
||||||
|
end
|
||||||
|
local funcType = type(func)
|
||||||
|
if funcType ~= "string" and funcType ~= "function" then
|
||||||
|
argError(funcName, "func", "string or function", funcType)
|
||||||
|
end
|
||||||
|
local object = {
|
||||||
|
regex = regex,
|
||||||
|
func = func
|
||||||
|
}
|
||||||
|
self:kill(name)
|
||||||
|
local ok, err = pcall(tempAlias, regex, func)
|
||||||
|
if not ok then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
object.handlerID = err
|
||||||
|
self.aliases[name] = object
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Registers an alias with the alias manager. Alias for register
|
||||||
|
-- @param name the name for the alias
|
||||||
|
-- @param regex the regular expression the alias matches against
|
||||||
|
-- @param func The code to run when the alias matches. Can wrap code in [[ ]] or pass an actual function
|
||||||
|
-- @see register
|
||||||
|
function aliasmgr:add(name, regex, func)
|
||||||
|
self:register(name, regex, func)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Disables an alias, but does not delete it so it can be enabled later without being redefined
|
||||||
|
-- @param name the name of the alias to disable
|
||||||
|
-- @return true if the alias exists and gets disabled, false if it does not exist or is already disabled
|
||||||
|
function aliasmgr:disable(name)
|
||||||
|
local funcName = "aliasmgr:disable(name)"
|
||||||
|
local nameType = type(name)
|
||||||
|
if nameType ~= "string" then
|
||||||
|
argError(funcName, "name", "string", nameType)
|
||||||
|
end
|
||||||
|
local object = self.aliases[name]
|
||||||
|
if not object or object.handlerID == -1 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
killAlias(object.handlerID)
|
||||||
|
object.handlerID = -1
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Disables all aliases registered with the manager
|
||||||
|
function aliasmgr:disableAll()
|
||||||
|
local aliases = self.aliases
|
||||||
|
for name, object in pairs(aliases) do
|
||||||
|
self:disable(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Enables an alias by name
|
||||||
|
-- @param name the name of the alias to enable
|
||||||
|
-- @return true if the alias exists and was enabled, false if it does not exist.
|
||||||
|
function aliasmgr:enable(name)
|
||||||
|
local funcName = "aliasmgr:enable(name)"
|
||||||
|
local nameType = type(name)
|
||||||
|
if nameType ~= "string" then
|
||||||
|
argError(funcName, "name", "string", nameType)
|
||||||
|
end
|
||||||
|
local object = self.aliases[name]
|
||||||
|
if not object then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self:register(name, object.regex, object.func)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Enables all aliases registered with the manager
|
||||||
|
function aliasmgr:enableAll()
|
||||||
|
local aliases = self.aliases
|
||||||
|
for name,_ in pairs(aliases) do
|
||||||
|
self:enable(name)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Kill an alias, deleting it from the manager
|
||||||
|
-- @param name the name of the alias to kill
|
||||||
|
-- @return true if the alias exists and gets deleted, false if the alias does not exist
|
||||||
|
function aliasmgr:kill(name)
|
||||||
|
local funcName = "aliasmgr:kill(name)"
|
||||||
|
local nameType = type(name)
|
||||||
|
if nameType ~= "string" then
|
||||||
|
argError(funcName, "name", "string", nameType)
|
||||||
|
end
|
||||||
|
local object = self.aliases[name]
|
||||||
|
if not object then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self:disable(name)
|
||||||
|
self.aliases[name] = nil
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Kills all aliases registered with the manager, clearing it out
|
||||||
|
function aliasmgr:killAll()
|
||||||
|
local aliases = self.aliases
|
||||||
|
for name, _ in pairs(aliases) do
|
||||||
|
self:kill(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Kills an alias, deleting it from the manager
|
||||||
|
-- @param name the name of the alias to delete
|
||||||
|
-- @return true if the alias exists and gets deleted, false if the alias does not exist
|
||||||
|
-- @see kill
|
||||||
|
function aliasmgr:delete(name)
|
||||||
|
return self:kill(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Kills all aliases, deleting them from the manager
|
||||||
|
-- @see killAll
|
||||||
|
function aliasmgr:deleteAll()
|
||||||
|
return self:killAll()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the list of aliases and the information being tracked for them
|
||||||
|
-- @return the table of alias information, with names as keys and a table of information as the values.
|
||||||
|
function aliasmgr:getAliases()
|
||||||
|
return self.aliases
|
||||||
|
end
|
||||||
|
|
||||||
|
return aliasmgr
|
235
src/resources/MDK/chyron.lua
Executable file
235
src/resources/MDK/chyron.lua
Executable file
@ -0,0 +1,235 @@
|
|||||||
|
--- Creates a label with a scrolling text element. It is highly recommended you use a monospace font for this label.
|
||||||
|
-- @classmod Chyron
|
||||||
|
-- @author Delra
|
||||||
|
-- @copyright 2019
|
||||||
|
-- @author Damian Monogue
|
||||||
|
-- @copyright 2020
|
||||||
|
local Chyron = {
|
||||||
|
name = "ChyronClass",
|
||||||
|
text = "",
|
||||||
|
displayWidth = 28,
|
||||||
|
updateTime = 200,
|
||||||
|
font = "Bitstream Vera Sans Mono",
|
||||||
|
fontSize = "9",
|
||||||
|
autoWidth = true,
|
||||||
|
delimiter = "|",
|
||||||
|
pos = 1,
|
||||||
|
enabled = true,
|
||||||
|
alignment = "center",
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Creates a new Chyron label
|
||||||
|
-- @tparam table cons table of constraints which configures the EMCO.
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <thead>
|
||||||
|
-- <tr>
|
||||||
|
-- <th>option name</th>
|
||||||
|
-- <th>description</th>
|
||||||
|
-- <th>default</th>
|
||||||
|
-- </tr>
|
||||||
|
-- </thead>
|
||||||
|
-- <tbody>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">text</td>
|
||||||
|
-- <td class="tg-1">The text to scroll on the label</td>
|
||||||
|
-- <td class="tg-1">""</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">updateTime</td>
|
||||||
|
-- <td class="tg-2">Milliseconds between movements (one letter shift)</td>
|
||||||
|
-- <td class="tg-2">200</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">displayWidth</td>
|
||||||
|
-- <td class="tg-1">How many chars wide to display the text</td>
|
||||||
|
-- <td class="tg-1">28</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">delimiter</td>
|
||||||
|
-- <td class="tg-2">This character will be inserted with a space either side to mark the stop/start of the message</td>
|
||||||
|
-- <td class="tg-2">"|"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">enabled</td>
|
||||||
|
-- <td class="tg-1">Should the chyron scroll?</td>
|
||||||
|
-- <td class="tg-1">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">font</td>
|
||||||
|
-- <td class="tg-2">What font to use for the Chyron? Available in Geyser.Label but we define a default.</td>
|
||||||
|
-- <td class="tg-2">"Bitstream Vera Sans Mono"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">fontSize</td>
|
||||||
|
-- <td class="tg-1">What font size to use for the Chyron? Available in Geyser.Label but we define a default.</td>
|
||||||
|
-- <td class="tg-1">9</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">autoWidth</td>
|
||||||
|
-- <td class="tg-2">Should the Chyron resize to just fit the text?</td>
|
||||||
|
-- <td class="tg-2">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">alignment</td>
|
||||||
|
-- <td class="tg-1">What alignment(left/right/center) to use for the Chyron text? Available in Geyser.Label but we define a default.</td>
|
||||||
|
-- <td class="tg-1">"center"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- </tbody>
|
||||||
|
-- </table>
|
||||||
|
-- @tparam GeyserObject container The container to use as the parent for the Chyron
|
||||||
|
function Chyron:new(cons, container)
|
||||||
|
cons = cons or {}
|
||||||
|
cons.type = cons.type or "Chyron"
|
||||||
|
local me = self.parent:new(cons, container)
|
||||||
|
setmetatable(me, self)
|
||||||
|
self.__index = self
|
||||||
|
me.pos = 0
|
||||||
|
me:setDisplayWidth(me.displayWidth)
|
||||||
|
me:setMessage(me.text)
|
||||||
|
if me.enabled then
|
||||||
|
me:start()
|
||||||
|
else
|
||||||
|
me:stop()
|
||||||
|
end
|
||||||
|
return me
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the numver of characters of the text to display at once
|
||||||
|
-- @tparam number displayWidth number of characters to show at once
|
||||||
|
function Chyron:setDisplayWidth(displayWidth)
|
||||||
|
displayWidth = displayWidth or self.displayWidth
|
||||||
|
self.displayWidth = displayWidth
|
||||||
|
if self.autoWidth then
|
||||||
|
local width = calcFontSize(self.fontSize, self.font)
|
||||||
|
self:resize(width * (displayWidth + 2), self.height)
|
||||||
|
end
|
||||||
|
if not self.enabled then
|
||||||
|
self.pos = self.pos - 1
|
||||||
|
self:doScroll()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Override setFontSize to call setDisplayWidth in order to resize if necessary
|
||||||
|
-- @local
|
||||||
|
function Chyron:setFontSize(fontSize)
|
||||||
|
Geyser.Label.setFontSize(self, fontSize)
|
||||||
|
self:setDisplayWidth()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Override setFont to call setDisplayWidth in order to resize if necessary
|
||||||
|
-- @local
|
||||||
|
function Chyron:setFont(font)
|
||||||
|
Geyser.Label.setFont(self, font)
|
||||||
|
self:setDisplayWidth()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the proper section of text
|
||||||
|
-- @local
|
||||||
|
-- @param start number the character to start at
|
||||||
|
-- @param length number the length of the text you want to extract
|
||||||
|
function Chyron:scrollText(start, length)
|
||||||
|
local t = self.textTable
|
||||||
|
local s = ''
|
||||||
|
local e = start + length
|
||||||
|
for i = start - 1, e - 2 do
|
||||||
|
local n = (i % #t) + 1
|
||||||
|
s = s .. t[n]
|
||||||
|
end
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
--- scroll the text
|
||||||
|
-- @local
|
||||||
|
function Chyron:doScroll()
|
||||||
|
self.pos = self.pos + 1
|
||||||
|
local displayString = self:scrollText(self.pos, self.displayWidth)
|
||||||
|
self:echo('<' .. displayString .. '>')
|
||||||
|
self.message = self.text
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the Chyron from the first position, without changing enabled status
|
||||||
|
function Chyron:reset()
|
||||||
|
self.pos = 0
|
||||||
|
if not self.enabled then
|
||||||
|
self:doScroll()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Stops the Chyron with its current display
|
||||||
|
function Chyron:pause()
|
||||||
|
self.enabled = false
|
||||||
|
if self.timer then
|
||||||
|
killTimer(self.timer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Start the Chyron back up from wherever it currently is
|
||||||
|
function Chyron:start()
|
||||||
|
self.enabled = true
|
||||||
|
if self.timer then
|
||||||
|
killTimer(self.timer)
|
||||||
|
end
|
||||||
|
self.timer = tempTimer(self.updateTime / 1000, function()
|
||||||
|
self:doScroll()
|
||||||
|
end, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Change the update time for the Chyron
|
||||||
|
-- @param updateTime number new updateTime in milliseconds
|
||||||
|
function Chyron:setUpdateTime(updateTime)
|
||||||
|
self.updateTime = updateTime or self.updateTime
|
||||||
|
if self.timer then
|
||||||
|
killTimer(self.timer)
|
||||||
|
end
|
||||||
|
if self.enabled then
|
||||||
|
self:start()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Enable autoWidth adjustment
|
||||||
|
function Chyron:enableAutoWidth()
|
||||||
|
self.autoWidth = true
|
||||||
|
self:setDisplayWidth()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Disable autoWidth adjustment
|
||||||
|
function Chyron:disableAutoWidth()
|
||||||
|
self.autoWidth = false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Stop the Chyron, and reset it to the original position
|
||||||
|
function Chyron:stop()
|
||||||
|
if self.timer then
|
||||||
|
killTimer(self.timer)
|
||||||
|
end
|
||||||
|
self.enabled = false
|
||||||
|
self.pos = 0
|
||||||
|
self:doScroll()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Change the text being scrolled on the Chyron
|
||||||
|
-- @param message string message the text you want to have scroll on the Chyron
|
||||||
|
function Chyron:setMessage(message)
|
||||||
|
self.text = message
|
||||||
|
self.pos = 0
|
||||||
|
message = string.format("%s %s ", message, self.delimiter)
|
||||||
|
local t = {}
|
||||||
|
for i = 1, #message do
|
||||||
|
t[i] = message:sub(i, i)
|
||||||
|
end
|
||||||
|
self.textTable = t
|
||||||
|
if not self.enabled then
|
||||||
|
self:doScroll()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Change the delimiter used to show the beginning and end of the message
|
||||||
|
-- @param delimiter string the new delimiter to use. I recommend using one character.
|
||||||
|
function Chyron:setDelimiter(delimiter)
|
||||||
|
self.delimiter = delimiter
|
||||||
|
end
|
||||||
|
|
||||||
|
Chyron.parent = Geyser.Label
|
||||||
|
setmetatable(Chyron, Geyser.Label)
|
||||||
|
|
||||||
|
return Chyron
|
BIN
src/resources/MDK/computer.png
Executable file
BIN
src/resources/MDK/computer.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 343 KiB |
1486
src/resources/MDK/demontools.lua
Executable file
1486
src/resources/MDK/demontools.lua
Executable file
File diff suppressed because it is too large
Load Diff
296
src/resources/MDK/echofile.lua
Executable file
296
src/resources/MDK/echofile.lua
Executable file
@ -0,0 +1,296 @@
|
|||||||
|
--- set of functions for echoing files to things. Uses a slightly hacked up version of f-strings for interpolation/templating
|
||||||
|
-- @module echofile
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2021 Damian Monogue
|
||||||
|
-- @copyright 2016 Hisham Muhammad (https://github.com/hishamhm/f-strings/blob/master/LICENSE)
|
||||||
|
-- @license MIT, see LICENSE.lua
|
||||||
|
local echofile = {}
|
||||||
|
|
||||||
|
-- following functions fiddled with from https://github.com/hishamhm/f-strings/blob/master/F.lua and https://hisham.hm/2016/01/04/string-interpolation-in-lua/
|
||||||
|
-- it seems to work :shrug:
|
||||||
|
local load = load
|
||||||
|
|
||||||
|
if _VERSION == "Lua 5.1" then
|
||||||
|
load = function(code, name, _, env)
|
||||||
|
local fn, err = loadstring(code, name)
|
||||||
|
if fn then
|
||||||
|
setfenv(fn, env)
|
||||||
|
return fn
|
||||||
|
end
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function f(str)
|
||||||
|
local outer_env = _ENV or getfenv(1)
|
||||||
|
return (str:gsub("%b{}", function(block)
|
||||||
|
local code = block:match("{(.*)}")
|
||||||
|
local exp_env = {}
|
||||||
|
setmetatable(exp_env, {
|
||||||
|
__index = function(_, k)
|
||||||
|
local stack_level = 5
|
||||||
|
while debug.getinfo(stack_level, "") ~= nil do
|
||||||
|
local i = 1
|
||||||
|
repeat
|
||||||
|
local name, value = debug.getlocal(stack_level, i)
|
||||||
|
if name == k then
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
i = i + 1
|
||||||
|
until name == nil
|
||||||
|
stack_level = stack_level + 1
|
||||||
|
end
|
||||||
|
return rawget(outer_env, k)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
local fn, err = load("return " .. code, "expression `" .. code .. "`", "t", exp_env)
|
||||||
|
if fn then
|
||||||
|
return tostring(fn())
|
||||||
|
else
|
||||||
|
error(err, 0)
|
||||||
|
end
|
||||||
|
end))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function xechoFile(options)
|
||||||
|
local filename = options.filename
|
||||||
|
local window = options.window
|
||||||
|
local func = options.func
|
||||||
|
local functionName = options.functionName
|
||||||
|
local fntype = type(filename)
|
||||||
|
if fntype ~= "string" then
|
||||||
|
return nil, f("{functionName}: filename as string expected, got {fnType}")
|
||||||
|
end
|
||||||
|
if not io.exists(filename) then
|
||||||
|
return nil, f("{functionName}: {filename} not found")
|
||||||
|
end
|
||||||
|
local file, err = io.open(filename, "r")
|
||||||
|
if not file then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
local lines = file:read("*a")
|
||||||
|
if options.ansi then
|
||||||
|
lines = ansi2decho(lines)
|
||||||
|
end
|
||||||
|
if options.filter then
|
||||||
|
lines = f(lines)
|
||||||
|
end
|
||||||
|
return func(window, lines)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getOptions(etype, filter, window, filename)
|
||||||
|
if filename == nil then
|
||||||
|
filename = window
|
||||||
|
window = "main"
|
||||||
|
end
|
||||||
|
local ansi = false
|
||||||
|
if etype == "a" then
|
||||||
|
etype = 'd'
|
||||||
|
ansi = true
|
||||||
|
end
|
||||||
|
local options = {
|
||||||
|
filename = filename,
|
||||||
|
window = window,
|
||||||
|
func = _G[etype .. "echo"],
|
||||||
|
functionName = etype .. "echoFile([window,] filename)",
|
||||||
|
ansi = ansi,
|
||||||
|
filter = filter,
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Takes a string and performs interpolation
|
||||||
|
--- Uses {} as the delimiter. Expressions will be evaluated
|
||||||
|
---@param str string: The string to interpolate
|
||||||
|
---@usage echofile = require("MDK.echofile")
|
||||||
|
--- echofile.f("{1+1}") -- returns "2"
|
||||||
|
--- local x = 4
|
||||||
|
--- echofile.f"4+3 = {x+3}" -- returns "4+3 = 7"
|
||||||
|
function echofile.f(str)
|
||||||
|
return f(str)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- reads the contents of a file, converts it to decho and then dechos it
|
||||||
|
---@param window string: Optional window to cecho to
|
||||||
|
---@param filename string: Full path to file
|
||||||
|
---@see echofile.f
|
||||||
|
---@see echofile.cechoFile
|
||||||
|
---@usage local ec = require("MDK.echofile")
|
||||||
|
--- local cechoFile,f = ec.cechoFile, ec.f
|
||||||
|
--- cechoFile("C:/path/to/file") -- windows1
|
||||||
|
--- cechoFile("C:\\path\\to\\file") -- windows2
|
||||||
|
--- cechoFile("/path/to/file") -- Linux/MacOS
|
||||||
|
--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
|
||||||
|
function echofile.aechoFile(window, filename)
|
||||||
|
local options = getOptions("a", false, window, filename)
|
||||||
|
return xechoFile(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- reads the contents of a file and then cechos it
|
||||||
|
---@param window string: Optional window to cecho to
|
||||||
|
---@param filename string: Full path to file
|
||||||
|
---@see echofile.f
|
||||||
|
---@see echofile.cechoFilef
|
||||||
|
---@usage local ec = require("MDK.echofile")
|
||||||
|
--- local cechoFile,f = ec.cechoFile, ec.f
|
||||||
|
--- cechoFile("C:/path/to/file") -- windows1
|
||||||
|
--- cechoFile("C:\\path\\to\\file") -- windows2
|
||||||
|
--- cechoFile("/path/to/file") -- Linux/MacOS
|
||||||
|
--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
|
||||||
|
function echofile.aechoFilef(window, filename)
|
||||||
|
local options = getOptions("a", true, window, filename)
|
||||||
|
return xechoFile(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- reads the contents of a file and then cechos it
|
||||||
|
---@param window string: Optional window to cecho to
|
||||||
|
---@param filename string: Full path to file
|
||||||
|
---@see echofile.f
|
||||||
|
---@usage local ec = require("MDK.echofile")
|
||||||
|
--- local cechoFile,f = ec.cechoFile, ec.f
|
||||||
|
--- cechoFile("C:/path/to/file") -- windows1
|
||||||
|
--- cechoFile("C:\\path\\to\\file") -- windows2
|
||||||
|
--- cechoFile("/path/to/file") -- Linux/MacOS
|
||||||
|
--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
|
||||||
|
function echofile.cechoFile(window, filename)
|
||||||
|
local options = getOptions("c", false, window, filename)
|
||||||
|
return xechoFile(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- reads the contents of a file, interpolates it as per echofile.f and then cechos it
|
||||||
|
---@param window string: Optional window to cecho to
|
||||||
|
---@param filename string: Full path to file
|
||||||
|
---@see echofile.f
|
||||||
|
---@usage local ec = require("MDK.echofile")
|
||||||
|
--- local cechoFile,f = ec.cechoFile, ec.f
|
||||||
|
--- cechoFile("C:/path/to/file") -- windows1
|
||||||
|
--- cechoFile("C:\\path\\to\\file") -- windows2
|
||||||
|
--- cechoFile("/path/to/file") -- Linux/MacOS
|
||||||
|
--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
|
||||||
|
function echofile.cechoFilef(window, filename)
|
||||||
|
local options = getOptions("c", true, window, filename)
|
||||||
|
return xechoFile(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- reads the contents of a file and then dechos it
|
||||||
|
---@param window string: Optional window to decho to
|
||||||
|
---@param filename string: Full path to file
|
||||||
|
---@see echofile.f
|
||||||
|
---@see echofile.cechoFile
|
||||||
|
function echofile.dechoFile(window, filename)
|
||||||
|
local options = getOptions("d", false, window, filename)
|
||||||
|
return xechoFile(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- reads the contents of a file, interpolates it as per echofile.f and then dechos it
|
||||||
|
---@param window string: Optional window to decho to
|
||||||
|
---@param filename string: Full path to file
|
||||||
|
---@see echofile.f
|
||||||
|
---@see echofile.cechoFile
|
||||||
|
function echofile.dechoFilef(window, filename)
|
||||||
|
local options = getOptions("d", true, window, filename)
|
||||||
|
return xechoFile(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- reads the contents of a file and then hechos it
|
||||||
|
---@param window string: Optional window to hecho to
|
||||||
|
---@param filename string: Full path to file
|
||||||
|
---@see echofile.f
|
||||||
|
---@see echofile.cechoFile
|
||||||
|
function echofile.hechoFile(window, filename)
|
||||||
|
local options = getOptions("h", false, window, filename)
|
||||||
|
return xechoFile(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- reads the contents of a file, interpolates it as per echofile.f and then hechos it
|
||||||
|
---@param window string: Optional window to hecho to
|
||||||
|
---@param filename string: Full path to file
|
||||||
|
---@see echofile.f
|
||||||
|
---@see echofile.cechoFile
|
||||||
|
function echofile.hechoFilef(window, filename)
|
||||||
|
local options = getOptions("h", true, window, filename)
|
||||||
|
return xechoFile(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- reads the contents of a file, interpolates it as per echofile.f and then echos it
|
||||||
|
---@param window string: Optional window to echo to
|
||||||
|
---@param filename string: Full path to file
|
||||||
|
---@see echofile.f
|
||||||
|
---@see echofile.cechoFile
|
||||||
|
function echofile.echoFile(window, filename)
|
||||||
|
local options = getOptions("", false, window, filename)
|
||||||
|
return xechoFile(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- reads the contents of a file, interpolates it as per echofile.f and then echos it
|
||||||
|
---@param window string: Optional window to echo to
|
||||||
|
---@param filename string: Full path to file
|
||||||
|
---@see echofile.f
|
||||||
|
---@see echofile.cechoFile
|
||||||
|
function echofile.echoFilef(window, filename)
|
||||||
|
local options = getOptions("", true, window, filename)
|
||||||
|
return xechoFile(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Adds c/d/h/echoFile functions to Geyser miniconsole and userwindow objects
|
||||||
|
---@usage require("MDK.echofile").patchGeyser()
|
||||||
|
--- myMC = Geyser.MiniConsole:new({name = "myMC"})
|
||||||
|
--- myMC:cechoFile(f"{getMudletHomeDir()}/helpfile")
|
||||||
|
function echofile.patchGeyser()
|
||||||
|
if Geyser.MiniConsole.echoFile then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
function Geyser.MiniConsole:echoFile(filename)
|
||||||
|
return echofile.echoFile(self.name, filename)
|
||||||
|
end
|
||||||
|
function Geyser.MiniConsole:echoFilef(filename)
|
||||||
|
return echofile.echoFilef(self.name, filename)
|
||||||
|
end
|
||||||
|
function Geyser.MiniConsole:aechoFile(filename)
|
||||||
|
return echofile.aechoFile(self.name, filename)
|
||||||
|
end
|
||||||
|
function Geyser.MiniConsole:aechoFilef(filename)
|
||||||
|
return echofile.aechoFilef(self.name, filename)
|
||||||
|
end
|
||||||
|
function Geyser.MiniConsole:cechoFile(filename)
|
||||||
|
return echofile.cechoFile(self.name, filename)
|
||||||
|
end
|
||||||
|
function Geyser.MiniConsole:cechoFilef(filename)
|
||||||
|
return echofile.cechoFilef(self.name, filename)
|
||||||
|
end
|
||||||
|
function Geyser.MiniConsole:dechoFile(filename)
|
||||||
|
return echofile.dechoFile(self.name, filename)
|
||||||
|
end
|
||||||
|
function Geyser.MiniConsole:dechoFilef(filename)
|
||||||
|
return echofile.dechoFilef(self.name, filename)
|
||||||
|
end
|
||||||
|
function Geyser.MiniConsole:hechoFile(filename)
|
||||||
|
return echofile.hechoFile(self.name, filename)
|
||||||
|
end
|
||||||
|
function Geyser.MiniConsole:hechoFilef(filename)
|
||||||
|
return echofile.hechoFilef(self.name, filename)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Installs c/d/h/echoFile and f to the global namespace, and adds functions to Geyser
|
||||||
|
---@usage require("MDK.echofile").installGlobal()
|
||||||
|
--- f"{1+2}" -- returns "2"
|
||||||
|
--- dechoFile(f"{getMudletHomeDir()}/fileWithDechoLines.txt")
|
||||||
|
--- -- reads contents of fileWithDechoLines.txt from profile directory
|
||||||
|
--- -- and dechos them to the main console
|
||||||
|
function echofile.installGlobal()
|
||||||
|
_G.f = f
|
||||||
|
_G.echoFile = echofile.echoFile
|
||||||
|
_G.echoFilef = echofile.echoFilef
|
||||||
|
_G.aechoFile = echofile.aechoFile
|
||||||
|
_G.aechoFilef = echofile.aechoFilef
|
||||||
|
_G.cechoFile = echofile.cechoFile
|
||||||
|
_G.cechoFilef = echofile.cechoFilef
|
||||||
|
_G.dechoFile = echofile.dechoFile
|
||||||
|
_G.dechoFilef = echofile.dechoFilef
|
||||||
|
_G.hechoFile = echofile.hechoFile
|
||||||
|
_G.hechoFilef = echofile.hechoFilef
|
||||||
|
echofile.patchGeyser()
|
||||||
|
end
|
||||||
|
|
||||||
|
return echofile
|
2353
src/resources/MDK/emco.lua
Executable file
2353
src/resources/MDK/emco.lua
Executable file
File diff suppressed because it is too large
Load Diff
1697
src/resources/MDK/ftext.lua
Executable file
1697
src/resources/MDK/ftext.lua
Executable file
File diff suppressed because it is too large
Load Diff
447
src/resources/MDK/ftext_spec.lua
Executable file
447
src/resources/MDK/ftext_spec.lua
Executable file
@ -0,0 +1,447 @@
|
|||||||
|
local ftext = require("MDK.ftext")
|
||||||
|
|
||||||
|
describe("ftext:", function()
|
||||||
|
describe("ftext.fText:", function()
|
||||||
|
local fText = ftext.fText
|
||||||
|
|
||||||
|
it("Should properly center text", function()
|
||||||
|
local expected = " some text "
|
||||||
|
local actual = fText("some text", {width = 20})
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(20, actual:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should properly pad left aligned text", function()
|
||||||
|
local expected = "some text "
|
||||||
|
local actual = fText("some text", {width = 20, alignment = "left"})
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(20, actual:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should properly pad right aligned text", function()
|
||||||
|
local expected = " some text"
|
||||||
|
local actual = fText("some text", {width = 20, alignment = "right"})
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(20, actual:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should wrap lines to the correct length", function()
|
||||||
|
local str = "This is a test of the emergency broadcast system. This is only a test"
|
||||||
|
local options = {width = 10, alignment = "centered"}
|
||||||
|
local actual = fText(str, options)
|
||||||
|
for _, line in ipairs(actual:split("\n")) do
|
||||||
|
assert.equals(line:len(), 10)
|
||||||
|
end
|
||||||
|
options.width = 15
|
||||||
|
actual = fText(str, options)
|
||||||
|
for _, line in ipairs(actual:split("\n")) do
|
||||||
|
assert.equals(line:len(), 15)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("non-space spacer character:", function()
|
||||||
|
local str = "some text"
|
||||||
|
local options = {width = "20", alignment = "left", spacer = "="}
|
||||||
|
it("Should work with left align", function()
|
||||||
|
local expected = "some text =========="
|
||||||
|
local actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(20, actual:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should work with right align", function()
|
||||||
|
local expected = "========== some text"
|
||||||
|
options.alignment = "right"
|
||||||
|
local actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(20, actual:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should work with center align", function()
|
||||||
|
local expected = ("==== some text =====")
|
||||||
|
options.alignment = "center"
|
||||||
|
local actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(20, actual:len())
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("nogap option:", function()
|
||||||
|
local str = "some text"
|
||||||
|
local options = {width = "20", alignment = "left", spacer = "=", nogap = true}
|
||||||
|
|
||||||
|
it("Should work with left align", function()
|
||||||
|
local expected = "some text==========="
|
||||||
|
local actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(20, actual:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should work with right align", function()
|
||||||
|
local expected = "===========some text"
|
||||||
|
options.alignment = "right"
|
||||||
|
local actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(20, actual:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should work with center align", function()
|
||||||
|
local expected = "=====some text======"
|
||||||
|
options.alignment = "center"
|
||||||
|
local actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(20, actual:len())
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("cap functionality", function()
|
||||||
|
local str = "some text"
|
||||||
|
local options = {width = 20, spacer = "=", cap = "|"}
|
||||||
|
|
||||||
|
it("Should place the spacer outside the cap by default", function()
|
||||||
|
local expected = "===| some text |===="
|
||||||
|
local actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(20, actual:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should place it inside the cap if inside option is true", function()
|
||||||
|
local expected = "|=== some text ====|"
|
||||||
|
options.inside = true
|
||||||
|
local actual = fText(str, options)
|
||||||
|
options.inside = nil
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(20, actual:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should mirror certain characters with their opposites", function()
|
||||||
|
local expected = "===[ some text ]===="
|
||||||
|
options.mirror = true
|
||||||
|
options.cap = "["
|
||||||
|
local actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
options.inside = true
|
||||||
|
expected = "[=== some text ====]"
|
||||||
|
actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
options.inside = nil
|
||||||
|
options.cap = "<"
|
||||||
|
expected = "===< some text >===="
|
||||||
|
actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
options.cap = "{"
|
||||||
|
expected = "==={ some text }===="
|
||||||
|
actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
options.cap = "("
|
||||||
|
expected = "===( some text )===="
|
||||||
|
actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
options.cap = "|"
|
||||||
|
expected = "===| some text |===="
|
||||||
|
actual = fText(str, options)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("ftext.cfText", function()
|
||||||
|
local cfText = ftext.cfText
|
||||||
|
local str = "some text"
|
||||||
|
local options = {
|
||||||
|
width = 20,
|
||||||
|
spacer = "=",
|
||||||
|
cap = "[",
|
||||||
|
inside = true,
|
||||||
|
mirror = true,
|
||||||
|
capColor = "<purple>",
|
||||||
|
spacerColor = "<green>",
|
||||||
|
textColor = "<red>",
|
||||||
|
}
|
||||||
|
it("Should handle cecho colored text", function()
|
||||||
|
local expectedStripped = "[=== some text ====]"
|
||||||
|
local expected = "<purple>[<reset><green>===<reset><red> some text <reset><green>====<reset><purple>]<reset>"
|
||||||
|
local actual = cfText(str, options)
|
||||||
|
local actualStripped = cecho2string(actual)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(expectedStripped, actualStripped)
|
||||||
|
assert.equals(20, actualStripped:len())
|
||||||
|
expectedStripped = "===[ some text ]===="
|
||||||
|
expected = "<green>===<reset><purple>[<reset><red> some text <reset><purple>]<reset><green>====<reset>"
|
||||||
|
options.inside = false
|
||||||
|
actual = cfText(str, options)
|
||||||
|
actualStripped = cecho2string(actual)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(expectedStripped, actualStripped)
|
||||||
|
assert.equals(20, actualStripped:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should wrap cecho lines to the correct length", function()
|
||||||
|
local str = "This is a test of the emergency broadcast system. This is only a test"
|
||||||
|
local options = {width = 10, alignment = "centered"}
|
||||||
|
local actual = cfText(str, options)
|
||||||
|
for _, line in ipairs(actual:split("\n")) do
|
||||||
|
assert.equals(cecho2string(line):len(), 10)
|
||||||
|
end
|
||||||
|
options.width = 15
|
||||||
|
actual = cfText(str, options)
|
||||||
|
for _, line in ipairs(actual:split("\n")) do
|
||||||
|
assert.equals(cecho2string(line):len(), 15)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("ftext.dfText", function()
|
||||||
|
local dfText = ftext.dfText
|
||||||
|
local str = "some text"
|
||||||
|
local options = {
|
||||||
|
width = 20,
|
||||||
|
spacer = "=",
|
||||||
|
cap = "[",
|
||||||
|
inside = true,
|
||||||
|
mirror = true,
|
||||||
|
capColor = "<160,32,240>",
|
||||||
|
spacerColor = "<0,255,0>",
|
||||||
|
textColor = "<255,0,0>",
|
||||||
|
}
|
||||||
|
it("Should handle decho colored text", function()
|
||||||
|
local expectedStripped = "[=== some text ====]"
|
||||||
|
local expected = "<160,32,240>[<r><0,255,0>===<r><255,0,0> some text <r><0,255,0>====<r><160,32,240>]<r>"
|
||||||
|
local actual = dfText(str, options)
|
||||||
|
local actualStripped = decho2string(actual)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(expectedStripped, actualStripped)
|
||||||
|
assert.equals(20, actualStripped:len())
|
||||||
|
expectedStripped = "===[ some text ]===="
|
||||||
|
expected = "<0,255,0>===<r><160,32,240>[<r><255,0,0> some text <r><160,32,240>]<r><0,255,0>====<r>"
|
||||||
|
options.inside = false
|
||||||
|
actual = dfText(str, options)
|
||||||
|
actualStripped = decho2string(actual)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(expectedStripped, actualStripped)
|
||||||
|
assert.equals(20, actualStripped:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should wrap decho lines to the correct length", function()
|
||||||
|
local str = "This is a test of the emergency broadcast system. This is only a test"
|
||||||
|
local options = {width = 10, alignment = "centered"}
|
||||||
|
local actual = dfText(str, options)
|
||||||
|
for _, line in ipairs(actual:split("\n")) do
|
||||||
|
assert.equals(decho2string(line):len(), 10)
|
||||||
|
end
|
||||||
|
options.width = 15
|
||||||
|
actual = dfText(str, options)
|
||||||
|
for _, line in ipairs(actual:split("\n")) do
|
||||||
|
assert.equals(decho2string(line):len(), 15)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("ftext.hfText", function()
|
||||||
|
local hfText = ftext.hfText
|
||||||
|
local str = "some text"
|
||||||
|
local options = {
|
||||||
|
width = 20,
|
||||||
|
spacer = "=",
|
||||||
|
cap = "[",
|
||||||
|
inside = true,
|
||||||
|
mirror = true,
|
||||||
|
capColor = "#a020f0",
|
||||||
|
spacerColor = "#00ff00",
|
||||||
|
textColor = "#ff0000",
|
||||||
|
}
|
||||||
|
it("Should handle hecho colored text", function()
|
||||||
|
local expectedStripped = "[=== some text ====]"
|
||||||
|
local expected = "#a020f0[#r#00ff00===#r#ff0000 some text #r#00ff00====#r#a020f0]#r"
|
||||||
|
local actual = hfText(str, options)
|
||||||
|
local actualStripped = hecho2string(actual)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(expectedStripped, actualStripped)
|
||||||
|
assert.equals(20, actualStripped:len())
|
||||||
|
expectedStripped = "===[ some text ]===="
|
||||||
|
expected = "#00ff00===#r#a020f0[#r#ff0000 some text #r#a020f0]#r#00ff00====#r"
|
||||||
|
options.inside = false
|
||||||
|
actual = hfText(str, options)
|
||||||
|
actualStripped = hecho2string(actual)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(expectedStripped, actualStripped)
|
||||||
|
assert.equals(20, actualStripped:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should wrap hecho lines to the correct length", function()
|
||||||
|
local str = "This is a test of the emergency broadcast system. This is only a test"
|
||||||
|
local options = {width = 10, alignment = "centered"}
|
||||||
|
local actual = hfText(str, options)
|
||||||
|
for _, line in ipairs(actual:split("\n")) do
|
||||||
|
assert.equals(hecho2string(line):len(), 10)
|
||||||
|
end
|
||||||
|
options.width = 15
|
||||||
|
actual = hfText(str, options)
|
||||||
|
for _, line in ipairs(actual:split("\n")) do
|
||||||
|
assert.equals(hecho2string(line):len(), 15)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("ftext.TextFormatter", function()
|
||||||
|
local tf = ftext.TextFormatter
|
||||||
|
local str = "some text"
|
||||||
|
local formatter
|
||||||
|
|
||||||
|
before_each(function()
|
||||||
|
formatter = tf:new({width = 20})
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should let you change width using :setWidth", function()
|
||||||
|
formatter:setWidth(80)
|
||||||
|
local expected =
|
||||||
|
"<white><reset><white> <reset><white> some text <reset><white> <reset><white><reset>"
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(80, cecho2string(actual):len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should format for cecho by default", function()
|
||||||
|
local expected = "<white><reset><white> <reset><white> some text <reset><white> <reset><white><reset>"
|
||||||
|
local expectedStripped = " some text "
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
local actualStripped = cecho2string(actual)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
assert.equals(expectedStripped, actualStripped)
|
||||||
|
assert.equals(20, actualStripped:len())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should produce the same line as cfText given the same options", function()
|
||||||
|
local expected = ftext.cfText(str, formatter.options)
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should let you change type using :setType", function()
|
||||||
|
formatter:setType("h")
|
||||||
|
local expected = ftext.hfText(str, formatter.options)
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
formatter:setType("d")
|
||||||
|
expected = ftext.dfText(str, formatter.options)
|
||||||
|
actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
formatter:setType("")
|
||||||
|
expected = ftext.fText(str, formatter.options)
|
||||||
|
actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should default to word wrapping, and let you change it with :setWrap", function()
|
||||||
|
formatter:setWidth(10)
|
||||||
|
local expected =
|
||||||
|
"<white><reset><white> <reset><white> some <reset><white> <reset><white><reset>\n<white><reset><white> <reset><white> text <reset><white> <reset><white><reset>"
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
expected = "<white><reset><white><reset><white> some text <reset><white><reset><white><reset>"
|
||||||
|
formatter:setWrap(false)
|
||||||
|
actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should allow you to change the cap using :setCap", function()
|
||||||
|
formatter:setCap('|')
|
||||||
|
local expected = "<white>|<reset><white> <reset><white> some text <reset><white> <reset><white>|<reset>"
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should allow you to change the capColor using :setCapColor", function()
|
||||||
|
formatter:setCapColor('<red>')
|
||||||
|
local expected = "<red><reset><white> <reset><white> some text <reset><white> <reset><red><reset>"
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should allow you to change the spacer color using :setSpacerColor", function()
|
||||||
|
formatter:setSpacerColor("<red>")
|
||||||
|
local expected = "<white><reset><red> <reset><white> some text <reset><red> <reset><white><reset>"
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should allow you to change the text color using :setTextColor", function()
|
||||||
|
formatter:setTextColor("<red>")
|
||||||
|
local expected = "<white><reset><white> <reset><red> some text <reset><white> <reset><white><reset>"
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should allow you to change the spacer using :setSpacer", function()
|
||||||
|
formatter:setSpacer("=")
|
||||||
|
-- local expected = "<white><reset><white> <reset><white> some text <reset><white> <reset><white><reset>"
|
||||||
|
local expected = "<white><reset><white>====<reset><white> some text <reset><white>=====<reset><white><reset>"
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should allow you to set the alignment using :setAlignment", function()
|
||||||
|
formatter:setAlignment("left")
|
||||||
|
local expected = "<white><reset><white><reset><white>some text <reset><white> <reset><white><reset>"
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
formatter:setAlignment("right")
|
||||||
|
expected = "<white><reset><white> <reset><white> some text<reset><white><reset><white><reset>"
|
||||||
|
actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should allow you to change the 'inside' option using :setInside", function()
|
||||||
|
formatter:setInside(false)
|
||||||
|
local expected = "<white> <reset><white><reset><white> some text <reset><white><reset><white> <reset>"
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should allow you to change the mirror option using :setMirror", function()
|
||||||
|
formatter:setCap('<')
|
||||||
|
formatter:setMirror(true)
|
||||||
|
local expected = "<white><<reset><white> <reset><white> some text <reset><white> <reset><white>><reset>"
|
||||||
|
local actual = formatter:format(str)
|
||||||
|
assert.equal(expected, actual)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("ftext.TableMaker", function()
|
||||||
|
local TableMaker = ftext.TableMaker
|
||||||
|
local tm
|
||||||
|
before_each(function()
|
||||||
|
tm = TableMaker:new()
|
||||||
|
tm:addColumn({name = "col1", width = 15, textColor = "<red>"})
|
||||||
|
tm:addColumn({name = "col2", width = 15, textColor = "<blue>"})
|
||||||
|
tm:addColumn({name = "col3", width = 15, textColor = "<green>"})
|
||||||
|
tm:addRow({"some text", "more text", "other text"})
|
||||||
|
tm:addRow({"little text", "bigger text", "text"})
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("Should assemble a formatted table given default options", function()
|
||||||
|
local expected = [[<white>*************************************************<reset>
|
||||||
|
<white>*<reset><white><reset><white> <reset><red> col1 <reset><white> <reset><white><reset><white>|<reset><white><reset><white> <reset><blue> col2 <reset><white> <reset><white><reset><white>|<reset><white><reset><white> <reset><green> col3 <reset><white> <reset><white><reset><white>*<reset>
|
||||||
|
<white>*<reset><white>---------------<reset><white>|<reset><white>---------------<reset><white>|<reset><white>---------------<reset><white>*<reset>
|
||||||
|
<white>*<reset><white><reset><white> <reset><red> some text <reset><white> <reset><white><reset><white>|<reset><white><reset><white> <reset><blue> more text <reset><white> <reset><white><reset><white>|<reset><white><reset><white> <reset><green> other text <reset><white> <reset><white><reset><white>*<reset>
|
||||||
|
<white>*<reset><white>---------------<reset><white>|<reset><white>---------------<reset><white>|<reset><white>---------------<reset><white>*<reset>
|
||||||
|
<white>*<reset><white><reset><white> <reset><red> little text <reset><white> <reset><white><reset><white>|<reset><white><reset><white> <reset><blue> bigger text <reset><white> <reset><white><reset><white>|<reset><white><reset><white> <reset><green> text <reset><white> <reset><white><reset><white>*<reset>
|
||||||
|
<white>*************************************************<reset>
|
||||||
|
]]
|
||||||
|
local actual = tm:assemble()
|
||||||
|
assert.equals(expected, actual)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("TableMaker:getCell should return the text and formatter for a specific cell", function()
|
||||||
|
local expectedText = "more text"
|
||||||
|
local expectedFormatter = tm.columns[2]
|
||||||
|
local actualText, actualFormatter = tm:getCell(1, 2)
|
||||||
|
assert.equals(expectedText, actualText)
|
||||||
|
assert.equals(expectedFormatter, actualFormatter)
|
||||||
|
local expectedFormatted = "<white><reset><white> <reset><blue> more text <reset><white> <reset><white><reset>"
|
||||||
|
local actualFormatted = actualFormatter:format(actualText)
|
||||||
|
assert.equals(expectedFormatted, actualFormatted)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
342
src/resources/MDK/gradientmaker.lua
Executable file
342
src/resources/MDK/gradientmaker.lua
Executable file
@ -0,0 +1,342 @@
|
|||||||
|
--- Module which provides for creating color gradients for your text.
|
||||||
|
-- Original functions found on <a href="https://forums.lusternia.com/discussion/3261/anyone-want-text-gradients">the Lusternia Forums</a>
|
||||||
|
-- <br> I added functions to work with hecho.
|
||||||
|
-- <br> I also made performance enhancements by storing already calculated gradients after first use for the session and only including the colorcode in the returned string if the color changed.
|
||||||
|
-- @module GradientMaker
|
||||||
|
-- @author Sylphas on the Lusternia forums
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2018 Sylphas
|
||||||
|
-- @copyright 2020 Damian Monogue
|
||||||
|
local GradientMaker = {}
|
||||||
|
local gradient_table = {}
|
||||||
|
|
||||||
|
local function _clamp(num1, num2, num3)
|
||||||
|
local smaller = math.min(num2, num3)
|
||||||
|
local larger = math.max(num2, num3)
|
||||||
|
local minimum = math.max(0, smaller)
|
||||||
|
local maximum = math.min(255, larger)
|
||||||
|
return math.min(maximum, math.max(minimum, num1))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _gradient(length, rgb1, rgb2)
|
||||||
|
assert(length > 0)
|
||||||
|
if length == 1 then
|
||||||
|
return {rgb1}
|
||||||
|
elseif length == 2 then
|
||||||
|
return {rgb1, rgb2}
|
||||||
|
else
|
||||||
|
local step = {}
|
||||||
|
for color = 1, 3 do
|
||||||
|
step[color] = (rgb2[color] - rgb1[color]) / (length - 2)
|
||||||
|
end
|
||||||
|
local gradient = {rgb1}
|
||||||
|
for iter = 1, length - 2 do
|
||||||
|
gradient[iter + 1] = {}
|
||||||
|
for color = 1, 3 do
|
||||||
|
gradient[iter + 1][color] = math.ceil(rgb1[color] + (iter * step[color]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
gradient[length] = rgb2
|
||||||
|
for index, color in ipairs(gradient) do
|
||||||
|
for iter = 1, 3 do
|
||||||
|
gradient[index][iter] = _clamp(color[iter], rgb1[iter], rgb2[iter])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return gradient
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gradient_to_string(gradient)
|
||||||
|
local gradstring = ""
|
||||||
|
for _, grad in ipairs(gradient) do
|
||||||
|
local nodestring = ""
|
||||||
|
for _, col in ipairs(grad) do
|
||||||
|
nodestring = string.format("%s%03d", nodestring, col)
|
||||||
|
end
|
||||||
|
if _ == 1 then
|
||||||
|
gradstring = nodestring
|
||||||
|
else
|
||||||
|
gradstring = gradstring .. "|" .. nodestring
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return gradstring
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _gradients(length, ...)
|
||||||
|
local arg = {...}
|
||||||
|
local argkey = gradient_to_string(arg)
|
||||||
|
local gradients_for_length = gradient_table[length]
|
||||||
|
if not gradients_for_length then
|
||||||
|
gradient_table[length] = {}
|
||||||
|
gradients_for_length = gradient_table[length]
|
||||||
|
end
|
||||||
|
local grads = gradients_for_length[argkey]
|
||||||
|
if grads then
|
||||||
|
return grads
|
||||||
|
end
|
||||||
|
if #arg == 0 then
|
||||||
|
gradients_for_length[argkey] = {}
|
||||||
|
return {}
|
||||||
|
elseif #arg == 1 then
|
||||||
|
gradients_for_length[argkey] = arg[1]
|
||||||
|
return arg[1]
|
||||||
|
elseif #arg == 2 then
|
||||||
|
gradients_for_length[argkey] = _gradient(length, arg[1], arg[2])
|
||||||
|
return gradients_for_length[argkey]
|
||||||
|
else
|
||||||
|
local quotient = math.floor(length / (#arg - 1))
|
||||||
|
local remainder = length % (#arg - 1)
|
||||||
|
local gradients = {}
|
||||||
|
for section = 1, #arg - 1 do
|
||||||
|
local slength = quotient
|
||||||
|
if section <= remainder then
|
||||||
|
slength = slength + 1
|
||||||
|
end
|
||||||
|
local gradient = _gradient(slength, arg[section], arg[section + 1])
|
||||||
|
for _, rgb in ipairs(gradient) do
|
||||||
|
table.insert(gradients, rgb)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
gradients_for_length[argkey] = gradients
|
||||||
|
return gradients
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _color_name(rgb)
|
||||||
|
local least_distance = math.huge
|
||||||
|
local cname = ""
|
||||||
|
for name, color in pairs(color_table) do
|
||||||
|
local color_distance = math.sqrt((color[1] - rgb[1]) ^ 2 + (color[2] - rgb[2]) ^ 2 + (color[3] - rgb[3]) ^ 2)
|
||||||
|
if color_distance < least_distance then
|
||||||
|
least_distance = color_distance
|
||||||
|
cname = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return cname
|
||||||
|
end
|
||||||
|
|
||||||
|
local function errorIfEmpty(text, funcName)
|
||||||
|
assert(#text > 0, string.format("%s: you passed in an empty string, and I cannot make a gradient out of an empty string", funcName))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dgradient_table(text, ...)
|
||||||
|
errorIfEmpty(text, "dgradient_table")
|
||||||
|
local gradients = _gradients(#text, ...)
|
||||||
|
local dgrads = {}
|
||||||
|
for character = 1, #text do
|
||||||
|
table.insert(dgrads, {gradients[character], text:sub(character, character)})
|
||||||
|
end
|
||||||
|
return dgrads
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dgradient(text, ...)
|
||||||
|
errorIfEmpty(text, "dgradient")
|
||||||
|
local gradients = _gradients(#text, ...)
|
||||||
|
local dgrad = ""
|
||||||
|
local current_color = ""
|
||||||
|
for character = 1, #text do
|
||||||
|
local new_color = "<" .. table.concat(gradients[character], ",") .. ">"
|
||||||
|
local char = text:sub(character, character)
|
||||||
|
if new_color == current_color then
|
||||||
|
dgrad = dgrad .. char
|
||||||
|
else
|
||||||
|
dgrad = dgrad .. new_color .. char
|
||||||
|
current_color = new_color
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return dgrad
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cgradient_table(text, ...)
|
||||||
|
errorIfEmpty(text, "cgradient_table")
|
||||||
|
local gradients = _gradients(#text, ...)
|
||||||
|
local cgrads = {}
|
||||||
|
for character = 1, #text do
|
||||||
|
table.insert(cgrads, {_color_name(gradients[character]), text:sub(character, character)})
|
||||||
|
end
|
||||||
|
return cgrads
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cgradient(text, ...)
|
||||||
|
errorIfEmpty(text, "cgradient")
|
||||||
|
local gradients = _gradients(#text, ...)
|
||||||
|
local cgrad = ""
|
||||||
|
local current_color = ""
|
||||||
|
for character = 1, #text do
|
||||||
|
local new_color = "<" .. _color_name(gradients[character]) .. ">"
|
||||||
|
local char = text:sub(character, character)
|
||||||
|
if new_color == current_color then
|
||||||
|
cgrad = cgrad .. char
|
||||||
|
else
|
||||||
|
cgrad = cgrad .. new_color .. char
|
||||||
|
current_color = new_color
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return cgrad
|
||||||
|
end
|
||||||
|
|
||||||
|
local hex = Geyser.Color.hex
|
||||||
|
|
||||||
|
local function hgradient_table(text, ...)
|
||||||
|
errorIfEmpty(text, "hgradient_table")
|
||||||
|
local grads = _gradients(#text, ...)
|
||||||
|
local hgrads = {}
|
||||||
|
for character = 1, #text do
|
||||||
|
table.insert(hgrads, {hex(unpack(grads[character])):sub(2, -1), text:sub(character, character)})
|
||||||
|
end
|
||||||
|
return hgrads
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hgradient(text, ...)
|
||||||
|
errorIfEmpty(text, "hgradient")
|
||||||
|
local grads = _gradients(#text, ...)
|
||||||
|
local hgrads = ""
|
||||||
|
local current_color = ""
|
||||||
|
for character = 1, #text do
|
||||||
|
local new_color = hex(unpack(grads[character]))
|
||||||
|
local char = text:sub(character, character)
|
||||||
|
if new_color == current_color then
|
||||||
|
hgrads = hgrads .. char
|
||||||
|
else
|
||||||
|
hgrads = hgrads .. new_color .. char
|
||||||
|
current_color = new_color
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return hgrads
|
||||||
|
end
|
||||||
|
|
||||||
|
local function color_name(...)
|
||||||
|
local arg = {...}
|
||||||
|
if #arg == 1 then
|
||||||
|
return _color_name(arg[1])
|
||||||
|
elseif #arg == 3 then
|
||||||
|
return _color_name(arg)
|
||||||
|
else
|
||||||
|
local errmsg =
|
||||||
|
"color_name: You must pass either a table of r,g,b values: color_name({r,g,b})\nor the three r,g,b values separately: color_name(r,g,b)"
|
||||||
|
error(errmsg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the closest color name to a given r,g,b color
|
||||||
|
-- @param r The red component. Can also pass the full color as a table, IE { 255, 0, 0 }
|
||||||
|
-- @param g The green component. If you pass the color as a table as noted above, this param should be empty
|
||||||
|
-- @param b the blue components. If you pass the color as a table as noted above, this param should be empty
|
||||||
|
-- @usage
|
||||||
|
-- closest_color = GradientMaker.color_name(128,200,30) -- returns "ansi_149"
|
||||||
|
-- closest_color = GradientMaker.color_name({128, 200, 30}) -- this is functionally equivalent to the first one
|
||||||
|
function GradientMaker.color_name(...)
|
||||||
|
return color_name(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the text, with the defined color gradients applied and formatted for us with decho. Usage example below produces the following text
|
||||||
|
-- <br><img src="https://demonnic.github.io/mdk/images/dechogradient.png" alt="dgradient example">
|
||||||
|
-- @tparam string text The text you want to apply the color gradients to
|
||||||
|
-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
|
||||||
|
-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
|
||||||
|
-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
|
||||||
|
-- @see cgradient
|
||||||
|
-- @see hgradient
|
||||||
|
-- @usage
|
||||||
|
-- decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255}))
|
||||||
|
-- decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255}))
|
||||||
|
-- decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50}))
|
||||||
|
function GradientMaker.dgradient(text, ...)
|
||||||
|
return dgradient(text, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the text, with the defined color gradients applied and formatted for us with cecho. Usage example below produces the following text
|
||||||
|
-- <br><img src="https://demonnic.github.io/mdk/images/cechogradient.png" alt="cgradient example">
|
||||||
|
-- @tparam string text The text you want to apply the color gradients to
|
||||||
|
-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
|
||||||
|
-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
|
||||||
|
-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
|
||||||
|
-- @see dgradient
|
||||||
|
-- @see hgradient
|
||||||
|
-- @usage
|
||||||
|
-- cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255}))
|
||||||
|
-- cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255}))
|
||||||
|
-- cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50}))
|
||||||
|
function GradientMaker.cgradient(text, ...)
|
||||||
|
return cgradient(text, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the text, with the defined color gradients applied and formatted for us with hecho. Usage example below produces the following text
|
||||||
|
-- <br><img src="https://demonnic.github.io/mdk/images/hechogradient.png" alt="hgradient example">
|
||||||
|
-- @tparam string text The text you want to apply the color gradients to
|
||||||
|
-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
|
||||||
|
-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
|
||||||
|
-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
|
||||||
|
-- @see cgradient
|
||||||
|
-- @see dgradient
|
||||||
|
-- @usage
|
||||||
|
-- hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255}))
|
||||||
|
-- hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255}))
|
||||||
|
-- hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50}))
|
||||||
|
function GradientMaker.hgradient(text, ...)
|
||||||
|
return hgradient(text, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns a table, each element of which is a table, the first element of which is the color name to use and the character which should be that color
|
||||||
|
-- @tparam string text The text you want to apply the color gradients to
|
||||||
|
-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
|
||||||
|
-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
|
||||||
|
-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
|
||||||
|
-- @see cgradient
|
||||||
|
function GradientMaker.cgradient_table(text, ...)
|
||||||
|
return cgradient_table(text, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns a table, each element of which is a table, the first element of which is the color({r,g,b} format) to use and the character which should be that color
|
||||||
|
-- @tparam string text The text you want to apply the color gradients to
|
||||||
|
-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
|
||||||
|
-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
|
||||||
|
-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
|
||||||
|
-- @see dgradient
|
||||||
|
function GradientMaker.dgradient_table(text, ...)
|
||||||
|
return dgradient_table(text, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns a table, each element of which is a table, the first element of which is the color(in hex) to use and the second element of which is the character which should be that color
|
||||||
|
-- @tparam string text The text you want to apply the color gradients to
|
||||||
|
-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
|
||||||
|
-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
|
||||||
|
-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
|
||||||
|
-- @see hgradient
|
||||||
|
function GradientMaker.hgradient_table(text, ...)
|
||||||
|
return hgradient_table(text, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Creates global copies of the c/d/hgradient(_table) functions and color_name for use without accessing the module table
|
||||||
|
-- @usage
|
||||||
|
-- GradientMaker.install_global()
|
||||||
|
-- cecho(cgradient(...)) -- use cgradient directly now
|
||||||
|
function GradientMaker.install_global()
|
||||||
|
_G["hgradient"] = function(...)
|
||||||
|
return hgradient(...)
|
||||||
|
end
|
||||||
|
_G["dgradient"] = function(...)
|
||||||
|
return dgradient(...)
|
||||||
|
end
|
||||||
|
_G["cgradient"] = function(...)
|
||||||
|
return cgradient(...)
|
||||||
|
end
|
||||||
|
_G["hgradient_table"] = function(...)
|
||||||
|
return hgradient_table(...)
|
||||||
|
end
|
||||||
|
_G["dgradient_table"] = function(...)
|
||||||
|
return dgradient_table(...)
|
||||||
|
end
|
||||||
|
_G["cgradient_table"] = function(...)
|
||||||
|
return cgradient_table(...)
|
||||||
|
end
|
||||||
|
_G["color_name"] = function(...)
|
||||||
|
return color_name(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- function GradientMaker.getGrads()
|
||||||
|
-- return gradient_table
|
||||||
|
-- end
|
||||||
|
|
||||||
|
return GradientMaker
|
461
src/resources/MDK/loggingconsole.lua
Executable file
461
src/resources/MDK/loggingconsole.lua
Executable file
@ -0,0 +1,461 @@
|
|||||||
|
--- MiniConsole with logging capabilities
|
||||||
|
-- @classmod LoggingConsole
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2020 Damian Monogue
|
||||||
|
-- @license MIT, see LICENSE.lua
|
||||||
|
local homedir = getMudletHomeDir():gsub("\\", "/")
|
||||||
|
local pathOfThisFile = (...):match("(.-)[^%.]+$")
|
||||||
|
local dt = require(pathOfThisFile .. "demontools")
|
||||||
|
local exists, htmlHeader, htmlHeaderPattern = dt.exists, dt.htmlHeader, dt.htmlHeaderPattern
|
||||||
|
|
||||||
|
local LoggingConsole = {log = true, logFormat = "h", path = "|h/log/consoleLogs/|y/|m/|d/", fileName = "|n.|e"}
|
||||||
|
|
||||||
|
--- Creates and returns a new LoggingConsole.
|
||||||
|
-- @param cons table of constraints. Includes all the valid Geyser.MiniConsole constraints, plus
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <thead>
|
||||||
|
-- <tr>
|
||||||
|
-- <th>option name</th>
|
||||||
|
-- <th>description</th>
|
||||||
|
-- <th>default</th>
|
||||||
|
-- </tr>
|
||||||
|
-- </thead>
|
||||||
|
-- <tbody>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">log</td>
|
||||||
|
-- <td class="tg-1">Should the miniconsole be logging?</td>
|
||||||
|
-- <td class="tg-1">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">logFormat</td>
|
||||||
|
-- <td class="tg-2">"h" for html, "t" for plaintext, "l" for log (with ansi)</td>
|
||||||
|
-- <td class="tg-2">h</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">path</td>
|
||||||
|
-- <td class="tg-1">The path the file lives in. It is templated.<br>|h is replaced by the profile homedir.<br>|y by 4 digit year.<br>|m by 2 digit month<br>|d by 2 digit day<br>|n by the name constraint<br>|e by the file extension (html for h logType, log for others)</td>
|
||||||
|
-- <td class="tg-1">"|h/log/consoleLogs/|y/|m/|d/"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">fileName</td>
|
||||||
|
-- <td class="tg-2">The name of the log file. It is templated, same as path above</td>
|
||||||
|
-- <td class="tg-2">"|n.|e"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- </tbody>
|
||||||
|
-- </table>
|
||||||
|
-- @param container the container for the console
|
||||||
|
-- @usage
|
||||||
|
-- local LoggingConsole = require("MDK.loggingconsole")
|
||||||
|
-- myLoggingConsole = LoggingConsole:new({
|
||||||
|
-- name = "my logging console",
|
||||||
|
-- x = 0,
|
||||||
|
-- y = 0,
|
||||||
|
-- height = 200,
|
||||||
|
-- width = 400,
|
||||||
|
-- }) -- just like making a miniconsole, really
|
||||||
|
function LoggingConsole:new(cons, container)
|
||||||
|
cons = cons or {}
|
||||||
|
local consType = type(cons)
|
||||||
|
assert(consType == "table", "LoggingConsole:new(cons, container): cons must be a valid table of constraints. Got: " .. consType)
|
||||||
|
local me = Geyser.MiniConsole:new(cons, container)
|
||||||
|
setmetatable(me, self)
|
||||||
|
self.__index = self
|
||||||
|
return me
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the file extension of the logfile this console will log to
|
||||||
|
function LoggingConsole:getExtension()
|
||||||
|
local extension = "log"
|
||||||
|
if table.contains({"h", "html"}, self.logFormat) then
|
||||||
|
extension = "html"
|
||||||
|
end
|
||||||
|
return extension
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns a string with all templated items replaced
|
||||||
|
---@tparam string str The templated string to transform
|
||||||
|
---@local
|
||||||
|
function LoggingConsole:transformTemplate(str)
|
||||||
|
local ttbl = getTime()
|
||||||
|
local year = ttbl.year
|
||||||
|
local month = string.format("%02d", ttbl.month)
|
||||||
|
local day = string.format("%02d", ttbl.day)
|
||||||
|
local name = self.name
|
||||||
|
local extension = self:getExtension()
|
||||||
|
str = str:gsub("|h", homedir)
|
||||||
|
str = str:gsub("|y", year)
|
||||||
|
str = str:gsub("|m", month)
|
||||||
|
str = str:gsub("|d", day)
|
||||||
|
str = str:gsub("|n", name)
|
||||||
|
str = str:gsub("|e", extension)
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the path to the logfile for this console
|
||||||
|
function LoggingConsole:getPath()
|
||||||
|
local path = self:transformTemplate(self.path)
|
||||||
|
if not path:ends("/") then
|
||||||
|
path = path .. "/"
|
||||||
|
end
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the path to use for the log file.
|
||||||
|
-- @param path the path to put the log file in. It is templated.<br>|h is replaced by the profile homedir.<br>|y by 4 digit year.<br>|m by 2 digit month<br>|d by 2 digit day<br>|n by the name constraint<br>|e by the file extension (html for h logType, log for others)
|
||||||
|
function LoggingConsole:setPath(path)
|
||||||
|
self.path = path
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the filename for the logfile for this console
|
||||||
|
function LoggingConsole:getFileName()
|
||||||
|
local fileName = self:transformTemplate(self.fileName)
|
||||||
|
fileName = fileName:gsub("[<>:'\"/\\?*]", "_")
|
||||||
|
return fileName
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the fileName to use for the log file.
|
||||||
|
-- @param fileName the fileName to use for the logfile. It is templated.<br>|h is replaced by the profile homedir.<br>|y by 4 digit year.<br>|m by 2 digit month<br>|d by 2 digit day<br>|n by the name constraint<br>|e by the file extension (html for h logType, log for others)
|
||||||
|
function LoggingConsole:setFileName(fileName)
|
||||||
|
self.fileName = fileName
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the pull path and filename for the logfile for this console
|
||||||
|
function LoggingConsole:getFullFilename()
|
||||||
|
local path = self:getPath()
|
||||||
|
local fileName = self:getFileName()
|
||||||
|
local fullPath = path .. fileName
|
||||||
|
fullPath = fullPath:gsub("|", "_")
|
||||||
|
return fullPath
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Turns logging for this console on
|
||||||
|
function LoggingConsole:enableLogging()
|
||||||
|
self.log = true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Turns logging for this console off
|
||||||
|
function LoggingConsole:disableLogging()
|
||||||
|
self.log = false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Creates the path for the logfile for this console if necessary
|
||||||
|
---@local
|
||||||
|
function LoggingConsole:createPathIfNotExists()
|
||||||
|
local path = self:transformTemplate(self.path)
|
||||||
|
if not path:ends("/") then
|
||||||
|
path = path .. "/"
|
||||||
|
end
|
||||||
|
if not exists(path) then
|
||||||
|
local ok, err = dt.mkdir_p(path)
|
||||||
|
if not ok then
|
||||||
|
assert(false, "Could not create directory for log files:" .. path .. "\n Reason was: " .. err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Handles actually writing to the log file
|
||||||
|
---@local
|
||||||
|
function LoggingConsole:writeToLog(str)
|
||||||
|
local fileName = self:getFullFilename()
|
||||||
|
self:createPathIfNotExists()
|
||||||
|
if self:getExtension() == "html" then
|
||||||
|
if not io.exists(fileName) then
|
||||||
|
str = htmlHeader .. str
|
||||||
|
end
|
||||||
|
str = str
|
||||||
|
end
|
||||||
|
local file, err = io.open(fileName, "a")
|
||||||
|
if not file then
|
||||||
|
echo(err .. "\n")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
file:write(str)
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
local parent = Geyser.MiniConsole
|
||||||
|
--- Handler function which does the lifting for c/d/h/echo and appendBuffer to provide the logfile writing functionality
|
||||||
|
---@param str the string to echo. Use "" for appends
|
||||||
|
---@param etype the type of echo. Valid are "c", "d", "h", "e", and "a"
|
||||||
|
---@param log Allows you to override the default behaviour defined by the .log property. Pass true to definitely log, false to skip logging.
|
||||||
|
---@local
|
||||||
|
function LoggingConsole:xEcho(str, etype, log)
|
||||||
|
if log == nil then
|
||||||
|
log = self.log
|
||||||
|
end
|
||||||
|
local logStr
|
||||||
|
local logType = self.logFormat
|
||||||
|
if logType:find("h") then
|
||||||
|
logType = "h"
|
||||||
|
elseif logType ~= "t" then
|
||||||
|
logType = "l"
|
||||||
|
end
|
||||||
|
if etype == "d" then -- decho
|
||||||
|
if logType == "h" then
|
||||||
|
logStr = dt.decho2html(str)
|
||||||
|
elseif logType == "t" then
|
||||||
|
logStr = dt.decho2string(str)
|
||||||
|
else
|
||||||
|
logStr = dt.decho2ansi(str)
|
||||||
|
end
|
||||||
|
parent.decho(self, str)
|
||||||
|
elseif etype == "c" then -- cecho
|
||||||
|
if logType == "h" then
|
||||||
|
logStr = dt.cecho2html(str)
|
||||||
|
elseif logType == "t" then
|
||||||
|
logStr = dt.cecho2string(str)
|
||||||
|
else
|
||||||
|
logStr = dt.cecho2ansi(str)
|
||||||
|
end
|
||||||
|
parent.cecho(self, str)
|
||||||
|
elseif etype == "h" then -- hecho
|
||||||
|
if logType == "h" then
|
||||||
|
logStr = dt.hecho2html(str)
|
||||||
|
elseif logType == "t" then
|
||||||
|
logStr = dt.hecho2string(str)
|
||||||
|
else
|
||||||
|
logStr = dt.hecho2ansi(str)
|
||||||
|
end
|
||||||
|
parent.hecho(self, str)
|
||||||
|
elseif etype == "a" then -- append
|
||||||
|
str = dt.append2decho()
|
||||||
|
str = str .. "\n"
|
||||||
|
if logType == "h" then
|
||||||
|
logStr = dt.decho2html(str)
|
||||||
|
elseif logType == "t" then
|
||||||
|
logStr = dt.decho2string(str)
|
||||||
|
else
|
||||||
|
logStr = dt.decho2ansi(str)
|
||||||
|
end
|
||||||
|
parent.appendBuffer(self)
|
||||||
|
elseif etype == "e" then -- echo
|
||||||
|
if logType == "h" then
|
||||||
|
logStr = dt.decho2html(str)
|
||||||
|
else
|
||||||
|
logStr = str
|
||||||
|
end
|
||||||
|
parent.echo(self, str)
|
||||||
|
end
|
||||||
|
if log then
|
||||||
|
self:writeToLog(logStr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Does the actual lifting of echoing links/popups
|
||||||
|
-- @local
|
||||||
|
function LoggingConsole:xEchoLink(text, lType, command, hint, useFormat, log)
|
||||||
|
if log == nil then
|
||||||
|
log = self.log
|
||||||
|
end
|
||||||
|
local logStr = ""
|
||||||
|
if lType:starts("c") then
|
||||||
|
if self.logFormat == "h" then
|
||||||
|
logStr = dt.cecho2html(text)
|
||||||
|
elseif self.logFormat == "l" then
|
||||||
|
logStr = dt.cecho2ansi(text)
|
||||||
|
elseif self.logFormat == "t" then
|
||||||
|
logStr = dt.cecho2string(text)
|
||||||
|
end
|
||||||
|
if lType:ends("p") then
|
||||||
|
parent.cechoPopup(self, text, command, hint, useFormat)
|
||||||
|
else
|
||||||
|
parent.cechoLink(self, text, command, hint, useFormat)
|
||||||
|
end
|
||||||
|
elseif lType:starts("d") then
|
||||||
|
if self.logFormat == "h" then
|
||||||
|
logStr = dt.decho2html(text)
|
||||||
|
elseif self.logFormat == "l" then
|
||||||
|
logStr = dt.decho2ansi(text)
|
||||||
|
elseif self.logFormat == "t" then
|
||||||
|
logStr = dt.decho2string(text)
|
||||||
|
end
|
||||||
|
if lType:ends("p") then
|
||||||
|
parent.dechoPopup(self, text, command, hint, useFormat)
|
||||||
|
else
|
||||||
|
parent.dechoLink(self, text, command, hint, useFormat)
|
||||||
|
end
|
||||||
|
elseif lType:starts("h") then
|
||||||
|
if self.logFormat == "h" then
|
||||||
|
logStr = dt.hecho2html(text)
|
||||||
|
elseif self.logFormat == "l" then
|
||||||
|
logStr = dt.hecho2ansi(text)
|
||||||
|
elseif self.logFormat == "t" then
|
||||||
|
logStr = dt.hecho2string(text)
|
||||||
|
end
|
||||||
|
if lType:ends("p") then
|
||||||
|
parent.hechoPopup(self, text, command, hint, useFormat)
|
||||||
|
else
|
||||||
|
parent.hechoLink(self, text, command, hint, useFormat)
|
||||||
|
end
|
||||||
|
elseif lType:starts("e") then
|
||||||
|
logStr = text
|
||||||
|
if lType:ends("p") then
|
||||||
|
parent.echoPopup(self, text, command, hint, useFormat)
|
||||||
|
else
|
||||||
|
parent.echoLink(self, text, command, hint, useFormat)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if log then
|
||||||
|
self:writeToLog(logStr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- cechoLink for LoggingConsole
|
||||||
|
-- @param text the text to use for the link
|
||||||
|
-- @param command the command to send when the link is clicked, as text. IE [[send("sleep")]]
|
||||||
|
-- @param hint A tooltip which is displayed when the mouse is over the link
|
||||||
|
-- @param log Should we log this line? Defaults to self.log if not passed.
|
||||||
|
function LoggingConsole:cechoLink(text, command, hint, log)
|
||||||
|
self:xEchoLink(text, "c", command, hint, true, log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- dechoLink for LoggingConsole
|
||||||
|
-- @param text the text to use for the link
|
||||||
|
-- @param command the command to send when the link is clicked, as text. IE [[send("sleep")]]
|
||||||
|
-- @param hint A tooltip which is displayed when the mouse is over the link
|
||||||
|
-- @param log Should we log this line? Defaults to self.log if not passed.
|
||||||
|
function LoggingConsole:dechoLink(text, command, hint, log)
|
||||||
|
self:xEchoLink(text, "d", command, hint, true, log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- hechoLink for LoggingConsole
|
||||||
|
-- @param text the text to use for the link
|
||||||
|
-- @param command the command to send when the link is clicked, as text. IE [[send("sleep")]]
|
||||||
|
-- @param hint A tooltip which is displayed when the mouse is over the link
|
||||||
|
-- @param log Should we log this line? Defaults to self.log if not passed.
|
||||||
|
function LoggingConsole:hechoLink(text, command, hint, log)
|
||||||
|
self:xEchoLink(text, "h", command, hint, true, log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- echoLink for LoggingConsole
|
||||||
|
-- @param text the text to use for the link
|
||||||
|
-- @param command the command to send when the link is clicked, as text. IE [[send("sleep")]]
|
||||||
|
-- @param hint A tooltip which is displayed when the mouse is over the link
|
||||||
|
-- @param useCurrentFormat If set to true, will look like the text around it. If false it will be blue and underline.
|
||||||
|
-- @param log Should we log this line? Defaults to self.log if not passed. If you want to pass this you must pass in useCurrentFormat
|
||||||
|
-- @usage myLoggingConsole:echoLink("This is a link!", [[send("sleep")]], "sleep") -- text "This is a link" will send("sleep") when clicked and be blue w/ underline. Defaut log behaviour (self.log)
|
||||||
|
-- @usage myLoggingConsole:echoLink("This is a link!", [[send("sleep")]], "sleep", false, false) -- same as above, but forces it not to log regardless of self.log setting
|
||||||
|
-- @usage myLoggingConsole:echoLink("This is a link!", [[send("sleep")]], "sleep", true, true) -- same as above, but forces it to log regardless of self.log setting and the text will look like anything else echoed to the console.
|
||||||
|
function LoggingConsole:echoLink(text, command, hint, useCurrentFormat, log)
|
||||||
|
self:xEchoLink(text, "e", command, hint, useCurrentFormat, log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- cechoPopup for LoggingConsole
|
||||||
|
-- @param text the text to use for the link
|
||||||
|
-- @param commands the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]}
|
||||||
|
-- @param hints A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}}
|
||||||
|
-- @param log Should we log this line? Defaults to self.log if not passed.
|
||||||
|
function LoggingConsole:cechoPopup(text, commands, hints, log)
|
||||||
|
self:xEchoLink(text, "cp", commands, hints, true, log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- dechoPopup for LoggingConsole
|
||||||
|
-- @param text the text to use for the link
|
||||||
|
-- @param commands the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]}
|
||||||
|
-- @param hints A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}}
|
||||||
|
-- @param log Should we log this line? Defaults to self.log if not passed.
|
||||||
|
function LoggingConsole:dechoPopup(text, commands, hints, log)
|
||||||
|
self:xEchoLink(text, "dp", commands, hints, true, log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- hechoPopup for LoggingConsole
|
||||||
|
-- @param text the text to use for the link
|
||||||
|
-- @param commands the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]}
|
||||||
|
-- @param hints A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}}
|
||||||
|
-- @param log Should we log this line? Defaults to self.log if not passed.
|
||||||
|
function LoggingConsole:hechoPopup(text, commands, hints, log)
|
||||||
|
self:xEchoLink(text, "hp", commands, hints, true, log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- echoPopup for LoggingConsole
|
||||||
|
-- @param text the text to use for the link
|
||||||
|
-- @param commands the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]}
|
||||||
|
-- @param hints A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}}
|
||||||
|
-- @param useCurrentFormat If set to true, will look like the text around it. If false it will be blue and underline.
|
||||||
|
-- @param log Should we log this line? Defaults to self.log if not passed. If you want to pass this you must pass in useCurrentFormat
|
||||||
|
-- @usage myLoggingConsole:echoPopup("This is a link!", {[[send("sleep")]], [[send("stand")]], {"sleep", "stand"}) -- text "This is a link" will send("sleep") when clicked and be blue w/ underline. Defaut log behaviour (self.log)
|
||||||
|
-- @usage myLoggingConsole:echoPopup("This is a link!", {[[send("sleep")]], [[send("stand")]], {"sleep", "stand"}, false, false) -- same as above, but forces it not to log regardless of self.log setting
|
||||||
|
-- @usage myLoggingConsole:echoPopup("This is a link!", {[[send("sleep")]], [[send("stand")]], {"sleep", "stand"}, true, true) -- same as above, but forces it to log regardless of self.log setting and the text will look like anything else echoed to the console.
|
||||||
|
function LoggingConsole:echoPopup(text, commands, hints, useCurrentFormat, log)
|
||||||
|
self:xEchoLink(text, "ep", commands, hints, useCurrentFormat, log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Append copy()ed text to the console
|
||||||
|
-- @param log should we log this?
|
||||||
|
function LoggingConsole:appendBuffer(log)
|
||||||
|
self:xEcho("", "a", log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Append copy()ed text to the console
|
||||||
|
-- @param log should we log this?
|
||||||
|
function LoggingConsole:append(log)
|
||||||
|
self:xEcho("", "a", log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- echo's a string to the console.
|
||||||
|
-- @param str the string to echo
|
||||||
|
-- @param log should this be logged? Used to override the .log constraint
|
||||||
|
function LoggingConsole:echo(str, log)
|
||||||
|
self:xEcho(str, "e", log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- hecho's a string to the console.
|
||||||
|
-- @param str the string to hecho
|
||||||
|
-- @param log should this be logged? Used to override the .log constraint
|
||||||
|
function LoggingConsole:hecho(str, log)
|
||||||
|
self:xEcho(str, "h", log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- decho's a string to the console.
|
||||||
|
-- @param str the string to decho
|
||||||
|
-- @param log should this be logged? Used to override the .log constraint
|
||||||
|
function LoggingConsole:decho(str, log)
|
||||||
|
self:xEcho(str, "d", log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- cecho's a string to the console.
|
||||||
|
-- @param str the string to cecho
|
||||||
|
-- @param log should this be logged? Used to override the .log constraint
|
||||||
|
function LoggingConsole:cecho(str, log)
|
||||||
|
self:xEcho(str, "c", log)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Replays the last X lines from the console's log file, if it exists
|
||||||
|
-- @param numberOfLines The number of lines to replay from the end of the file
|
||||||
|
function LoggingConsole:replay(numberOfLines)
|
||||||
|
local fileName = self:getFullFilename()
|
||||||
|
if not exists(fileName) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local file = io.open(fileName, "r")
|
||||||
|
local lines = file:read("*a")
|
||||||
|
if self:getExtension() == "html" then
|
||||||
|
for _, line in ipairs(htmlHeaderPattern:split("\n")) do
|
||||||
|
if line ~= "" then
|
||||||
|
lines = lines:gsub(line .. "\n", "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
lines = dt.html2decho(lines)
|
||||||
|
else
|
||||||
|
lines = ansi2decho(lines)
|
||||||
|
end
|
||||||
|
local linesTbl = lines:split("\n")
|
||||||
|
local result
|
||||||
|
if #linesTbl <= numberOfLines then
|
||||||
|
result = lines
|
||||||
|
else
|
||||||
|
result = ""
|
||||||
|
local start = #linesTbl - numberOfLines
|
||||||
|
for index, str in ipairs(linesTbl) do
|
||||||
|
if index >= start then
|
||||||
|
result = string.format("%s\n%s", result, str)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:decho(result, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
setmetatable(LoggingConsole, parent)
|
||||||
|
|
||||||
|
return LoggingConsole
|
456
src/resources/MDK/loginator.lua
Executable file
456
src/resources/MDK/loginator.lua
Executable file
@ -0,0 +1,456 @@
|
|||||||
|
--- Loginator creates an object which allows you to log things to file at
|
||||||
|
-- various severity levels, with the ability to only log items above a specific
|
||||||
|
-- severity to file.
|
||||||
|
-- @classmod Loginator
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2021 Damian Monogue
|
||||||
|
-- @license MIT, see LICENSE.lua
|
||||||
|
local Loginator = {
|
||||||
|
format = "h",
|
||||||
|
name = "logname",
|
||||||
|
fileNameTemplate = "|p/log/Loginator/|y-|M-|d-|n.|e",
|
||||||
|
entryTemplate = "|y-|M-|d |h:|m:|s.|x [|c|l|r] |t",
|
||||||
|
level = "warn",
|
||||||
|
bgColor = "black",
|
||||||
|
fontSize = 12,
|
||||||
|
fgColor = "white",
|
||||||
|
}
|
||||||
|
|
||||||
|
local levelColors = {error = "red", warn = "DarkOrange", info = "ForestGreen", debug = "ansi_yellow"}
|
||||||
|
local loggerLevels = {error = 1, warn = 2, info = 3, debug = 4}
|
||||||
|
|
||||||
|
local function exists(path)
|
||||||
|
local ok, err, code = os.rename(path, path)
|
||||||
|
if not ok and code == 13 then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return ok, err
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isWindows()
|
||||||
|
return package.config:sub(1, 1) == [[\]]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mkdir_p(path)
|
||||||
|
path = path:gsub("\\", "/")
|
||||||
|
local pathTbl = path:split("/")
|
||||||
|
local cwd = "/"
|
||||||
|
if isWindows() then
|
||||||
|
cwd = ""
|
||||||
|
end
|
||||||
|
for index, dirName in ipairs(pathTbl) do
|
||||||
|
if index == 1 then
|
||||||
|
cwd = cwd .. dirName
|
||||||
|
else
|
||||||
|
cwd = cwd .. "/" .. dirName
|
||||||
|
cwd = cwd:gsub("//", "/")
|
||||||
|
end
|
||||||
|
if not table.contains({"/", "C:"}, cwd) and not exists(cwd) then
|
||||||
|
local ok, err = lfs.mkdir(cwd)
|
||||||
|
if not ok then
|
||||||
|
return ok, err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local htmlHeaderTemplate = [=[ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/html4/loose.dtd">
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" >
|
||||||
|
<link href='http://fonts.googleapis.com/css?family=Droid+Sans+Mono' rel='stylesheet' type='text/css'>
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
background-color: |b;
|
||||||
|
color: |c;
|
||||||
|
font-family: 'Droid Sans Mono';
|
||||||
|
white-space: pre;
|
||||||
|
font-size: |fpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body><span>
|
||||||
|
]=]
|
||||||
|
|
||||||
|
--- Creates a new Loginator object
|
||||||
|
--@tparam table options table of options for the logger
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <thead>
|
||||||
|
-- <tr>
|
||||||
|
-- <th>option name</th>
|
||||||
|
-- <th>description</th>
|
||||||
|
-- <th>default</th>
|
||||||
|
-- </tr>
|
||||||
|
-- </thead>
|
||||||
|
-- <tbody>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">format</td>
|
||||||
|
-- <td class="tg-1">What format to log in? "h" for html, "a" for ansi, anything else for plaintext.</td>
|
||||||
|
-- <td class="tg-1">"h"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">name</td>
|
||||||
|
-- <td class="tg-2">What is the name of the logger? Will replace |n in templates</td>
|
||||||
|
-- <td class="tg-2">logname</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">level</td>
|
||||||
|
-- <td class="tg-1">What level should the logger operate at? This will control what level the log function defaults to, as well as what logs will actually be written<br>
|
||||||
|
-- Only items of an equal or higher severity to this will be written to the log file.</td>
|
||||||
|
-- <td class="tg-1">"info"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">bgColor</td>
|
||||||
|
-- <td class="tg-2">What background color to use for html logs</td>
|
||||||
|
-- <td class="tg-2">"black"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">fgColor</td>
|
||||||
|
-- <td class="tg-1">What color to use for the main text in html logs</td>
|
||||||
|
-- <td class="tg-1">"white"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">fontSize</td>
|
||||||
|
-- <td class="tg-2">What font size to use in html logs</td>
|
||||||
|
-- <td class="tg-2">12</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">levelColors</td>
|
||||||
|
-- <td class="tg-1">Table with the log level as the key, and the color which corresponds to it as the value</td>
|
||||||
|
-- <td class="tg-1">{ error = "red", warn = "DarkOrange", info = "ForestGreen", debug = "ansi_yellow" }</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">fileNameTemplate</td>
|
||||||
|
-- <td class="tg-2">A template which will be transformed into the full filename, with path. See template options below for replacements</td>
|
||||||
|
-- <td class="tg-2">"|p/log/Loginator/|y-|M-|d-|n.|e"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">entryTemplate</td>
|
||||||
|
-- <td class="tg-1">The template which controls the look of each log entry. See template options below for replacements</td>
|
||||||
|
-- <td class="tg-1">"|y-|M-|d |h:|m:|s.|x [|c|l|r] |t"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- </tbody>
|
||||||
|
-- </table><br>
|
||||||
|
-- Table of template options
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <thead>
|
||||||
|
-- <tr>
|
||||||
|
-- <th>template code</th>
|
||||||
|
-- <th>what it is replaced with</th>
|
||||||
|
-- <th>example</th>
|
||||||
|
-- </tr>
|
||||||
|
-- </thead>
|
||||||
|
-- <tbody>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">|y</td>
|
||||||
|
-- <td class="tg-1">the year in 4 digits</td>
|
||||||
|
-- <td class="tg-1">2021</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">|p</td>
|
||||||
|
-- <td class="tg-2">getMudletHomeDir()</td>
|
||||||
|
-- <td class="tg-2">/home/demonnic/.config/mudlet/profiles/testprofile</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">|M</td>
|
||||||
|
-- <td class="tg-1">Month as 2 digits</td>
|
||||||
|
-- <td class="tg-1">05</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">|d</td>
|
||||||
|
-- <td class="tg-2">day, as 2 digits</td>
|
||||||
|
-- <td class="tg-2">23</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">|h</td>
|
||||||
|
-- <td class="tg-1">hour in 24hr time format, 2 digits</td>
|
||||||
|
-- <td class="tg-1">03</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">|m</td>
|
||||||
|
-- <td class="tg-2">minute as 2 digits</td>
|
||||||
|
-- <td class="tg-2">42</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">|s</td>
|
||||||
|
-- <td class="tg-1">seconds as 2 digits</td>
|
||||||
|
-- <td class="tg-1">34</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">|x</td>
|
||||||
|
-- <td class="tg-2">milliseconds as 3 digits</td>
|
||||||
|
-- <td class="tg-2">194</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">|e</td>
|
||||||
|
-- <td class="tg-1">Filename extension expected. "html" for html format, "log" for everything else</td>
|
||||||
|
-- <td class="tg-1">html</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">|l</td>
|
||||||
|
-- <td class="tg-2">The logging level of the entry, in ALLCAPS</td>
|
||||||
|
-- <td class="tg-2">WARN</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">|c</td>
|
||||||
|
-- <td class="tg-1">The color which corresponds with the logging level. Set via the levelColors table in the options. Example not included.</td>
|
||||||
|
-- <td class="tg-1"></td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">|r</td>
|
||||||
|
-- <td class="tg-2">Reset back to standard color. Used to close |c. Example not included</td>
|
||||||
|
-- <td class="tg-2"></td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">|n</td>
|
||||||
|
-- <td class="tg-1">The name of the logger, set via the options when you have Loginator create it.</td>
|
||||||
|
-- <td class="tg-1">CoolPackageLog</td>
|
||||||
|
-- </tr>
|
||||||
|
--</tbody>
|
||||||
|
--</table>
|
||||||
|
--@return newly created logger object
|
||||||
|
function Loginator:new(options)
|
||||||
|
options = options or {}
|
||||||
|
local optionsType = type(options)
|
||||||
|
if optionsType ~= "table" then
|
||||||
|
return nil, f "Loginator:new(options) options as table expected, got {optionsType}"
|
||||||
|
end
|
||||||
|
local me = table.deepcopy(options)
|
||||||
|
me.levelColors = me.levelColors or {}
|
||||||
|
local lcType = type(me.levelColors)
|
||||||
|
if lcType ~= "table" then
|
||||||
|
return nil, f "Loginator:new(options) provided options.levelColors must be a table, but you provided a {lcType}"
|
||||||
|
end
|
||||||
|
for lvl,clr in pairs(levelColors) do
|
||||||
|
me.levelColors[lvl] = me.levelColors[lvl] or clr
|
||||||
|
end
|
||||||
|
setmetatable(me, self)
|
||||||
|
self.__index = self
|
||||||
|
return me
|
||||||
|
end
|
||||||
|
|
||||||
|
---@local
|
||||||
|
function Loginator:processTemplate(str, level)
|
||||||
|
local lvl = level or self.level
|
||||||
|
local timeTable = getTime()
|
||||||
|
for what, with in pairs({
|
||||||
|
["|y"] = function()
|
||||||
|
return timeTable.year
|
||||||
|
end,
|
||||||
|
["|p"] = getMudletHomeDir,
|
||||||
|
["|M"] = function()
|
||||||
|
return string.format("%02d", timeTable.month)
|
||||||
|
end,
|
||||||
|
["|d"] = function()
|
||||||
|
return string.format("%02d", timeTable.day)
|
||||||
|
end,
|
||||||
|
["|h"] = function()
|
||||||
|
return string.format("%02d", timeTable.hour)
|
||||||
|
end,
|
||||||
|
["|m"] = function()
|
||||||
|
return string.format("%02d", timeTable.min)
|
||||||
|
end,
|
||||||
|
["|s"] = function()
|
||||||
|
return string.format("%02d", timeTable.sec)
|
||||||
|
end,
|
||||||
|
["|x"] = function()
|
||||||
|
return string.format("%03d", timeTable.msec)
|
||||||
|
end,
|
||||||
|
["|e"] = function()
|
||||||
|
return (self.format:starts("h") and "html" or "log")
|
||||||
|
end,
|
||||||
|
["|l"] = function()
|
||||||
|
return lvl:upper()
|
||||||
|
end,
|
||||||
|
["|c"] = function()
|
||||||
|
return self:getColor(lvl)
|
||||||
|
end,
|
||||||
|
["|r"] = function()
|
||||||
|
return self:getReset()
|
||||||
|
end,
|
||||||
|
["|n"] = function()
|
||||||
|
return self.name
|
||||||
|
end,
|
||||||
|
}) do
|
||||||
|
if str:find(what) then
|
||||||
|
str = str:gsub(what, with())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the color to associate with a logging level post-creation
|
||||||
|
--@param color The color to set for the level, as a string. Can be any valid color string for cecho, decho, or hecho.
|
||||||
|
--@param level The level to set the color for. Must be one of 'error', 'warn', 'info', or 'debug'
|
||||||
|
--@return true if the color is updated, or nil+error if it could not be updated for some reason.
|
||||||
|
function Loginator:setColorForLevel(color, level)
|
||||||
|
if not color then
|
||||||
|
return nil, "You must provide a color to set"
|
||||||
|
end
|
||||||
|
if not level then
|
||||||
|
return nil, "You must provide a level to set the color for"
|
||||||
|
end
|
||||||
|
if not loggerLevels[level] then
|
||||||
|
return nil, "Invalid level. Valid levels are 'error', 'warn', 'info', or 'debug'"
|
||||||
|
end
|
||||||
|
if not Geyser.Color.parse(color) then
|
||||||
|
return nil, "You must provide a color which can be parsed by Geyser.Color.parse. Examples are 'blue' (cecho), '<128,0,0>' (decho), '#aa3388' (hecho), or {128,0,0} (table of r,g,b values)"
|
||||||
|
end
|
||||||
|
self.levelColors[level] = color
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---@local
|
||||||
|
function Loginator:getColor(level)
|
||||||
|
if self.format == "t" then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
local r, g, b = Geyser.Color.parse((self.levelColors[level] or {128, 128, 128}))
|
||||||
|
if self.format == "h" then
|
||||||
|
return string.format("<span style='color: rgb(%d,%d,%d);'>", r, g, b)
|
||||||
|
elseif self.format == "a" then
|
||||||
|
return string.format("\27[38:2::%d:%d:%dm", r, g, b)
|
||||||
|
end
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
---@local
|
||||||
|
function Loginator:getReset()
|
||||||
|
if self.format == "t" then
|
||||||
|
return ""
|
||||||
|
elseif self.format == "h" then
|
||||||
|
return "</span>"
|
||||||
|
elseif self.format == "a" then
|
||||||
|
return "\27[39;49m"
|
||||||
|
end
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the full path and filename to the logfile
|
||||||
|
function Loginator:getFullFilename()
|
||||||
|
return self:processTemplate(self.fileNameTemplate)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Write an error level message to the logfile. Error level messages are always written.
|
||||||
|
--@param msg the message to log
|
||||||
|
--@return true if msg written, nil+error if error
|
||||||
|
function Loginator:error(msg)
|
||||||
|
return self:log(msg, "error")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Write a warn level message to the logfile.
|
||||||
|
-- Msg is only written if the logger level is <= warn
|
||||||
|
-- From most to least severe the levels are:
|
||||||
|
-- error > warn > info > debug
|
||||||
|
--@param msg the message to log
|
||||||
|
--@return true if msg written, false if skipped due to level, nil+error if error
|
||||||
|
function Loginator:warn(msg)
|
||||||
|
return self:log(msg, "warn")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Write an info level message to the logfile.
|
||||||
|
-- Msg is only written if the logger level is <= info
|
||||||
|
-- From most to least severe the levels are:
|
||||||
|
-- error > warn > info > debug
|
||||||
|
--@param msg the message to log
|
||||||
|
--@return true if msg written, false if skipped due to level, nil+error if error
|
||||||
|
function Loginator:info(msg)
|
||||||
|
return self:log(msg, "info")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Write a debug level message to the logfile.
|
||||||
|
-- Msg is only written if the logger level is debug
|
||||||
|
-- From most to least severe the levels are:
|
||||||
|
-- error > warn > info > debug
|
||||||
|
--@param msg the message to log
|
||||||
|
--@return true if msg written, false if skipped due to level, nil+error if error
|
||||||
|
function Loginator:debug(msg)
|
||||||
|
return self:log(msg, "debug")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Write a message to the log file and optionally specify the level
|
||||||
|
--@param msg the message to log
|
||||||
|
--@param level the level to log the message at. Defaults to the level of the logger itself if not provided.
|
||||||
|
--@return true if msg written, false if skipped due to level, nil+error if error
|
||||||
|
function Loginator:log(msg, level)
|
||||||
|
level = level or self.level
|
||||||
|
local levelNumber = loggerLevels[level]
|
||||||
|
if not levelNumber then
|
||||||
|
return nil, f"Unknown logging level: {level}. Valid levels are 'error', 'warn', 'info', and 'debug'"
|
||||||
|
end
|
||||||
|
local displayLevelNumber = loggerLevels[self.level]
|
||||||
|
if levelNumber > displayLevelNumber then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local filename = self:getFullFilename()
|
||||||
|
local filteredMsg = self:processTemplate(self.entryTemplate, level):gsub("|t", msg)
|
||||||
|
local ok, err = self:createPathIfNotExists(filename)
|
||||||
|
if err then
|
||||||
|
debugc(err)
|
||||||
|
return ok, err
|
||||||
|
end
|
||||||
|
if self.format == "h" and not io.exists(filename) then
|
||||||
|
filteredMsg = self:getHtmlHeader() .. filteredMsg
|
||||||
|
end
|
||||||
|
local file, err = io.open(filename, "a")
|
||||||
|
if not file then
|
||||||
|
err = string.format("Logger %s failed to open %s because: %s\n", self.name, filename, err)
|
||||||
|
debugc(err)
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
file:write(filteredMsg .. "\n")
|
||||||
|
file:close()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Uses openUrl() to request your OS open the logfile in the appropriate application. Usually your web browser for html and text editor for all others.
|
||||||
|
function Loginator:open()
|
||||||
|
openUrl(self:getFullFilename())
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Uses openUrl() to request your OS open the directory the logfile resides in. This allows for easier browsing if you have more than one file.
|
||||||
|
function Loginator:openDir()
|
||||||
|
openUrl(self:getPath())
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the path to the log file (directory in which the file resides) as a string
|
||||||
|
--@param filename optional filename to return the path of. If not supplied, with use the logger's current filename
|
||||||
|
function Loginator:getPath(filename)
|
||||||
|
filename = filename or self:getFullFilename()
|
||||||
|
filename = filename:gsub([[\]], "/")
|
||||||
|
local filenameTable = filename:split("/")
|
||||||
|
filenameTable[#filenameTable] = nil
|
||||||
|
local path = table.concat(filenameTable, "/")
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
---@local
|
||||||
|
function Loginator:createPathIfNotExists(filename)
|
||||||
|
if exists(filename) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
filename = filename:gsub([[\]], "/")
|
||||||
|
local path = self:getPath(filename)
|
||||||
|
if exists(path) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local ok, err = mkdir_p(path)
|
||||||
|
if not ok then
|
||||||
|
err = string.format("Could not create directory for log files: %s\n Reason was: %s", path, err)
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---@local
|
||||||
|
function Loginator:getHtmlHeader()
|
||||||
|
local header = htmlHeaderTemplate
|
||||||
|
header = header:gsub("|b", self.bgColor)
|
||||||
|
header = header:gsub("|c", self.fgColor)
|
||||||
|
header = header:gsub("|f", self.fontSize)
|
||||||
|
return header
|
||||||
|
end
|
||||||
|
|
||||||
|
return Loginator
|
254
src/resources/MDK/mastermindsolver.lua
Executable file
254
src/resources/MDK/mastermindsolver.lua
Executable file
@ -0,0 +1,254 @@
|
|||||||
|
--- Interactive object which helps you solve a Master Mind puzzle.
|
||||||
|
-- @classmod MasterMindSolver
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2021 Damian Monogue
|
||||||
|
-- @copyright 2008,2009 Konstantinos Asimakis for code used to turn an index number into a guess (indexToGuess method)
|
||||||
|
local MasterMindSolver = {
|
||||||
|
places = 4,
|
||||||
|
items = {"red", "orange", "yellow", "green", "blue", "purple"},
|
||||||
|
template = "|t",
|
||||||
|
autoSend = false,
|
||||||
|
singleCommand = false,
|
||||||
|
separator = " ",
|
||||||
|
allowDuplicates = true,
|
||||||
|
}
|
||||||
|
local mod, floor, random, randomseed = math.mod, math.floor, math.random, math.randomseed
|
||||||
|
local initialGuess = {{1}, {1, 2}, {1, 1, 2}, {1, 1, 2, 2}, {1, 1, 1, 2, 2}, {1, 1, 1, 2, 2, 2}, {1, 1, 1, 1, 2, 2, 2}, {1, 1, 1, 1, 2, 2, 2, 2}}
|
||||||
|
|
||||||
|
--- Removes duplicate elements from a list
|
||||||
|
-- @param tbl the table you want to remove dupes from
|
||||||
|
-- @local
|
||||||
|
local function tableUnique(tbl)
|
||||||
|
local used = {}
|
||||||
|
local result = {}
|
||||||
|
for _, item in ipairs(tbl) do
|
||||||
|
if not used[item] then
|
||||||
|
result[#result + 1] = item
|
||||||
|
used[item] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Creates a new Master Mind solver
|
||||||
|
-- @tparam table options table of configuration options for the solver
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <thead>
|
||||||
|
-- <tr>
|
||||||
|
-- <th>option name</th>
|
||||||
|
-- <th>description</th>
|
||||||
|
-- <th>default</th>
|
||||||
|
-- </tr>
|
||||||
|
-- </thead>
|
||||||
|
-- <tbody>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">places</td>
|
||||||
|
-- <td class="tg-1">How many spots in the code we're breaking?</td>
|
||||||
|
-- <td class="tg-1">4</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">items</td>
|
||||||
|
-- <td class="tg-2">The table of colors/gemstones/whatever which can be part of the code</td>
|
||||||
|
-- <td class="tg-2">{"red", "orange", "yellow", "green", "blue", "purple"}</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">template</td>
|
||||||
|
-- <td class="tg-1">The string template to use for the guess. Within the template, |t is replaced by the item. Used as the command if autoSend is true</td>
|
||||||
|
-- <td class="tg-1">"|t"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">autoSend</td>
|
||||||
|
-- <td class="tg-2">Should we send the guess directly to the server?</td>
|
||||||
|
-- <td class="tg-2">false</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">allowDuplicates</td>
|
||||||
|
-- <td class="tg-1">Can the same item be used more than once in a code?</td>
|
||||||
|
-- <td class="tg-1">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">singleCommand</td>
|
||||||
|
-- <td class="tg-2">If true, combines the guess into a single command, with each one separated by the separator</td>
|
||||||
|
-- <td class="tg-2">false</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">separator</td>
|
||||||
|
-- <td class="tg-1">If sending the guess as a single command, what should we put between the guesses to separate them?</td>
|
||||||
|
-- <td class="tg-1">" "</td>
|
||||||
|
-- </tr>
|
||||||
|
-- </tbody>
|
||||||
|
-- </table>
|
||||||
|
function MasterMindSolver:new(options)
|
||||||
|
if options == nil then
|
||||||
|
options = {}
|
||||||
|
end
|
||||||
|
local optionsType = type(options)
|
||||||
|
if optionsType ~= "table" then
|
||||||
|
error(f "MasterMindSolver:new(options): options as table expected, got {tostring(options)} of type: {optionsType}")
|
||||||
|
end
|
||||||
|
local me = options
|
||||||
|
setmetatable(me, self)
|
||||||
|
self.__index = self
|
||||||
|
me:populateInitialSet()
|
||||||
|
if not me.allowDuplicates then
|
||||||
|
me.initialGuessMade = true -- skip the preset initial guess, they assume duplicates
|
||||||
|
end
|
||||||
|
return me
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Takes a guess number (4, or 1829, or any number from 1 - <total possible combinations>) and returns the
|
||||||
|
-- actual guess.
|
||||||
|
-- @tparam number index which guess to generate
|
||||||
|
-- @local
|
||||||
|
function MasterMindSolver:indexToGuess(index)
|
||||||
|
local guess = {}
|
||||||
|
local options = #self.items
|
||||||
|
for place = 1, self.places do
|
||||||
|
guess[place] = mod(floor((index - 1) / options ^ (place - 1)), options) + 1
|
||||||
|
end
|
||||||
|
return guess
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Compares a guess with the solution and returns the answer
|
||||||
|
-- @tparam table guess The guess you are checking, as numbers. { 1 , 1, 2, 2 } as an example
|
||||||
|
-- @tparam table solution the solution you are checking against, as numbers. { 3, 4, 1, 6 } as an example.
|
||||||
|
-- @local
|
||||||
|
function MasterMindSolver:compare(guess, solution)
|
||||||
|
local coloredPins = 0
|
||||||
|
local whitePins = 0
|
||||||
|
local usedGuessPlace = {}
|
||||||
|
local usedSolutionPlace = {}
|
||||||
|
local places = self.places
|
||||||
|
for place = 1, places do
|
||||||
|
if guess[place] == solution[place] then
|
||||||
|
coloredPins = coloredPins + 1
|
||||||
|
usedGuessPlace[place] = true
|
||||||
|
usedSolutionPlace[place] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for guessPlace = 1, places do
|
||||||
|
if not usedGuessPlace[guessPlace] then
|
||||||
|
for solutionPlace = 1, places do
|
||||||
|
if not usedSolutionPlace[solutionPlace] then
|
||||||
|
if guess[guessPlace] == solution[solutionPlace] then
|
||||||
|
whitePins = whitePins + 1
|
||||||
|
usedSolutionPlace[solutionPlace] = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return coloredPins, whitePins
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Generates an initial table of all guesses from 1 to <total possible> that are valid.
|
||||||
|
-- If allowDuplicates is false, will filter out any of the possible combinations which contain duplicates
|
||||||
|
-- @local
|
||||||
|
function MasterMindSolver:populateInitialSet()
|
||||||
|
local possible = {}
|
||||||
|
local allowDuplicates = self.allowDuplicates
|
||||||
|
local places = self.places
|
||||||
|
local numberOfItems = #self.items
|
||||||
|
local totalCombos = numberOfItems ^ places
|
||||||
|
local numberRemaining = 0
|
||||||
|
for entry = 1, totalCombos do
|
||||||
|
local useItem = true
|
||||||
|
if not allowDuplicates then
|
||||||
|
local guess = self:indexToGuess(entry)
|
||||||
|
local guessUnique = tableUnique(guess)
|
||||||
|
if #guessUnique ~= self.places then
|
||||||
|
useItem = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if useItem then
|
||||||
|
possible[entry] = true
|
||||||
|
numberRemaining = numberRemaining + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.possible = possible
|
||||||
|
self.numberRemaining = numberRemaining
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Function used to reduce the remaining possible answers, given a guess and the answer to that guess. This is not undoable.
|
||||||
|
-- @tparam table guess guess which the answer belongs to. Uses numbers, rather than item names. IE { 1, 1, 2, 2} rather than { "blue", "blue", "green", "green" }
|
||||||
|
-- @tparam number coloredPins how many parts of the guess are both the right color and the right place
|
||||||
|
-- @tparam number whitePins how many parts of the guess are the right color, but in the wrong place
|
||||||
|
-- @return true if you solved the puzzle (coloredPins == number of positions in the code), or false otherwise
|
||||||
|
function MasterMindSolver:reducePossible(guess, coloredPins, whitePins)
|
||||||
|
if coloredPins == #guess then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local possible = self.possible
|
||||||
|
local numberRemaining = 0
|
||||||
|
for entry, _ in pairs(possible) do
|
||||||
|
local testColor, testWhite = self:compare(guess, self:indexToGuess(entry))
|
||||||
|
if testColor ~= coloredPins or testWhite ~= whitePins then
|
||||||
|
possible[entry] = nil
|
||||||
|
else
|
||||||
|
numberRemaining = numberRemaining + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.possible = possible
|
||||||
|
self.numberRemaining = numberRemaining
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Function which assumes you used the last suggested guess from the solver, and reduces the number of possible correct solutions based on the answer given
|
||||||
|
-- @see MasterMindSolver:reducePossible
|
||||||
|
-- @tparam number coloredPins how many parts of the guess are both the right color and the right place
|
||||||
|
-- @tparam number whitePins how many parts of the guess are the right color, but in the wrong place
|
||||||
|
-- @return true if you solved the puzzle (coloredPins == number of positions in the code), or false otherwise
|
||||||
|
function MasterMindSolver:checkLastSuggestion(coloredPins, whitePins)
|
||||||
|
return self:reducePossible(self.guess, coloredPins, whitePins)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Used to get one of the remaining valid possible guesses
|
||||||
|
-- @tparam boolean useActions if true, will return the guess as the commands which would be sent, rather than the numbered items
|
||||||
|
function MasterMindSolver:getValidGuess(useActions)
|
||||||
|
local guess
|
||||||
|
if not self.initialGuessMade then
|
||||||
|
self.initialGuessMade = true
|
||||||
|
guess = initialGuess[self.places]
|
||||||
|
end
|
||||||
|
if not guess then
|
||||||
|
local possible = self.possible
|
||||||
|
local keys = table.keys(possible)
|
||||||
|
randomseed(os.time())
|
||||||
|
guess = self:indexToGuess(keys[random(#keys)])
|
||||||
|
end
|
||||||
|
self.guess = guess
|
||||||
|
if self.autoSend then
|
||||||
|
self:sendGuess(guess)
|
||||||
|
end
|
||||||
|
if useActions then
|
||||||
|
return self:guessToActions(guess)
|
||||||
|
end
|
||||||
|
return guess
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Takes a guess and converts the numbers to commands/actions. IE guessToActions({1, 1, 2, 2}) might return { "blue", "blue", "green", "green" }
|
||||||
|
-- @tparam table guess the guess to convert as numbers. IE { 1, 1, 2, 2}
|
||||||
|
-- @return table of commands/actions correlating to the numbers in the guess.
|
||||||
|
-- @local
|
||||||
|
function MasterMindSolver:guessToActions(guess)
|
||||||
|
local actions = {}
|
||||||
|
for index, itemNumber in ipairs(guess) do
|
||||||
|
local item = self.items[itemNumber]
|
||||||
|
actions[index] = self.template:gsub("|t", item)
|
||||||
|
end
|
||||||
|
return actions
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Handles sending the commands to the game for a guess
|
||||||
|
-- @local
|
||||||
|
function MasterMindSolver:sendGuess(guess)
|
||||||
|
local actions = self:guessToActions(guess)
|
||||||
|
if self.singleCommand then
|
||||||
|
send(table.concat(actions, self.separator))
|
||||||
|
else
|
||||||
|
sendAll(unpack(actions))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return MasterMindSolver
|
1
src/resources/MDK/mdkversion.txt
Executable file
1
src/resources/MDK/mdkversion.txt
Executable file
@ -0,0 +1 @@
|
|||||||
|
2.10.0
|
141
src/resources/MDK/revisionator.lua
Executable file
141
src/resources/MDK/revisionator.lua
Executable file
@ -0,0 +1,141 @@
|
|||||||
|
--- The revisionator provides a standardized way of migrating configurations between revisions
|
||||||
|
-- for instance, it will track what the currently applied revision number is, and when you tell
|
||||||
|
-- tell it to migrate, it will apply every individual migration between the currently applied
|
||||||
|
-- revision and the latest/current revision. This should allow for more seamlessly moving from
|
||||||
|
-- an older version of a package to a new one.
|
||||||
|
-- @classmod revisionator
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2023
|
||||||
|
-- @license MIT, see https://raw.githubusercontent.com/demonnic/MDK/main/src/scripts/LICENSE.lua
|
||||||
|
local revisionator = {
|
||||||
|
name = "Revisionator",
|
||||||
|
patches = {},
|
||||||
|
}
|
||||||
|
revisionator.__index = revisionator
|
||||||
|
local dataDir = getMudletHomeDir() .. "/revisionator"
|
||||||
|
revisionator.dataDir = dataDir
|
||||||
|
if not io.exists(dataDir) then
|
||||||
|
local ok,err = lfs.mkdir(dataDir)
|
||||||
|
if not ok then
|
||||||
|
printDebug(f"Error creating the directory for storing applied revisions: {err}", true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Creates a new revisionator
|
||||||
|
-- @tparam table options the options to create the revisionator with.
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <thead>
|
||||||
|
-- <tr>
|
||||||
|
-- <th>option name</th>
|
||||||
|
-- <th>description</th>
|
||||||
|
-- <th>default</th>
|
||||||
|
-- </tr>
|
||||||
|
-- </thead>
|
||||||
|
-- <tbody>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">name</td>
|
||||||
|
-- <td class="tg-1">The name of the revisionator. This is absolutely required, as the name is used for tracking the currently applied patch level</td>
|
||||||
|
-- <td class="tg-1">raises an error if not provided</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">patches</td>
|
||||||
|
-- <td class="tg-2">A table of patch functions. It is traversed using ipairs, so must be in the form of {function1, function2, function3} etc. If you do not provide it, you can add the patches by calling :addPatch for each patch in order.</td>
|
||||||
|
-- <td class="tg-2">{}</td>
|
||||||
|
-- </tr>
|
||||||
|
--</tbody>
|
||||||
|
--</table>
|
||||||
|
function revisionator:new(options)
|
||||||
|
options = options or {}
|
||||||
|
local optionsType = type(options)
|
||||||
|
if optionsType ~= "table" then
|
||||||
|
printError(f"revisionator:new bad argument #1 type, options as table expected, got {optionsType}", true, true)
|
||||||
|
end
|
||||||
|
if not options.name then
|
||||||
|
printError("revisionator:new(options) options must include a 'name' key as this is used as part of tracking the applied patch level.", true, true)
|
||||||
|
end
|
||||||
|
local me = table.deepcopy(options)
|
||||||
|
setmetatable(me, self)
|
||||||
|
return me
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the currently applied revision from file
|
||||||
|
--- @treturn[1] number the revision number currently applied, or 0 if it can't read a current version
|
||||||
|
--- @treturn[2] nil nil
|
||||||
|
--- @treturn[2] string error message
|
||||||
|
function revisionator:getAppliedPatch()
|
||||||
|
local fileName = f"{self.dataDir}/{self.name}.txt"
|
||||||
|
debugc(fileName)
|
||||||
|
local revision = 0
|
||||||
|
if io.exists(fileName) then
|
||||||
|
local file = io.open(fileName, "r")
|
||||||
|
local fileContents = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
local revNumber = tonumber(fileContents)
|
||||||
|
if revNumber then
|
||||||
|
revision = revNumber
|
||||||
|
else
|
||||||
|
return nil, f"Error while attempting to read current patch version from file: {fileName}\nThe contents of the file are {fileContents} and it was unable to be converted to a revision number"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return revision
|
||||||
|
end
|
||||||
|
|
||||||
|
--- go through all the patches in order and apply any which are still necessary
|
||||||
|
--- @treturn boolean true if it successfully applied patches, false if it was already at the latest patch level
|
||||||
|
--- @error error message
|
||||||
|
function revisionator:migrate()
|
||||||
|
local applied,err = self:getAppliedPatch()
|
||||||
|
if not applied then
|
||||||
|
printError(err, true, true)
|
||||||
|
end
|
||||||
|
local patches = self.patches
|
||||||
|
if applied >= #patches then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for revision, patch in ipairs(patches) do
|
||||||
|
if applied < revision then
|
||||||
|
local ok, err = pcall(patch)
|
||||||
|
if not ok then
|
||||||
|
self:setAppliedPatch(revision - 1)
|
||||||
|
return nil, f"Error while running patch #{revision}: {err}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:setAppliedPatch(#patches)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- add a patch to the table of patches
|
||||||
|
--- @tparam function func the function to run as the patch
|
||||||
|
--- @number[opt] position which patch to insert it as? If not supplied, inserts it as the last patch. Which is usually what you want.
|
||||||
|
function revisionator:addPatch(func, position)
|
||||||
|
if position then
|
||||||
|
table.insert(self.patches, position, func)
|
||||||
|
else
|
||||||
|
table.insert(self.patches, func)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Remove a patch from the table of patches
|
||||||
|
--- this is primarily used for testing
|
||||||
|
--- @local
|
||||||
|
--- @number[opt] patchNumber the patch number to remove. Will remove the last item if not provided.
|
||||||
|
function revisionator:removePatch(patchNumber)
|
||||||
|
table.remove(self.patches, patchNumber)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- set the currently applied patch number
|
||||||
|
-- only directly called for testing
|
||||||
|
--- @local
|
||||||
|
--- @number patchNumber the patch number to set as the currently applied patch
|
||||||
|
function revisionator:setAppliedPatch(patchNumber)
|
||||||
|
local fileName = f"{self.dataDir}/{self.name}.txt"
|
||||||
|
local revFile, err = io.open(fileName, "w+")
|
||||||
|
if not revFile then
|
||||||
|
printError(err, true, true)
|
||||||
|
end
|
||||||
|
revFile:write(patchNumber)
|
||||||
|
revFile:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
return revisionator
|
644
src/resources/MDK/schema.lua
Executable file
644
src/resources/MDK/schema.lua
Executable file
@ -0,0 +1,644 @@
|
|||||||
|
--[[
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Sebastian Schoener
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local schema = {}
|
||||||
|
|
||||||
|
-- Checks an object against a schema.
|
||||||
|
function schema.CheckSchema(obj, schem, path)
|
||||||
|
if path == nil then
|
||||||
|
path = schema.Path.new()
|
||||||
|
path:setBase(obj)
|
||||||
|
end
|
||||||
|
if type(schem) == "function" then
|
||||||
|
return schem(obj, path)
|
||||||
|
else -- attempt to simply compare the values
|
||||||
|
if schem == obj then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return schema.Error("Invalid value: "..path.." should be "..tostring(schem), path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function schema.FormatOutput(output)
|
||||||
|
local format = schema.List()
|
||||||
|
for k,v in ipairs(output) do
|
||||||
|
format:append(v:format())
|
||||||
|
end
|
||||||
|
return table.concat(format, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Infrastructure
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Path class. Represents paths to values in a table (the path's *base*).
|
||||||
|
local Path = {}
|
||||||
|
function Path.new(...)
|
||||||
|
local arg = {...}
|
||||||
|
local self = setmetatable({}, Path)
|
||||||
|
self.p = {}
|
||||||
|
for k,v in ipairs(arg) do
|
||||||
|
self.p[k] = v
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sets the base of the path, i.e. the table to which the path is relative.
|
||||||
|
-- Note that this is the actual *table*, not the table's name.
|
||||||
|
function Path:setBase(base)
|
||||||
|
self.base = base
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Gets the base of the path.
|
||||||
|
function Path:getBase()
|
||||||
|
return self.base
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns the target of the path or 'nil' if the path is invalid.
|
||||||
|
function Path:target()
|
||||||
|
if self.base == nil then
|
||||||
|
error("Path:target() called on a path without a base!")
|
||||||
|
end
|
||||||
|
local current = self.base
|
||||||
|
for k,v in ipairs(self.p) do
|
||||||
|
current = current[v]
|
||||||
|
if current == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return current
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pushes an entry to the end of the path.
|
||||||
|
function Path:push(obj)
|
||||||
|
self.p[#self.p + 1] = obj
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pops an entry from the end of the path.
|
||||||
|
function Path:pop()
|
||||||
|
local tmp = self.p[#self.p]
|
||||||
|
self.p[#self.p] = nil
|
||||||
|
return tmp
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns the topmost entry of the end of the path.
|
||||||
|
function Path:top()
|
||||||
|
return self.p[#self.p]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns the length of the path.
|
||||||
|
function Path:length()
|
||||||
|
return #self.p
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns the element at the specified index.
|
||||||
|
function Path:get(index)
|
||||||
|
return self.p[index]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Copies the path.
|
||||||
|
function Path:copy()
|
||||||
|
local cp = Path.new()
|
||||||
|
cp.base = self.base
|
||||||
|
for k,v in ipairs(self) do
|
||||||
|
cp.p[k] = v
|
||||||
|
end
|
||||||
|
return cp
|
||||||
|
end
|
||||||
|
|
||||||
|
Path.__index = Path
|
||||||
|
Path.__tostring = function(tbl)
|
||||||
|
if #tbl.p == 0 then
|
||||||
|
return '<val>'
|
||||||
|
end
|
||||||
|
return table.concat(tbl.p,".")
|
||||||
|
end
|
||||||
|
Path.__concat = function(lhs, rhs)
|
||||||
|
if type(lhs) == "table" then
|
||||||
|
return tostring(lhs)..rhs
|
||||||
|
elseif type(rhs) == "table" then
|
||||||
|
return lhs..tostring(rhs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Path.__len = function(self)
|
||||||
|
return #self.p
|
||||||
|
end
|
||||||
|
|
||||||
|
setmetatable(Path, {
|
||||||
|
__call = function (cls, ...)
|
||||||
|
return Path.new(...)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
schema.Path = Path
|
||||||
|
|
||||||
|
-- List class
|
||||||
|
local List = {}
|
||||||
|
function List.new(...)
|
||||||
|
local self = setmetatable({}, List)
|
||||||
|
local arg = {...}
|
||||||
|
for k,v in ipairs(arg) do
|
||||||
|
self[k] = v
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function List:add(obj)
|
||||||
|
self[#self+1] = obj
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function List:append(list)
|
||||||
|
for k,v in ipairs(list) do
|
||||||
|
self[#self+k] = v
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
List.__index = List
|
||||||
|
List.__tostring = function(self)
|
||||||
|
local tmp = {}
|
||||||
|
for k,v in ipairs(self) do
|
||||||
|
tmp[k] = tostring(v)
|
||||||
|
end
|
||||||
|
return table.concat(tmp, "\n")
|
||||||
|
end
|
||||||
|
setmetatable(List, {
|
||||||
|
__call = function(cls, ...)
|
||||||
|
return List.new(...)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
schema.List = List
|
||||||
|
|
||||||
|
-- Error class. Describes mismatches that occured during the schema-checking.
|
||||||
|
local Error = {}
|
||||||
|
function Error.new(msg, path, suberrors)
|
||||||
|
local self = setmetatable({}, Error)
|
||||||
|
self.message = msg
|
||||||
|
self.path = path:copy()
|
||||||
|
self.suberrors = suberrors
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns a list of strings which represent the error (with indenttation for
|
||||||
|
-- suberrors).
|
||||||
|
function Error:format()
|
||||||
|
local output = List.new(self.message)
|
||||||
|
if self.suberrors ~= nil then
|
||||||
|
for k,sub in pairs(self.suberrors) do
|
||||||
|
local subout = sub:format()
|
||||||
|
for k1,msg in pairs(subout) do
|
||||||
|
output = output:add(" "..msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
Error.__tostring = function(self)
|
||||||
|
return table.concat(self:format(), "\n")
|
||||||
|
end
|
||||||
|
Error.__index = Error
|
||||||
|
setmetatable(Error, {
|
||||||
|
__call = function(cls, ...)
|
||||||
|
return List(Error.new(...))
|
||||||
|
end
|
||||||
|
})
|
||||||
|
schema.Error = Error
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Schema Building Blocks
|
||||||
|
-- A schema is a function taking the object to be checked and the path to the
|
||||||
|
-- current value in the environment.
|
||||||
|
-- It returns either 'true' if the schema accepted the object or an Error
|
||||||
|
-- object which describes why it was rejected.
|
||||||
|
-- The schemata below are just some basic building blocks. Expand them to your
|
||||||
|
-- liking.
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Always accepts.
|
||||||
|
function schema.Any(obj, path)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Always fails.
|
||||||
|
function schema.Nothing(obj, path)
|
||||||
|
return schema.Error("Failure: '"..path.."' will always fail.", path)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Checks a value against a specific type.
|
||||||
|
local function TypeSchema(obj, path, typeId)
|
||||||
|
if type(obj) ~= typeId then
|
||||||
|
return schema.Error("Type mismatch: '"..path.."' should be "..typeId..", is "..type(obj), path)
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function schema.Boolean (obj, path) return TypeSchema(obj, path, "boolean") end
|
||||||
|
function schema.Function(obj, path) return TypeSchema(obj, path, "function") end
|
||||||
|
function schema.Nil (obj, path) return TypeSchema(obj, path, "nil") end
|
||||||
|
function schema.Number (obj, path) return TypeSchema(obj, path, "number") end
|
||||||
|
function schema.String (obj, path) return TypeSchema(obj, path, "string") end
|
||||||
|
function schema.Table (obj, path) return TypeSchema(obj, path, "table") end
|
||||||
|
function schema.UserData(obj, path) return TypeSchema(obj, path, "userdata") end
|
||||||
|
|
||||||
|
-- Checks that some value is a string matching a given pattern.
|
||||||
|
function schema.Pattern(pattern)
|
||||||
|
local userPattern = pattern
|
||||||
|
if not pattern:match("^^") then
|
||||||
|
pattern = "^" .. pattern
|
||||||
|
end
|
||||||
|
if not pattern:match("$$") then
|
||||||
|
pattern = pattern .. "$"
|
||||||
|
end
|
||||||
|
local function CheckPattern(obj, path)
|
||||||
|
local err = schema.String(obj, path)
|
||||||
|
if err then
|
||||||
|
return err
|
||||||
|
end
|
||||||
|
if string.match(obj, pattern) then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return schema.Error("Invalid value: '"..path.."' must match pattern '"..userPattern.."'", path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return CheckPattern
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Checks that some number is an integer.
|
||||||
|
function schema.Integer(obj, path)
|
||||||
|
local err = schema.Number(obj, path)
|
||||||
|
if err then
|
||||||
|
return err
|
||||||
|
end
|
||||||
|
if math.floor(obj) == obj then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return schema.Error("Invalid value: '"..path.."' must be an integral number", path)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Checks that some number is >= 0.
|
||||||
|
function schema.NonNegativeNumber(obj, path)
|
||||||
|
local err = schema.Number(obj, path)
|
||||||
|
if err then
|
||||||
|
return err
|
||||||
|
end
|
||||||
|
if obj >= 0 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return schema.Error("Invalid value: '"..path.."' must be >= 0", path)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Checks that some number is > 0.
|
||||||
|
function schema.PositiveNumber(obj, path)
|
||||||
|
local err = schema.Number(obj, path)
|
||||||
|
if err then
|
||||||
|
return err
|
||||||
|
end
|
||||||
|
if obj > 0 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return schema.Error("Invalid value: '"..path.."' must be > 0", path)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Checks that some value is a number from the interval [lower, upper].
|
||||||
|
function schema.NumberFrom(lower, upper)
|
||||||
|
local function CheckNumberFrom(obj, path)
|
||||||
|
local err = schema.Number(obj, path)
|
||||||
|
if err then
|
||||||
|
return err
|
||||||
|
end
|
||||||
|
if lower <= obj and upper >= obj then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return schema.Error("Invalid value: '"..path.."' must be between "..lower.." and "..upper, path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return CheckNumberFrom
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Takes schemata and accepts their disjunction.
|
||||||
|
function schema.OneOf(...)
|
||||||
|
local arg = {...}
|
||||||
|
local function CheckOneOf(obj, path)
|
||||||
|
for k,v in ipairs(arg) do
|
||||||
|
local err = schema.CheckSchema(obj, v, path)
|
||||||
|
if not err then return nil end
|
||||||
|
end
|
||||||
|
return schema.Error("No suitable alternative: No schema matches '"..path.."'", path)
|
||||||
|
end
|
||||||
|
return CheckOneOf
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Takes a schema and returns an optional schema.
|
||||||
|
function schema.Optional(s)
|
||||||
|
return schema.OneOf(s, schema.Nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Takes schemata and accepts their conjuction.
|
||||||
|
function schema.AllOf(...)
|
||||||
|
local arg = {...}
|
||||||
|
local function CheckAllOf(obj, path)
|
||||||
|
local errmsg = nil
|
||||||
|
for k,v in ipairs(arg) do
|
||||||
|
local err = schema.CheckSchema(obj, v, path)
|
||||||
|
if err then
|
||||||
|
if errmsg == nil then
|
||||||
|
errmsg = err
|
||||||
|
else
|
||||||
|
errmsg = errmsg:append(err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return errmsg
|
||||||
|
end
|
||||||
|
return CheckAllOf
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Builds a record type schema, i.e. a table with a fixed set of keys (strings)
|
||||||
|
-- with corresponding values. Use as in
|
||||||
|
-- Record({
|
||||||
|
-- name = schema,
|
||||||
|
-- name2 = schema2
|
||||||
|
-- })
|
||||||
|
function schema.Record(recordschema, additionalValues)
|
||||||
|
if additionalValues == nil then
|
||||||
|
additionalValues = false
|
||||||
|
end
|
||||||
|
local function CheckRecord(obj, path)
|
||||||
|
if type(obj) ~= "table" then
|
||||||
|
return schema.Error("Type mismatch: '"..path.."' should be a record (table), is "..type(obj), path)
|
||||||
|
end
|
||||||
|
|
||||||
|
local errmsg = nil
|
||||||
|
local function AddError(msg)
|
||||||
|
if errmsg == nil then
|
||||||
|
errmsg = msg
|
||||||
|
else
|
||||||
|
errmsg = errmsg:append(msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for k,v in pairs(recordschema) do
|
||||||
|
path:push(k)
|
||||||
|
local err = schema.CheckSchema(obj[k], v, path)
|
||||||
|
if err then
|
||||||
|
AddError(err)
|
||||||
|
end
|
||||||
|
path:pop()
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, v in pairs(obj) do
|
||||||
|
path:push(k)
|
||||||
|
if type(k) ~= "string" then
|
||||||
|
AddError(schema.Error("Invalid key: '"..path.."' must be of type 'string'", path))
|
||||||
|
end
|
||||||
|
if recordschema[k] == nil and not additionalValues then
|
||||||
|
AddError(schema.Error("Superfluous value: '"..path.."' does not appear in the record schema", path))
|
||||||
|
end
|
||||||
|
path:pop()
|
||||||
|
end
|
||||||
|
return errmsg
|
||||||
|
end
|
||||||
|
return CheckRecord
|
||||||
|
end
|
||||||
|
|
||||||
|
function schema.MixedTable(t_schema, additional_values)
|
||||||
|
local function CheckMixedTable(obj, path)
|
||||||
|
local obj_t = type(obj)
|
||||||
|
if obj_t ~= "table" then
|
||||||
|
local msg = ("Type mismatch: '%s' should be a table, is %s"):format(path, obj_t)
|
||||||
|
return schema.Error(msg, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
local errmsg = nil
|
||||||
|
local function AddError(msg)
|
||||||
|
if errmsg == nil then
|
||||||
|
errmsg = msg
|
||||||
|
else
|
||||||
|
errmsg = errmsg:append(msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local checked_keys = {}
|
||||||
|
for k, v in pairs(t_schema) do
|
||||||
|
path:push(k)
|
||||||
|
local err = schema.CheckSchema(obj[k], v, path)
|
||||||
|
if err then
|
||||||
|
AddError(err)
|
||||||
|
end
|
||||||
|
checked_keys[k] = true
|
||||||
|
path:pop()
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, v in pairs(obj) do
|
||||||
|
if not checked_keys[k] then
|
||||||
|
path:push(k)
|
||||||
|
local k_type = type(k)
|
||||||
|
if k_type ~= "string" and k_type ~= "number" then
|
||||||
|
local msg = ("Invalid key: '%s' must be of type 'string' or 'number'"):format(k_type)
|
||||||
|
AddError(schema.Error(msg, path))
|
||||||
|
end
|
||||||
|
|
||||||
|
local t_schema_v = t_schema[k]
|
||||||
|
if t_schema_v then
|
||||||
|
local err = schema.CheckSchema(v, t_schema_v, path)
|
||||||
|
if err then
|
||||||
|
AddError(err)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if not additional_values then
|
||||||
|
local msg = ("Superfluous value: '%s' does not appear in the table schema")
|
||||||
|
:format(path)
|
||||||
|
AddError(schema.Error(msg, path))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
path:pop()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return errmsg
|
||||||
|
end
|
||||||
|
return CheckMixedTable
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Builds a map type schema, i.e. a table with an arbitraty number of
|
||||||
|
-- entries where both all keys (and all vaules) fit a common schema.
|
||||||
|
function schema.Map(keyschema, valschema)
|
||||||
|
local function CheckMap(obj, path)
|
||||||
|
if type(obj) ~= "table" then
|
||||||
|
return schema.Error("Type mismatch: '"..path.."' should be a map (table), is "..type(obj), path)
|
||||||
|
end
|
||||||
|
|
||||||
|
local errmsg = nil
|
||||||
|
local function AddError(msg)
|
||||||
|
if errmsg == nil then
|
||||||
|
errmsg = msg
|
||||||
|
else
|
||||||
|
errmsg = errmsg:append(msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- aggregate error message
|
||||||
|
for k, v in pairs(obj) do
|
||||||
|
path:push(k)
|
||||||
|
local keyErr = schema.CheckSchema(k, keyschema, path)
|
||||||
|
if keyErr then
|
||||||
|
AddError(schema.Error("Invalid map key", path, keyErr))
|
||||||
|
end
|
||||||
|
|
||||||
|
local valErr = schema.CheckSchema(v, valschema, path)
|
||||||
|
if valErr then
|
||||||
|
AddError(valErr)
|
||||||
|
end
|
||||||
|
path:pop()
|
||||||
|
end
|
||||||
|
return errmsg
|
||||||
|
end
|
||||||
|
return CheckMap
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Builds a collection type schema, i.e. a table with an arbitrary number of
|
||||||
|
-- entries where we only care about the type of the values.
|
||||||
|
function schema.Collection(valschema)
|
||||||
|
return schema.Map(schema.Any, valschema)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Builds a tuple type schema, i.e. a table with a fixed number of entries,
|
||||||
|
-- each indexed by a number and with a fixed type.
|
||||||
|
function schema.Tuple(...)
|
||||||
|
local arg = {...}
|
||||||
|
|
||||||
|
local function CheckTuple(obj, path)
|
||||||
|
if type(obj) ~= "table" then
|
||||||
|
return schema.Error("Type mismatch: '"..path.."' should be a map (tuple), is "..type(obj), path)
|
||||||
|
end
|
||||||
|
|
||||||
|
if #obj ~= #arg then
|
||||||
|
return schema.Error("Invalid length: '"..path.." should have exactly "..#arg.." elements", path)
|
||||||
|
end
|
||||||
|
|
||||||
|
local errmsg = nil
|
||||||
|
local function AddError(msg)
|
||||||
|
if errmsg == nil then
|
||||||
|
errmsg = msg
|
||||||
|
else
|
||||||
|
errmsg = errmsg:append(msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local min = 1
|
||||||
|
local max = #arg
|
||||||
|
for k, v in pairs(obj) do
|
||||||
|
path:push(k)
|
||||||
|
local err = schema.Integer(k, path)
|
||||||
|
if not err then
|
||||||
|
err = schema.CheckSchema(v, arg[k], path)
|
||||||
|
if err then
|
||||||
|
AddError(err)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
AddError(schema.Error("Invalid tuple key", path, err))
|
||||||
|
end
|
||||||
|
path:pop()
|
||||||
|
end
|
||||||
|
return errmsg
|
||||||
|
end
|
||||||
|
return CheckTuple
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Builds a conditional type schema, i.e. a schema that depends on the value of
|
||||||
|
-- another value. The dependence must be *local*, i.e. defined in the same
|
||||||
|
-- table. Use as in
|
||||||
|
-- Case("name", {"Peter", schema1}, {"Mary", schema2}, {OneOf(...), schema3})
|
||||||
|
-- This will check the field "name" against every schema in the first component
|
||||||
|
-- and will return the second component of the first match.
|
||||||
|
function schema.Case(relativePath, ...)
|
||||||
|
if type(relativePath) ~= "table" then
|
||||||
|
relativePath = schema.Path("..", relativePath)
|
||||||
|
end
|
||||||
|
local cases = {...}
|
||||||
|
for k,v in ipairs(cases) do
|
||||||
|
if type(v) ~= "table" then
|
||||||
|
error("Cases expects inputs of the form {conditionSchema, schema}; argument "..v.." is invalid")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function CheckCase(obj, path)
|
||||||
|
local condPath = path:copy()
|
||||||
|
for k=0, #relativePath do
|
||||||
|
local s = relativePath:get(k)
|
||||||
|
if s == ".." then
|
||||||
|
condPath:pop()
|
||||||
|
else
|
||||||
|
condPath:push(s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local errmsg = nil
|
||||||
|
local function AddError(msg)
|
||||||
|
if errmsg == nil then
|
||||||
|
errmsg = msg
|
||||||
|
else
|
||||||
|
errmsg = errmsg:append(msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local anyCond = false
|
||||||
|
local condObj = condPath:target()
|
||||||
|
for k,v in ipairs(cases) do
|
||||||
|
local condSchema = v[1]
|
||||||
|
local valSchema = v[2]
|
||||||
|
local condErr = schema.CheckSchema(condObj, condSchema, condPath)
|
||||||
|
if not condErr then
|
||||||
|
anyCond = true
|
||||||
|
local err = schema.CheckSchema(obj, valSchema, path)
|
||||||
|
if err then
|
||||||
|
AddError(schema.Error("Case failed: Condition "..k.." of '"..path.."' holds but the consequence does not", path, err))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not anyCond then
|
||||||
|
AddError(schema.Error("Case failed: No condition on '"..path.."' holds"))
|
||||||
|
end
|
||||||
|
|
||||||
|
return errmsg
|
||||||
|
end
|
||||||
|
return CheckCase
|
||||||
|
end
|
||||||
|
|
||||||
|
function schema.Test(fn, msg)
|
||||||
|
local function CheckTest(obj, path)
|
||||||
|
local pok, ok = pcall(fn, obj)
|
||||||
|
if pok and ok then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return schema.Error("Invalid value: '"..path..(msg and "': "..msg or ""), path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return CheckTest
|
||||||
|
end
|
||||||
|
|
||||||
|
return schema
|
514
src/resources/MDK/sortbox.lua
Executable file
514
src/resources/MDK/sortbox.lua
Executable file
@ -0,0 +1,514 @@
|
|||||||
|
---An H/VBox alternative which can be set to either vertical or horizontal, and will autosort the windows
|
||||||
|
-- @classmod SortBox
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2020 Damian Monogue
|
||||||
|
-- @license MIT, see LICENSE.lua
|
||||||
|
local SortBox = Geyser.Container:new({
|
||||||
|
name = "SortBoxClass",
|
||||||
|
autoSort = true,
|
||||||
|
timerSort = true,
|
||||||
|
sortInterval = 500,
|
||||||
|
elastic = false,
|
||||||
|
maxHeight = 0,
|
||||||
|
maxWidth = 0,
|
||||||
|
boxType = "v",
|
||||||
|
sortFunction = "gaugeValue",
|
||||||
|
})
|
||||||
|
local BIGNUMBER = 999999999
|
||||||
|
|
||||||
|
--- Sorting functions for spairs, should you wish
|
||||||
|
-- @table SortFunctions
|
||||||
|
-- @field gaugeValue sorts Geyser gauges by value, ascending
|
||||||
|
-- @field reverseGaugeValue sorts Geyser gauges by value, descending
|
||||||
|
-- @field timeLeft sorts TimerGauges by how much time is left, ascending
|
||||||
|
-- @field reverseTimeLeft sorts TimerGauges by how much time is left, descending.
|
||||||
|
-- @field name sorts Geyser objects by name, ascending
|
||||||
|
-- @field reverseName sorts Geyser objects by name, descending
|
||||||
|
-- @field message sorts Geyser labels and gauges by their echoed text, ascending
|
||||||
|
-- @field reverseMessage sorts Geyser labels and gauges by their echoed text, descending
|
||||||
|
SortBox.SortFunctions = {
|
||||||
|
gaugeValue = function(t, a, b)
|
||||||
|
local avalue = t[a].value or BIGNUMBER
|
||||||
|
local bvalue = t[b].value or BIGNUMBER
|
||||||
|
return avalue < bvalue
|
||||||
|
end,
|
||||||
|
reverseGaugeValue = function(t, a, b)
|
||||||
|
local avalue = t[a].value or BIGNUMBER
|
||||||
|
local bvalue = t[b].value or BIGNUMBER
|
||||||
|
return avalue > bvalue
|
||||||
|
end,
|
||||||
|
timeLeft = function(t, a, b)
|
||||||
|
a = t[a]
|
||||||
|
b = t[b]
|
||||||
|
local avalue = a.getTime and tonumber(a:getTime("S.mm")) or BIGNUMBER
|
||||||
|
local bvalue = b.getTime and tonumber(b:getTime("S.mm")) or BIGNUMBER
|
||||||
|
return avalue < bvalue
|
||||||
|
end,
|
||||||
|
reverseTimeLeft = function(t, a, b)
|
||||||
|
a = t[a]
|
||||||
|
b = t[b]
|
||||||
|
local avalue = a.getTime and tonumber(a:getTime("S.mm")) or BIGNUMBER
|
||||||
|
local bvalue = b.getTime and tonumber(b:getTime("S.mm")) or BIGNUMBER
|
||||||
|
return avalue > bvalue
|
||||||
|
end,
|
||||||
|
name = function(t, a, b)
|
||||||
|
return t[a].name < t[b].name
|
||||||
|
end,
|
||||||
|
reverseName = function(t, a, b)
|
||||||
|
return t[a].name > t[b].name
|
||||||
|
end,
|
||||||
|
message = function(t, a, b)
|
||||||
|
a = t[a]
|
||||||
|
b = t[b]
|
||||||
|
local avalue = a.text and a.text.message or a.message
|
||||||
|
local bvalue = b.text and b.text.message or b.message
|
||||||
|
avalue = avalue or ""
|
||||||
|
bvalue = bvalue or ""
|
||||||
|
return avalue < bvalue
|
||||||
|
end,
|
||||||
|
reverseMessage = function(t, a, b)
|
||||||
|
a = t[a]
|
||||||
|
b = t[b]
|
||||||
|
local avalue = a.text and a.text.message or a.message
|
||||||
|
local bvalue = b.text and b.text.message or b.message
|
||||||
|
avalue = avalue or ""
|
||||||
|
bvalue = bvalue or ""
|
||||||
|
return avalue > bvalue
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
--- Creates a new SortBox
|
||||||
|
-- @usage
|
||||||
|
-- local SortBox = require("MDK.sortbox")
|
||||||
|
-- mySortBox = SortBox:new({
|
||||||
|
-- name = "mySortBox",
|
||||||
|
-- x = 400,
|
||||||
|
-- y = 100,
|
||||||
|
-- height = 150,
|
||||||
|
-- width = 300,
|
||||||
|
-- sortFunction = "timeLeft"
|
||||||
|
-- })
|
||||||
|
-- @tparam table options the options to use for the SortBox. See table below for added options
|
||||||
|
-- @param[opt] container the container to add the SortBox into
|
||||||
|
-- <br><br>Table of new options
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <thead>
|
||||||
|
-- <tr>
|
||||||
|
-- <th>option name</th>
|
||||||
|
-- <th>description</th>
|
||||||
|
-- <th>default</th>
|
||||||
|
-- </tr>
|
||||||
|
-- </thead>
|
||||||
|
-- <tbody>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">autoSort</td>
|
||||||
|
-- <td class="tg-1">should the SortBox perform function based sorting? If false, will behave like a normal H/VBox</td>
|
||||||
|
-- <td class="tg-1">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">timerSort</td>
|
||||||
|
-- <td class="tg-2">should the SortBox automatically perform sorting on a timer?</td>
|
||||||
|
-- <td class="tg-2">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">sortInterval</td>
|
||||||
|
-- <td class="tg-1">how frequently should we sort on a timer if timerSort is true, in milliseconds</td>
|
||||||
|
-- <td class="tg-1">500</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">boxType</td>
|
||||||
|
-- <td class="tg-2">Should we stack like an HBox or VBox? use 'h' for hbox and 'v' for vbox</td>
|
||||||
|
-- <td class="tg-2">v</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">sortFunction</td>
|
||||||
|
-- <td class="tg-1">how should we sort the items in the SortBox? see setSortFunction for valid options</td>
|
||||||
|
-- <td class="tg-1">gaugeValue</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">elastic</td>
|
||||||
|
-- <td class="tg-2">Should this container stretch to fit its contents? boxType v stretches in height, h stretches in width.</td>
|
||||||
|
-- <td class="tg-2">false</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">maxHeight</td>
|
||||||
|
-- <td class="tg-1">If elastic, what's the biggest a 'v' style box should grow in height? Use 0 for unlimited</td>
|
||||||
|
-- <td class="tg-1">0</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">maxWidth</td>
|
||||||
|
-- <td class="tg-2">If elastic, what's the biggest a 'h' style box should grow in width? Use 0 for unlimited</td>
|
||||||
|
-- <td class="tg-2">0</td>
|
||||||
|
-- </tr>
|
||||||
|
-- </tbody>
|
||||||
|
-- </table>
|
||||||
|
function SortBox:new(options, container)
|
||||||
|
options = options or {}
|
||||||
|
options.type = options.type or "SortBox"
|
||||||
|
local me = self.parent:new(options, container)
|
||||||
|
setmetatable(me, self)
|
||||||
|
self.__index = self
|
||||||
|
if me.timerSort then
|
||||||
|
me:enableTimer()
|
||||||
|
end
|
||||||
|
me:setBoxType(me.boxType)
|
||||||
|
return me
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Iterates a key:value pair table in a sorted fashion
|
||||||
|
-- @local
|
||||||
|
-- I first found this on https://stackoverflow.com/questions/15706270/sort-a-table-in-lua
|
||||||
|
-- modified slightly, as Mudlet already has table.keys to collect keys, and I don't want
|
||||||
|
-- to sort if no function to sort with is given. In this case, I want it to work like pairs.
|
||||||
|
local function spairs(t, order)
|
||||||
|
local keys = table.keys(t)
|
||||||
|
if order then
|
||||||
|
table.sort(keys, function(a, b)
|
||||||
|
return order(t, a, b)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local i = 0
|
||||||
|
return function()
|
||||||
|
i = i + 1
|
||||||
|
if keys[i] then
|
||||||
|
return keys[i], t[keys[i]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function SortBox:add(window, cons)
|
||||||
|
if self.useAdd2 then
|
||||||
|
Geyser.add2(self, window, cons)
|
||||||
|
else
|
||||||
|
Geyser.add(self, window, cons)
|
||||||
|
end
|
||||||
|
if not self.defer_updates then
|
||||||
|
self:organize()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function SortBox:remove(window)
|
||||||
|
Geyser.remove(self, window)
|
||||||
|
self:organize()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Calling this will cause the SortBox to reposition/resize everything
|
||||||
|
function SortBox:organize()
|
||||||
|
-- make sure we don't divide by zero later
|
||||||
|
if self:get_width() == 0 then
|
||||||
|
self:resize("0.9px", nil)
|
||||||
|
end
|
||||||
|
if self:get_height() == 0 then
|
||||||
|
self:resize(nil, "0.9px")
|
||||||
|
end
|
||||||
|
-- handle the individual boxType organization
|
||||||
|
if self.boxType == "v" then
|
||||||
|
self:vorganize()
|
||||||
|
else
|
||||||
|
self:horganize()
|
||||||
|
end
|
||||||
|
-- shrink/grow if needed
|
||||||
|
self:handleElastic()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- replicates Geyser.HBox functionality, but with the option of sorting
|
||||||
|
-- @local
|
||||||
|
function SortBox:horganize()
|
||||||
|
local window_width = (self:calculate_dynamic_window_size().width / self:get_width()) * 100
|
||||||
|
local start_x = 0
|
||||||
|
local sortFunction = (self.autoSort and self.sortFunction) and SortBox.SortFunctions[self.sortFunction] or nil
|
||||||
|
if sortFunction then
|
||||||
|
for _, window in spairs(self.windowList, sortFunction) do
|
||||||
|
start_x = start_x + self:handleWindow(window, start_x, window_width)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for _, window_name in ipairs(self.windows) do
|
||||||
|
local window = self.windowList[window_name]
|
||||||
|
start_x = start_x + self:handleWindow(window, start_x, window_width)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- replicates Geyser.VBox functionality, but with the option of sorting
|
||||||
|
-- @local
|
||||||
|
function SortBox:vorganize()
|
||||||
|
local window_height = (self:calculate_dynamic_window_size().height / self:get_height()) * 100
|
||||||
|
local start_y = 0
|
||||||
|
local sortFunction = (self.autoSort and self.sortFunction) and SortBox.SortFunctions[self.sortFunction] or nil
|
||||||
|
if sortFunction then
|
||||||
|
for _, window in spairs(self.windowList, sortFunction) do
|
||||||
|
start_y = start_y + self:handleWindow(window, start_y, window_height)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for _, window_name in ipairs(self.windows) do
|
||||||
|
local window = self.windowList[window_name]
|
||||||
|
start_y = start_y + self:handleWindow(window, start_y, window_height)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- handles a single window during the shuffle process
|
||||||
|
-- @local
|
||||||
|
function SortBox:handleWindow(window, start, window_dimension)
|
||||||
|
local width = (window:get_width() / self:get_width()) * 100
|
||||||
|
local height = (window:get_height() / self:get_height()) * 100
|
||||||
|
if window.h_policy == Geyser.Fixed or window.v_policy == Geyser.Fixed then
|
||||||
|
self.contains_fixed = true
|
||||||
|
end
|
||||||
|
if self.boxType == "v" then
|
||||||
|
window:move("0%", start .. "%")
|
||||||
|
if window.h_policy == Geyser.Dynamic then
|
||||||
|
width = 100
|
||||||
|
if window.width ~= width then
|
||||||
|
window:resize(width .. "%", nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if window.v_policy == Geyser.Dynamic then
|
||||||
|
height = window_dimension * window.v_stretch_factor
|
||||||
|
if window.height ~= height then
|
||||||
|
window:resize(nil, height .. "%")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return height
|
||||||
|
else
|
||||||
|
window:move(start .. "%", "0%")
|
||||||
|
if window.h_policy == Geyser.Dynamic then
|
||||||
|
width = window_dimension * window.h_stretch_factor
|
||||||
|
if window.width ~= width then
|
||||||
|
window:resize(width .. "%", nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if window.v_policy == Geyser.Dynamic then
|
||||||
|
height = 100
|
||||||
|
if window.height ~= height then
|
||||||
|
window:resize(nil, height .. "%")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return width
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---handles actually resizing the window if elastic
|
||||||
|
-- @local
|
||||||
|
function SortBox:handleElastic()
|
||||||
|
if not self.elastic or table.is_empty(self.windows) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self.boxType == "v" then
|
||||||
|
local contentHeight, canElastic = self:getContentHeight()
|
||||||
|
if not canElastic then
|
||||||
|
debugc(string.format("SortBox named %s cannot properly elasticize, as it contains at least one item with a dynamic v_policy", self.name))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local currentHeight = self:get_height()
|
||||||
|
local maxHeight = self.maxHeight
|
||||||
|
if maxHeight > 0 and contentHeight > maxHeight then
|
||||||
|
contentHeight = maxHeight
|
||||||
|
end
|
||||||
|
if contentHeight ~= currentHeight then
|
||||||
|
self:resize(nil, contentHeight)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local contentWidth, canElastic = self:getContentWidth()
|
||||||
|
if not canElastic then
|
||||||
|
debugc(string.format("SortBox named %s cannot properly elasticize, as it contains at least one item with a dynamic h_policy", self.name))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local currentWidth = self:get_width()
|
||||||
|
local maxWidth = self.maxWidth
|
||||||
|
if maxWidth > 0 and contentWidth > maxWidth then
|
||||||
|
contentWidth = maxWidth
|
||||||
|
end
|
||||||
|
if contentWidth ~= currentWidth then
|
||||||
|
self:resize(contentWidth, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---prevents gaps from forming during resize if it doesn't autoorganize on a timer.
|
||||||
|
-- @local
|
||||||
|
function SortBox:reposition()
|
||||||
|
Geyser.Container.reposition(self)
|
||||||
|
if self.contains_fixed then
|
||||||
|
self:organize()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the sum of the heights of the contents, and whether this SortBox can be elastic in height
|
||||||
|
-- @local
|
||||||
|
function SortBox:getContentHeight()
|
||||||
|
if self.boxType ~= "v" then
|
||||||
|
return self:get_height()
|
||||||
|
end
|
||||||
|
local canElastic = true
|
||||||
|
local contentHeight = 0
|
||||||
|
for _, window in pairs(self.windowList) do
|
||||||
|
contentHeight = contentHeight + window:get_height()
|
||||||
|
if window.v_policy == Geyser.Dynamic then
|
||||||
|
canElastic = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return contentHeight, canElastic
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the sum of the widths of the contents, and whether this SortBox can be elastic in width.
|
||||||
|
-- @local
|
||||||
|
function SortBox:getContentWidth()
|
||||||
|
if self.boxType == "v" then
|
||||||
|
return self:get_width()
|
||||||
|
end
|
||||||
|
local canElastic = true
|
||||||
|
local contentWidth = 0
|
||||||
|
for _, window in pairs(self.windowList) do
|
||||||
|
contentWidth = contentWidth + window:get_width()
|
||||||
|
if window.h_policy == Geyser.Dynamic then
|
||||||
|
canElastic = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return contentWidth, canElastic
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Enables elasticity for the SortBox.
|
||||||
|
function SortBox:enableElastic()
|
||||||
|
self:setElastic(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Disables elasticity for the SortBox
|
||||||
|
function SortBox:disableElastic()
|
||||||
|
self:setElastic(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set elasticity specifically
|
||||||
|
-- @tparam boolean enabled if true, enable elasticity. If false, disable it.
|
||||||
|
function SortBox:setElastic(enabled)
|
||||||
|
self.elastic = enabled and true or false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the max width of the SortBox if it's elastic
|
||||||
|
-- @tparam number maxWidth The maximum width in pixels to resize the SortBox to. Use 0 for unlimited.
|
||||||
|
function SortBox:setMaxWidth(maxWidth)
|
||||||
|
local mwtype = type(maxWidth)
|
||||||
|
assert(mwtype == "number", string.format("SortBox:setMaxWidth(maxWidth): SortBox: %s maxWidth as number expected, got %s", self.name, mwtype))
|
||||||
|
assert(maxWidth >= 0, string.format("SortBox:setMaxWidth(maxWidth): SortBox: %s maxWidth must be >= 0, %d", self.name, maxWidth))
|
||||||
|
self.maxWidth = maxWidth
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the max height of the SortBox if it's elastic
|
||||||
|
-- @tparam number maxHeight The maximum height in pixels to resize the SortBox to. Use 0 for unlimited.
|
||||||
|
function SortBox:setMaxHeight(maxHeight)
|
||||||
|
local mhtype = type(maxHeight)
|
||||||
|
assert(mhtype == "number", string.format("SortBox:setMaxHeight(maxHeight): SortBox: %s maxHeight as number expected, got %s", self.name, mhtype))
|
||||||
|
assert(maxHeight >= 0, string.format("SortBox:setMaxHeight(maxHeight): SortBox: %s maxHeight must be >= 0, %d", self.name, maxHeight))
|
||||||
|
self.maxHeight = maxHeight
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Starts the SortBox sorting and organizing itself on a timer
|
||||||
|
function SortBox:enableTimer()
|
||||||
|
if self.timerID then
|
||||||
|
self:disableTimer()
|
||||||
|
end
|
||||||
|
self.timerSort = true
|
||||||
|
self.timerID = tempTimer(self.sortInterval / 1000, function()
|
||||||
|
self:organize()
|
||||||
|
end, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Stops the SortBox from sorting and organizing itself on a timer
|
||||||
|
function SortBox:disableTimer()
|
||||||
|
killTimer(self.timerID)
|
||||||
|
self.timerID = nil
|
||||||
|
self.timerSort = false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the sortInterval, or amount of time in milliseconds between auto sorting on a timer if timerSort is true
|
||||||
|
-- @tparam number sortInterval time in milliseconds between auto sorting if timerSort is true
|
||||||
|
function SortBox:setSortInterval(sortInterval)
|
||||||
|
local sitype = type(sortInterval)
|
||||||
|
assert(sitype == "number", string.format("SortBox:setSortInterval(sortInterval): sortInterval as number expected, got %s", sitype))
|
||||||
|
assert(sortInterval > 0, string.format("SortBox:setSortInterval(sortInterval): sortInterval must be positive"))
|
||||||
|
self.sortInterval = sortInterval
|
||||||
|
if self.timerSort then
|
||||||
|
self:enableTimer()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Enables sorting when items are added/removed, or if timerSort is true, every sortInterval milliseconds
|
||||||
|
function SortBox:enableSort()
|
||||||
|
self.autoSort = true
|
||||||
|
self:organize()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Disables sorting when items are added or removed
|
||||||
|
function SortBox:disableSort()
|
||||||
|
self.autoSort = false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set whether the SortBox acts as a VBox or HBox.
|
||||||
|
-- @tparam string boxType If you pass 'h' or 'horizontal' it will act like an HBox. Anything else it will act like a VBox.
|
||||||
|
-- @usage mySortBox:setBoxType("v") -- behave like a VBox
|
||||||
|
-- mySortBox:setBoxType("h") -- behave like an HBox
|
||||||
|
-- mySortBox:setBoxType("beeblebrox") -- why?! Why would you do this? It'll behave like a VBox
|
||||||
|
function SortBox:setBoxType(boxType)
|
||||||
|
boxType = boxType:lower()
|
||||||
|
if boxType == "h" or boxType == "horizontal" then
|
||||||
|
self.boxType = "h"
|
||||||
|
else
|
||||||
|
self.boxType = "v"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Sets the type of sorting in use by this SortBox.
|
||||||
|
-- <br>If an item in the box does not have the appropriate property or function, then 999999999 is used for sorting except as otherwise noted.
|
||||||
|
-- <br>If an invalid option is given, then existing H/VBox behaviour is maintained, just like if autoSort is false.
|
||||||
|
-- @usage mySortBox:setSortFunction("gaugeValue")
|
||||||
|
-- @tparam string functionName what type of sorting should we use? See table below for valid options and their descriptions.
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <thead>
|
||||||
|
-- <tr>
|
||||||
|
-- <th>sort type</th>
|
||||||
|
-- <th>description</th>
|
||||||
|
-- </tr>
|
||||||
|
-- </thead>
|
||||||
|
-- <tbody>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">gaugeValue</td>
|
||||||
|
-- <td class="tg-1">sort gauges based on how full the gauge is, from less full to more</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">reverseGaugeValue</td>
|
||||||
|
-- <td class="tg-2">sort gauges based on how full the gauge is, from more full to less</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">timeLeft</td>
|
||||||
|
-- <td class="tg-1">sort TimerGauges based on the total time left in the gauge, from less time to more</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">reverseTimeLeft</td>
|
||||||
|
-- <td class="tg-2">sort TimerGauges based on the total time left in the gauge, from more time to less</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">name</td>
|
||||||
|
-- <td class="tg-1">sort any item (and mixed types) by name, alphabetically.</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">reverseName</td>
|
||||||
|
-- <td class="tg-2">sort any item (and mixed types) by name, reverse alphabetically.</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">message</td>
|
||||||
|
-- <td class="tg-1">sorts Labels based on their echoed message, alphabetically. If not a label, the empty string will be used</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">reverseMessage</td>
|
||||||
|
-- <td class="tg-2">sorts Labels based on their echoed message, reverse alphabetically. If not a label, the empty string will be used</td>
|
||||||
|
-- </tr>
|
||||||
|
-- </tbody>
|
||||||
|
-- </table>
|
||||||
|
|
||||||
|
function SortBox:setSortFunction(functionName)
|
||||||
|
self.sortFunction = functionName
|
||||||
|
end
|
||||||
|
|
||||||
|
SortBox.parent = Geyser.Container
|
||||||
|
|
||||||
|
return SortBox
|
481
src/resources/MDK/spinbox.lua
Executable file
481
src/resources/MDK/spinbox.lua
Executable file
@ -0,0 +1,481 @@
|
|||||||
|
--- A Geyser object to create a spinbox for adjusting a number
|
||||||
|
-- @classmod spinbox
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2023
|
||||||
|
-- @license MIT, see https://raw.githubusercontent.com/demonnic/MDK/main/src/scripts/LICENSE.lua
|
||||||
|
local spinbox = {
|
||||||
|
parent = Geyser.Container,
|
||||||
|
name = 'SpinboxClass',
|
||||||
|
min = 0,
|
||||||
|
max = 10,
|
||||||
|
delta = 1,
|
||||||
|
value = 0,
|
||||||
|
activeButtonColor = "gray",
|
||||||
|
inactiveButtonColor = "DimGray",
|
||||||
|
integer = true,
|
||||||
|
upArrowLocation = "https://demonnic.github.io/image-assets/uparrow.png",
|
||||||
|
downArrowLocation = "https://demonnic.github.io/image-assets/downarrow.png",
|
||||||
|
color = "#202020"
|
||||||
|
}
|
||||||
|
spinbox.__index = spinbox
|
||||||
|
setmetatable(spinbox, spinbox.parent)
|
||||||
|
|
||||||
|
local gss = Geyser.StyleSheet
|
||||||
|
local directory = getMudletHomeDir() .. "/spinbox/"
|
||||||
|
local saveFile = directory .. "fileLocations.lua"
|
||||||
|
if not io.exists(directory) then
|
||||||
|
lfs.mkdir(directory)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Creates a new spinbox.
|
||||||
|
-- @tparam table cons a table containing the options for this spinbox.
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <thead>
|
||||||
|
-- <tr>
|
||||||
|
-- <th>option name</th>
|
||||||
|
-- <th>description</th>
|
||||||
|
-- <th>default</th>
|
||||||
|
-- </tr>
|
||||||
|
-- </thead>
|
||||||
|
-- <tbody>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">min</td>
|
||||||
|
-- <td class="tg-1">The minimum value for this spinbox</td>
|
||||||
|
-- <td class="tg-1">0</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">max</td>
|
||||||
|
-- <td class="tg-2">The maximum value for this spinbox</td>
|
||||||
|
-- <td class="tg-2">10</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">activeButtonColor</td>
|
||||||
|
-- <td class="tg-1">The color the up/down buttons should be when they are active/able to be used</td>
|
||||||
|
-- <td class="tg-1">gray</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">inactiveButtonColor</td>
|
||||||
|
-- <td class="tg-2">The color the up/down buttons should be when they are inactive/unable to be used</td>
|
||||||
|
-- <td class="tg-2">dimgray</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">integer</td>
|
||||||
|
-- <td class="tg-1">Boolean value. When true, values must always be integers (no decimal place)</td>
|
||||||
|
-- <td class="tg-1">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">delta</td>
|
||||||
|
-- <td class="tg-2">The amount to change the spinbox's value when the up or down button is pressed.</td>
|
||||||
|
-- <td class="tg-2">1</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">upArrowLocation</td>
|
||||||
|
-- <td class="tg-1">The location of the up arrow image. Either a web URL where it can be downloaded, or the location on disk to read it from</td>
|
||||||
|
-- <td class="tg-1">https://demonnic.github.io/image-assets/uparrow.png</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">downArrowLocation</td>
|
||||||
|
-- <td class="tg-2">The location of the down arrow image. Either a web URL where it can be downloaded, or the location on disk to read it from</td>
|
||||||
|
-- <td class="tg-2">https://demonnic.github.io/image-assets/downarrow.png</td>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">callBack</td>
|
||||||
|
-- <td class="tg-1">The function to run when the spinbox's value is updated. Is called with parameters (self.name, value, oldValue)</td>
|
||||||
|
-- <td class="tg-1">nil</td>
|
||||||
|
-- </tr>
|
||||||
|
-- </tr>
|
||||||
|
--</tbody>
|
||||||
|
--</table>
|
||||||
|
-- @param container The Geyser container for this spinbox
|
||||||
|
function spinbox:new(cons, container)
|
||||||
|
cons = cons or {}
|
||||||
|
local consType = type(cons)
|
||||||
|
if consType ~= "table" then
|
||||||
|
printError(f"spinbox:new(cons, container): cons as table of options expected, got {consType}!", true, true)
|
||||||
|
end
|
||||||
|
cons.name = cons.name or Geyser.nameGen("spinbox")
|
||||||
|
local me = self.parent:new(cons, container)
|
||||||
|
setmetatable(me, self)
|
||||||
|
me:createComponents()
|
||||||
|
if me.callBack then
|
||||||
|
me:setCallBack(me.callBack)
|
||||||
|
end
|
||||||
|
me.oldValue = me.value
|
||||||
|
return me
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Creates the components that make up the spinbox UI.
|
||||||
|
-- @local
|
||||||
|
-- Obtains the up and down arrow images specified in the spinbox options.
|
||||||
|
-- Generates styles for the spinbox.
|
||||||
|
-- Calculates the height of the up/down buttons and any remainder space.
|
||||||
|
-- Creates:
|
||||||
|
-- `self.upButton` - A button with an up arrow image for incrementing the value
|
||||||
|
-- `self.downButton` - A button with a down arrow image for decrementing the value
|
||||||
|
-- `self.displayLabel` - A label to display the current spinbox value
|
||||||
|
-- `self.input` - A command line input to allow directly entering a value
|
||||||
|
-- Hides the input by default.
|
||||||
|
-- Applies the generated styles.
|
||||||
|
function spinbox:createComponents()
|
||||||
|
self:obtainImages()
|
||||||
|
self:generateStyles()
|
||||||
|
self:calculateButtonDimensions()
|
||||||
|
|
||||||
|
self.upButton = self:createButton("up")
|
||||||
|
self.downButton = self:createButton("down")
|
||||||
|
|
||||||
|
self.displayLabel = self:createDisplayLabel()
|
||||||
|
|
||||||
|
self.input = self:createInput()
|
||||||
|
self.input:hide()
|
||||||
|
|
||||||
|
self:applyStyles()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Calculates the button height. We use square buttons in this house.
|
||||||
|
-- @local
|
||||||
|
-- Calculates the height of the up/down buttons by dividing the spinbox height in half.
|
||||||
|
-- Stores the remainder (if any) in self.remainder.
|
||||||
|
-- Stores the calculated button height in self.buttonHeight.
|
||||||
|
function spinbox:calculateButtonDimensions()
|
||||||
|
self.buttonHeight = math.floor(self.get_height() / 2)
|
||||||
|
self.remainder = self.get_height() % 2
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Creates a button (up or down arrow) for the spinbox.
|
||||||
|
-- @param type Either "up" or "down" to specify which direction the arrow should point
|
||||||
|
-- @return The created Geyser.Label button
|
||||||
|
-- @local
|
||||||
|
-- Creates a Geyser.Label button with an up or down arrow image.
|
||||||
|
-- Positions the button at the top or bottom of the spinbox respectively.
|
||||||
|
-- Sets a click callback on the button to call increment() or decrement() depending on the type.
|
||||||
|
-- Returns the created button.
|
||||||
|
function spinbox:createButton(type)
|
||||||
|
local button = Geyser.Label:new({
|
||||||
|
name = self.name .. "spinbox_"..type.."Arrow",
|
||||||
|
height = self.buttonHeight,
|
||||||
|
width = self.buttonHeight,
|
||||||
|
x = "100%-" .. self.buttonHeight,
|
||||||
|
y = type == "up" and 0 or self.buttonHeight + self.remainder,
|
||||||
|
}, self)
|
||||||
|
|
||||||
|
button:setClickCallback(function()
|
||||||
|
if type == "up" then
|
||||||
|
self:increment()
|
||||||
|
else
|
||||||
|
self:decrement()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return button
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Creates the display label for the spinbox value.
|
||||||
|
-- @return The created Geyser.Label display label
|
||||||
|
-- @local
|
||||||
|
-- Creates a Geyser.Label to display the current spinbox value.
|
||||||
|
-- Centers the text in the label.
|
||||||
|
-- Sets a double click callback on the label to show the input, put the current
|
||||||
|
-- value in it, select the text, and hide the label.
|
||||||
|
-- Returns the created display label.
|
||||||
|
function spinbox:createDisplayLabel()
|
||||||
|
local displayLabel = Geyser.Label:new({
|
||||||
|
name = self.name .. "spinbox_displayLabel",
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
width = "100%-" .. self.buttonHeight,
|
||||||
|
height = "100%",
|
||||||
|
message = self.value
|
||||||
|
}, self)
|
||||||
|
displayLabel:setAlignment("center")
|
||||||
|
displayLabel:setDoubleClickCallback(function()
|
||||||
|
self.input:show()
|
||||||
|
self.input:print(self.value)
|
||||||
|
self.input:selectText()
|
||||||
|
displayLabel:hide()
|
||||||
|
end)
|
||||||
|
return displayLabel
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Creates the input for directly entering a spinbox value.
|
||||||
|
-- @return The created Geyser.CommandLine input
|
||||||
|
-- @local
|
||||||
|
-- Creates a Geyser.CommandLine input.
|
||||||
|
-- Sets an action on the input to:
|
||||||
|
-- - Attempt to convert the input text to a number
|
||||||
|
-- - If successful, call setValue() with the number to set the spinbox value
|
||||||
|
-- - Hide the input
|
||||||
|
-- - Show the display label
|
||||||
|
-- - Put the new spinbox value in the input
|
||||||
|
-- Returns the created input.
|
||||||
|
function spinbox:createInput()
|
||||||
|
local input = Geyser.CommandLine:new({
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
width = "100%-".. self.buttonHeight,
|
||||||
|
height = "100%",
|
||||||
|
}, self)
|
||||||
|
input:setAction(function(txt)
|
||||||
|
txt = tonumber(txt)
|
||||||
|
if txt then
|
||||||
|
self:setValue(txt)
|
||||||
|
input:hide()
|
||||||
|
end
|
||||||
|
self.displayLabel:show()
|
||||||
|
input:print(self.value)
|
||||||
|
end)
|
||||||
|
return input
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Used to increment the value by the delta amount
|
||||||
|
-- @local
|
||||||
|
-- Increments the spinbox value by the delta amount.
|
||||||
|
-- Checks if the new value would exceed the max, and if so sets it to the max.
|
||||||
|
-- Updates the display label with the new value.
|
||||||
|
-- Applies any styles that depend on the value.
|
||||||
|
function spinbox:increment()
|
||||||
|
local val = self.value + self.delta
|
||||||
|
if val >= self.max then
|
||||||
|
val = self.max
|
||||||
|
end
|
||||||
|
self.oldValue = self.value
|
||||||
|
self.value = val
|
||||||
|
self.displayLabel:echo(val)
|
||||||
|
self:applyStyles()
|
||||||
|
self:handleCallBacks()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Used to decrement the value by the delta amount
|
||||||
|
-- @local
|
||||||
|
-- Decrements the spinbox value by the delta amount.
|
||||||
|
-- Checks if the new value would be below the min, and if so sets it to the min.
|
||||||
|
-- Updates the display label with the new value.
|
||||||
|
-- Applies any styles that depend on the value.
|
||||||
|
function spinbox:decrement()
|
||||||
|
local val = self.value - self.delta
|
||||||
|
if val <= self.min then
|
||||||
|
val = self.min
|
||||||
|
end
|
||||||
|
self.oldValue = self.value
|
||||||
|
self.value = val
|
||||||
|
self.displayLabel:echo(val)
|
||||||
|
self:applyStyles()
|
||||||
|
self:handleCallBacks()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Used to directly set the value of the spinbox.
|
||||||
|
-- @param value The new value to set
|
||||||
|
-- Rounds the value to an integer if the spinbox is integer only.
|
||||||
|
-- Checks if the new value is within the min/max range and clamps it if not.
|
||||||
|
-- Updates the display label with the new value.
|
||||||
|
-- Applies any styles that depend on the value.
|
||||||
|
function spinbox:setValue(value)
|
||||||
|
if self.integer then
|
||||||
|
value = math.floor(value)
|
||||||
|
end
|
||||||
|
if value >= self.max then
|
||||||
|
value = self.max
|
||||||
|
elseif value <= self.min then
|
||||||
|
value = self.min
|
||||||
|
end
|
||||||
|
self.oldValue = self.value
|
||||||
|
self.value = value
|
||||||
|
self.displayLabel:echo(value)
|
||||||
|
self:applyStyles()
|
||||||
|
self:handleCallBacks()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Obtains the up and down arrow images for the spinbox.
|
||||||
|
-- @local
|
||||||
|
-- Gets the previously saved file locations.
|
||||||
|
-- Checks if the up arrow image exists at the upArrowLocation.
|
||||||
|
-- If not, it will download the image from a URL or copy a local file. It saves
|
||||||
|
-- the new location.
|
||||||
|
-- Does the same for the down arrow image and downArrowLocation.
|
||||||
|
-- Saves any new locations to the save file.
|
||||||
|
-- Sets self.upArrowFile and self.downArrowFile to the locations of the images.
|
||||||
|
function spinbox:obtainImages()
|
||||||
|
local locations = self:getFileLocs()
|
||||||
|
local upURL = self.upArrowLocation
|
||||||
|
local downURL = self.downArrowLocation
|
||||||
|
local upFile = locations[upURL]
|
||||||
|
local downFile = locations[downURL]
|
||||||
|
local locationsChanged = false
|
||||||
|
if not (upFile and io.exists(upFile)) then
|
||||||
|
if not upFile then
|
||||||
|
upFile = directory .. self.name .. "/uparrow.png"
|
||||||
|
locations[upURL] = upFile
|
||||||
|
locationsChanged = true
|
||||||
|
end
|
||||||
|
if upURL:match("^http") then
|
||||||
|
self:downloadFile(upURL, upFile)
|
||||||
|
elseif io.exists(upURL) then
|
||||||
|
upFile = upURL
|
||||||
|
locations[upURL] = upFile
|
||||||
|
locationsChanged = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not (downFile and io.exists(downFile)) then
|
||||||
|
if not downFile then
|
||||||
|
downFile = directory .. self.name .. "/downarrow.png"
|
||||||
|
locations[downURL] = downFile
|
||||||
|
locationsChanged = true
|
||||||
|
end
|
||||||
|
if downURL:match("^http") then
|
||||||
|
self:downloadFile(downURL, downFile)
|
||||||
|
elseif io.exists(downURL) then
|
||||||
|
downFile = downURL
|
||||||
|
locations[downURL] = downFile
|
||||||
|
locationsChanged = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.upArrowFile = upFile
|
||||||
|
self.downArrowFile = downFile
|
||||||
|
if locationsChanged then
|
||||||
|
table.save(saveFile, locations)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Handles the actual download of a file from a url
|
||||||
|
-- @param url The url to download the file from
|
||||||
|
-- @param fileName The location to save the downloaded file
|
||||||
|
-- @local
|
||||||
|
-- Creates any missing directories in the file path.
|
||||||
|
-- Registers named event handlers to handle the download completing or erroring.
|
||||||
|
-- The completion handler stops the error handler.
|
||||||
|
-- The error handler prints an error message and stops the completion handler.
|
||||||
|
-- Downloads the file from the url to the fileName location.
|
||||||
|
function spinbox:downloadFile(url, fileName)
|
||||||
|
local parts = fileName:split("/")
|
||||||
|
parts[#parts] = nil
|
||||||
|
local dirName = table.concat(parts, "/") .. "/"
|
||||||
|
if not io.exists(dirName) then
|
||||||
|
lfs.mkdir(dirName)
|
||||||
|
end
|
||||||
|
local uname = "spinbox"
|
||||||
|
local handlerName = self.name .. url
|
||||||
|
local handler = function(event, ...)
|
||||||
|
local args = {...}
|
||||||
|
local file = #args == 1 and args[1] or args[2]
|
||||||
|
if file ~= fileName then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if event == "sysDownloadDone" then
|
||||||
|
debugc(f"INFO:Spinbox successfully downloaded {file}")
|
||||||
|
stopNamedEventHandler(uname, handlerName .. "error")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
cecho(f"\n<red>ERROR:<reset>Spinbox had an issue downloading an image file to {file}: {args[1]}\n")
|
||||||
|
stopNamedEventHandler(uname, handlerName .. "done")
|
||||||
|
end
|
||||||
|
registerNamedEventHandler(uname, handlerName .. "done", "sysDownloadDone", handler, true)
|
||||||
|
registerNamedEventHandler(uname, handlerName .. "error", "sysDownloadError", handler, true)
|
||||||
|
downloadFile(fileName, url)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Responsible for reading the file locations from disk and returning them
|
||||||
|
-- @local
|
||||||
|
function spinbox:getFileLocs()
|
||||||
|
local locations = {}
|
||||||
|
if io.exists(saveFile) then
|
||||||
|
table.load(saveFile, locations)
|
||||||
|
end
|
||||||
|
return locations
|
||||||
|
end
|
||||||
|
|
||||||
|
--- (Re)generates the stylesheets for the spinbox
|
||||||
|
-- Should not need to call but if you change something and it doesn't take effect
|
||||||
|
-- you can try calling this followed by applyStyles
|
||||||
|
function spinbox:generateStyles()
|
||||||
|
self.baseStyle = gss:new([[
|
||||||
|
border-radius: 2px;
|
||||||
|
border-color: black;
|
||||||
|
]])
|
||||||
|
self.activeStyle = gss:new(f[[
|
||||||
|
background-color: {self.activeButtonColor};
|
||||||
|
]], self.baseStyle)
|
||||||
|
self.inactiveStyle = gss:new(f[[
|
||||||
|
background-color: {self.inactiveButtonColor};
|
||||||
|
]], self.baseStyle)
|
||||||
|
self.upStyle = gss:new(f[[
|
||||||
|
border-image: url("{self.upArrowFile}");
|
||||||
|
]])
|
||||||
|
self.downStyle = gss:new(f[[
|
||||||
|
border-image: url("{self.downArrowFile}");
|
||||||
|
]])
|
||||||
|
self.displayStyle = gss:new(f[[
|
||||||
|
background-color: {Geyser.Color.hex(self.color)};
|
||||||
|
text-align: center;
|
||||||
|
]], self.baseStyle)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Applies updated stylesheets to the components of the spinbox
|
||||||
|
-- Should not need to call this directly
|
||||||
|
function spinbox:applyStyles()
|
||||||
|
if self.value >= self.max then
|
||||||
|
self.upStyle:setParent(self.inactiveStyle)
|
||||||
|
else
|
||||||
|
self.upStyle:setParent(self.activeStyle)
|
||||||
|
end
|
||||||
|
if self.value <= self.min then
|
||||||
|
self.downStyle:setParent(self.inactiveStyle)
|
||||||
|
else
|
||||||
|
self.downStyle:setParent(self.activeStyle)
|
||||||
|
end
|
||||||
|
self.upButton:setStyleSheet(self.upStyle:getCSS())
|
||||||
|
self.downButton:setStyleSheet(self.downStyle:getCSS())
|
||||||
|
self.displayLabel:setStyleSheet(self.displayStyle:getCSS())
|
||||||
|
end
|
||||||
|
|
||||||
|
--- sets the color for active buttons on the spinbox
|
||||||
|
-- @param color any valid color formatting string, such a "red" or "#880000" or "<128,0,0>" or a table of colors, like {128, 0,0}. See Geyser.Color.parse at https://www.mudlet.org/geyser/files/geyser/GeyserColor.html#Geyser.Color.parse
|
||||||
|
function spinbox:setActiveButtonColor(color)
|
||||||
|
local colorType = type(color)
|
||||||
|
local hex
|
||||||
|
if colorType == "table" then
|
||||||
|
hex = Geyser.Color.hex(unpack(color))
|
||||||
|
else
|
||||||
|
hex = Geyser.Color.hex(color)
|
||||||
|
end
|
||||||
|
self.activeButtonColor = hex
|
||||||
|
self.activeStyle:set("background-color", hex)
|
||||||
|
self:applyStyles()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- sets the color for inactive buttons on the spinbox
|
||||||
|
-- @param color any valid color formatting string, such a "<red>" or "red" or "<128,0,0>" or a table of colors, like {128, 0,0}. See Geyser.Color.parse at https://www.mudlet.org/geyser/files/geyser/GeyserColor.html#Geyser.Color.parse
|
||||||
|
function spinbox:setInactiveButtonColor(color)
|
||||||
|
local colorType = type(color)
|
||||||
|
local hex
|
||||||
|
if colorType == "table" then
|
||||||
|
hex = Geyser.Color.hex(unpack(color))
|
||||||
|
else
|
||||||
|
hex = Geyser.Color.hex(color)
|
||||||
|
end
|
||||||
|
self.inactiveButtonColor = hex
|
||||||
|
self.inactiveStyle:set("background-color", hex)
|
||||||
|
self:applyStyles()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- internal function that handles calling a registered callback and raising an event any time the
|
||||||
|
-- spinbox value is changed, whether using the buttons or the :set function.
|
||||||
|
function spinbox:handleCallBacks()
|
||||||
|
raiseEvent("spinbox updated", self.name, self.value, self.oldValue)
|
||||||
|
if self.callBack then
|
||||||
|
local ok, err = pcall(self.callBack, self.name, self.value, self.oldValue)
|
||||||
|
if not ok then
|
||||||
|
printError(f"Had an issue running the callback handler for spinbox named {self.name}: {err}", true, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set a callback function for the spinbox to call any time the value of the spinbox is changed
|
||||||
|
-- the function will be called as func(self.value, self.name)
|
||||||
|
function spinbox:setCallBack(func)
|
||||||
|
local funcType = type(func)
|
||||||
|
if funcType ~= "function" then
|
||||||
|
printError(f"spinbox:setCallBack(func): func as function required, got {funcType}", true, true)
|
||||||
|
end
|
||||||
|
self.callBack = func
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return spinbox
|
255
src/resources/MDK/sug.lua
Executable file
255
src/resources/MDK/sug.lua
Executable file
@ -0,0 +1,255 @@
|
|||||||
|
--- Self Updating Gauge, extends <a href="https://www.mudlet.org/geyser/files/geyser/GeyserGauge.html">Geyser.Gauge</a>
|
||||||
|
-- @classmod SUG
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2020 Damian Monogue
|
||||||
|
-- @license MIT, see LICENSE.lua
|
||||||
|
local SUG = {
|
||||||
|
name = "SelfUpdatingGaugeClass",
|
||||||
|
active = true,
|
||||||
|
updateTime = 333,
|
||||||
|
currentVariable = "",
|
||||||
|
maxVariable = "",
|
||||||
|
defaultCurrent = 50,
|
||||||
|
defaultMax = 100,
|
||||||
|
textTemplate = " |c/|m |p%",
|
||||||
|
strict = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Internal function, used to turn a string variable name into a value
|
||||||
|
local function getValueAt(accessString)
|
||||||
|
local ok, err = pcall(loadstring("return " .. tostring(accessString)))
|
||||||
|
if ok then return err end
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ========== End section copied from demontools.lua
|
||||||
|
|
||||||
|
--- Creates a new Self Updating Gauge.
|
||||||
|
-- @tparam table cons table of options which control the Gauge's behaviour. In addition to all valid contraints for Geyser.Gauge, SUG adds:
|
||||||
|
-- <br>
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <tr>
|
||||||
|
-- <th>name</th>
|
||||||
|
-- <th>description</th>
|
||||||
|
-- <th>default</th>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">active</td>
|
||||||
|
-- <td class="tg-1">boolean, if true starts the timer updating</td>
|
||||||
|
-- <td class="tg-1">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">updateTime</td>
|
||||||
|
-- <td class="tg-2">How often should the gauge autoupdate? Milliseconds. 0 to disable the timer but still allow event updates</td>
|
||||||
|
-- <td class="tg-2">333</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">currentVariable</td>
|
||||||
|
-- <td class="tg-1">What variable will hold the 'current' value of the gauge? Pass the name as a string, IE "currentHP" or "gmcp.Char.Vitals.hp"</td>
|
||||||
|
-- <td class="tg-1">""</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">maxVariable</td>
|
||||||
|
-- <td class="tg-2">What variable will hold the 'current' value of the gauge? Pass the name as a string, IE "maxHP" or "gmcp.Char.Vitals.maxhp"</td>
|
||||||
|
-- <td class="tg-2">""</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">textTemplate</td>
|
||||||
|
-- <td class="tg-1">Template to use for the text on the gauge. "|c" replaced with current value, "|m" replaced with max value, "|p" replaced with the % full the gauge should be</td>
|
||||||
|
-- <td class="tg-1">" |c/|m |p%"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">defaultCurrent</td>
|
||||||
|
-- <td class="tg-2">What value to use if the currentVariable points to nil or something which cannot be made a number?</td>
|
||||||
|
-- <td class="tg-2">50</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">defaultMax</td>
|
||||||
|
-- <td class="tg-1">What value to use if the maxVariable points to nil or something which cannot be made a number?</td>
|
||||||
|
-- <td class="tg-1">100</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">updateEvent</td>
|
||||||
|
-- <td class="tg-2">The name of an event to listen for to perform an update. Can be run alongside or instead of the timer updates. Empty string to turn off</td>
|
||||||
|
-- <td class="tg-2">""</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">updateHook</td>
|
||||||
|
-- <td class="tg-1">A function which is run each time the gauge updates. Should take 3 arguments, the gauge itself, current value, and max value. You can return new current and max values to be used, for example `return 34, 120` would cause the gauge to use 34 for current and 120 for max regardless of what the variables it reads say.</td>
|
||||||
|
-- <td class="tg-1"></td>
|
||||||
|
-- </tr>
|
||||||
|
-- </table>
|
||||||
|
-- @param container The Geyser container for this gauge
|
||||||
|
-- @usage
|
||||||
|
-- local SUG = require("MDK.sug") --the following will watch "gmcp.Char.Vitals.hp" and "gmcp.Char.Vitals.maxhp" and update itself every 333 milliseconds
|
||||||
|
-- myGauge = SUG:new({
|
||||||
|
-- name = "myGauge",
|
||||||
|
-- currentVariable = "gmcp.Char.Vitals.hp", --if this is nil, it will use the defaultCurrent of 50
|
||||||
|
-- maxVariable = "gmcp.Char.Vitals.maxhp", --if this is nil, it will use the defaultMax of 100.
|
||||||
|
-- height = 50,
|
||||||
|
-- })
|
||||||
|
function SUG:new(cons, container)
|
||||||
|
local funcName = "SUG:new(cons, container)"
|
||||||
|
cons = cons or {}
|
||||||
|
local consType = type(cons)
|
||||||
|
assert(consType == "table", string.format("%s: cons as table expected, got %s", funcName, consType))
|
||||||
|
local me = SUG.parent:new(cons, container)
|
||||||
|
setmetatable(me, self)
|
||||||
|
self.__index = self
|
||||||
|
-- apply any styling requested
|
||||||
|
if me.cssFront then
|
||||||
|
if not me.cssBack then
|
||||||
|
me.cssBack = me.cssFront .. "background-color: black;"
|
||||||
|
end
|
||||||
|
me:setStyleSheet(me.cssFront, me.cssBack, me.cssText)
|
||||||
|
end
|
||||||
|
if me.active then
|
||||||
|
me:start()
|
||||||
|
end
|
||||||
|
me:update()
|
||||||
|
return me
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set how often to update the gauge on a timer
|
||||||
|
-- @tparam number time time in milliseconds. 0 to disable the timer
|
||||||
|
function SUG:setUpdateTime(time)
|
||||||
|
if type(time) ~= "number" then
|
||||||
|
debugc("SUG:setUpdateTime(time) time as number expected, got " .. type(time))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.updateTime = time
|
||||||
|
if self.active then self:start() end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the event to listen for to update the gauge
|
||||||
|
-- @tparam string event the name of the event to listen for, use "" to disable events without stopping any existing timers
|
||||||
|
function SUG:setUpdateEvent(event)
|
||||||
|
if type(event) ~= string then
|
||||||
|
debugc("SUG:setUpdateEvent(event) event name as string expected, got " .. type(event))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.updateEvent = event
|
||||||
|
if self.active then self:start() end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the name of the variable the Self Updating Gauge watches for the 'current' value of the gauge
|
||||||
|
-- @tparam string variableName The name of the variable to get the current value for the gauge. For instance "currentHP", "gmcp.Char.Vitals.hp" etc
|
||||||
|
function SUG:setCurrentVariable(variableName)
|
||||||
|
local nameType = type(variableName)
|
||||||
|
local funcName = "SUG:setCurrentVariable(variableName)"
|
||||||
|
assert(nameType == "string", string.format("%s: variableName as string expected, got: %s", funcName, nameType))
|
||||||
|
local val = getValueAt(variableName)
|
||||||
|
local valType = type(tonumber(val))
|
||||||
|
assert(valType == "number",
|
||||||
|
string.format("%s: variableName must point to a variable which is a number or coercable into one. %s points to a %s", funcName, variableName,
|
||||||
|
type(val)))
|
||||||
|
self.currentVariable = variableName
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the name of the variable the Self Updating Gauge watches for the 'max' value of the gauge
|
||||||
|
-- @tparam string variableName The name of the variable to get the max value for the gauge. For instance "maxHP", "gmcp.Char.Vitals.maxhp" etc. Set to "" to only check the current value
|
||||||
|
function SUG:setMaxVariable(variableName)
|
||||||
|
if variableName == "" then
|
||||||
|
self.maxVariable = variableName
|
||||||
|
self:update()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local nameType = type(variableName)
|
||||||
|
local funcName = "SUG:setMaxVariable(variableName)"
|
||||||
|
assert(nameType == "string", string.format("%s: variableName as string expected, got: %s", funcName, nameType))
|
||||||
|
local val = getValueAt(variableName)
|
||||||
|
local valType = type(tonumber(val))
|
||||||
|
assert(valType == "number",
|
||||||
|
string.format("%s: variableName must point to a variable which is a number or coercable into one. %s points to a %s", funcName, variableName,
|
||||||
|
type(val)))
|
||||||
|
self.maxVariable = variableName
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the template for the Self Updating Gauge to set the text with. "|c" is replaced by the current value, "|m" is replaced by the max value, and "|p" is replaced by the percentage current/max
|
||||||
|
-- @tparam string template The template to use for the text on the gauge. If the max value is 200 and current is 68, then |c will be replace by 68, |m replaced by 200, and |p replaced by 34.
|
||||||
|
function SUG:setTextTemplate(template)
|
||||||
|
local templateType = type(template)
|
||||||
|
local funcName = "SUG:setTextTemplate(template)"
|
||||||
|
assert(templateType == "string", string.format("%s: template as string expected, got %s", funcName, templateType))
|
||||||
|
self.textTemplate = template
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the updateHook function which is run just prior to the gauge updating
|
||||||
|
-- @tparam function func The function which will be called when the gauge updates. It should take 3 arguments, the gauge itself, the current value, and the max value. If you wish to override the current or max values used for the gauge, you can return new current and max values, like `return newCurrent newMax`
|
||||||
|
function SUG:setUpdateHook(func)
|
||||||
|
local funcType = type(func)
|
||||||
|
if funcType ~= "function" then
|
||||||
|
return nil, "setUpdateHook only takes functions, no strings or anything like that. You passed in: " .. funcType
|
||||||
|
end
|
||||||
|
self.updateHook = func
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Stops the Self Updating Gauge from updating
|
||||||
|
function SUG:stop()
|
||||||
|
self.active = false
|
||||||
|
if self.timer then
|
||||||
|
killTimer(self.timer)
|
||||||
|
self.timer = nil
|
||||||
|
end
|
||||||
|
if self.eventHandler then
|
||||||
|
killAnonymousEventHandler(self.eventHandler)
|
||||||
|
self.eventHandler = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Starts the Self Updating Gauge updating. If it is already updating, it will restart it.
|
||||||
|
function SUG:start()
|
||||||
|
self:stop()
|
||||||
|
self.active = true
|
||||||
|
local update = function() self:update() end
|
||||||
|
if self.updateTime > 0 then
|
||||||
|
self.timer = tempTimer(self.updateTime / 1000, update, true)
|
||||||
|
end
|
||||||
|
local updateEvent = self.updateEvent
|
||||||
|
if updateEvent and updateEvent ~= "" and updateEvent ~= "*" then
|
||||||
|
self.eventHandler = registerAnonymousEventHandler(self.updateEvent, update)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Reads the values from currentVariable and maxVariable, and updates the gauge's value and text.
|
||||||
|
function SUG:update()
|
||||||
|
local current = getValueAt(self.currentVariable)
|
||||||
|
local max = getValueAt(self.maxVariable)
|
||||||
|
current = tonumber(current)
|
||||||
|
max = tonumber(max)
|
||||||
|
if current == nil then
|
||||||
|
current = self.defaultCurrent
|
||||||
|
debugc(string.format(
|
||||||
|
"Self Updating Gauge named %s is trying to update with an invalid current value. Using the defaultCurrent instead. currentVariable: '%s' maxVariable: '%s'",
|
||||||
|
self.name, self.currentVariable, self.maxVariable))
|
||||||
|
end
|
||||||
|
if max == nil then
|
||||||
|
max = self.defaultMax
|
||||||
|
if self.maxVariable ~= "" then
|
||||||
|
debugc(string.format(
|
||||||
|
"Self Updating Gauge named %s is trying to update with an invalid max value. Using the defaultCurrent instead. currentVariable: '%s' maxVariable: '%s'",
|
||||||
|
self.name, self.currentVariable, self.maxVariable))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if self.updateHook and type(self.updateHook) == "function" then
|
||||||
|
local ok, newcur, newmax = pcall(self.updateHook, self, current, max)
|
||||||
|
if ok and newcur then
|
||||||
|
current = newcur
|
||||||
|
max = newmax and newmax or self.defaultMax
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local text = self.textTemplate
|
||||||
|
local percent = math.floor((current / max * 100) + 0.5)
|
||||||
|
text = text:gsub("|c", current)
|
||||||
|
text = text:gsub("|m", max)
|
||||||
|
text = text:gsub("|p", percent)
|
||||||
|
self:setValue(current, max, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
SUG.parent = Geyser.Gauge
|
||||||
|
setmetatable(SUG, Geyser.Gauge)
|
||||||
|
|
||||||
|
return SUG
|
335
src/resources/MDK/textgauge.lua
Executable file
335
src/resources/MDK/textgauge.lua
Executable file
@ -0,0 +1,335 @@
|
|||||||
|
--- Creates a text based gauge, for use in miniconsoles and the like.
|
||||||
|
-- @classmod TextGauge
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2020 Damian Monogue
|
||||||
|
-- @copyright 2021 Damian Monogue
|
||||||
|
-- @license MIT, see LICENSE.lua
|
||||||
|
local TextGauge = {width = 24, fillCharacter = ":", emptyCharacter = "-", showPercent = true, showPercentSymbol = true, format = "c", value = 50}
|
||||||
|
|
||||||
|
--- Creates a new TextGauge.
|
||||||
|
-- @tparam[opt] table options The table of options you would like the TextGauge to start with.
|
||||||
|
-- <br><br>Table of new options
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <thead>
|
||||||
|
-- <tr>
|
||||||
|
-- <th>option name</th>
|
||||||
|
-- <th>description</th>
|
||||||
|
-- <th>default</th>
|
||||||
|
-- </tr>
|
||||||
|
-- </thead>
|
||||||
|
-- <tbody>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">width</td>
|
||||||
|
-- <td class="tg-1">How many characters wide to make the gauge</td>
|
||||||
|
-- <td class="tg-1">24</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">fillCharacter</td>
|
||||||
|
-- <td class="tg-2">What character to use for the 'full' part of the gauge</td>
|
||||||
|
-- <td class="tg-2">:</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">overflowCharacter</td>
|
||||||
|
-- <td class="tg-1">What character to use for >100% part of the gauge</td>
|
||||||
|
-- <td class="tg-1">if not set, it uses whatever you set fillCharacter to</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">emptyCharacter</td>
|
||||||
|
-- <td class="tg-2">What character to use for the 'empty' part of the gauge</td>
|
||||||
|
-- <td class="tg-2">-</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">showPercentSymbol</td>
|
||||||
|
-- <td class="tg-1">Should we show the % sign itself?</td>
|
||||||
|
-- <td class="tg-1">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">showPercent</td>
|
||||||
|
-- <td class="tg-2">Should we show what % of the gauge is filled?</td>
|
||||||
|
-- <td class="tg-2">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">value</td>
|
||||||
|
-- <td class="tg-1">How much of the gauge should be filled</td>
|
||||||
|
-- <td class="tg-1">50</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">format</td>
|
||||||
|
-- <td class="tg-2">What type of color formatting to use? 'c' for cecho, 'd' for decho, 'h' for hecho</td>
|
||||||
|
-- <td class="tg-2">c</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">fillColor</td>
|
||||||
|
-- <td class="tg-1">What color to make the full part of the bar?</td>
|
||||||
|
-- <td class="tg-1">"DarkOrange" or equivalent for your format type</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">emptyColor</td>
|
||||||
|
-- <td class="tg-2">what color to use for the empty part of the bar?</td>
|
||||||
|
-- <td class="tg-2">"white" or format appropriate equivalent</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">percentColor</td>
|
||||||
|
-- <td class="tg-1">What color to print the percentage numvers in, if shown?</td>
|
||||||
|
-- <td class="tg-1">"white" or fortmat appropriate equivalent</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">percentSymbolColor</td>
|
||||||
|
-- <td class="tg-2">What color to make the % if shown?</td>
|
||||||
|
-- <td class="tg-2">If not set, uses what percentColor is set to.</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">overflowColor</td>
|
||||||
|
-- <td class="tg-1">What color to make the >100% portion of the bar?</td>
|
||||||
|
-- <td class="tg-1">If not set, will use the same color as fillColor</td>
|
||||||
|
-- </tr>
|
||||||
|
-- </tbody>
|
||||||
|
-- </table>
|
||||||
|
-- @usage
|
||||||
|
-- local TextGauge = require("MDK.textgauge")
|
||||||
|
-- myTextGauge = TextGauge:new()
|
||||||
|
-- gaugeText = myTextGauge:setValue(382, 830)
|
||||||
|
function TextGauge:new(options)
|
||||||
|
options = options or {}
|
||||||
|
local optionsType = type(options)
|
||||||
|
assert(optionsType == "table" or optionsType == "nil", "TextGauge:new(options): options expected as table, got " .. optionsType)
|
||||||
|
local me = table.deepcopy(options)
|
||||||
|
setmetatable(me, self)
|
||||||
|
self.__index = self
|
||||||
|
me:setDefaultColors()
|
||||||
|
return me
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the width in characters of the gauge
|
||||||
|
-- @tparam number width number of characters wide to make the gauge
|
||||||
|
function TextGauge:setWidth(width)
|
||||||
|
local widthType = type(width)
|
||||||
|
assert(widthType == "number", string.format("TextGauge:setWidth(width): width as number expected, got %s", widthType))
|
||||||
|
self.width = width
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextGauge:setFormat(format)
|
||||||
|
self.format = self:getColorType(format)
|
||||||
|
self:setDefaultColors()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the character to use for the 'full' part of the gauge
|
||||||
|
-- @tparam string character the character to use.
|
||||||
|
function TextGauge:setFillCharacter(character)
|
||||||
|
assert(character ~= nil, "TextGauge:setFillCharacter(character): character required, got nil")
|
||||||
|
assert(utf8.len(character) == 1, "TextGauge:setFillCharacter(character): character must be a single character")
|
||||||
|
self.fillCharacter = character
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the character to use for the 'overflow' (>100%) part of the gauge
|
||||||
|
-- @tparam string character the character to use.
|
||||||
|
function TextGauge:setOverflowCharacter(character)
|
||||||
|
assert(character ~= nil, "TextGauge:setOverflowCharacter(character): character required, got nil")
|
||||||
|
assert(utf8.len(character) == 1, "TextGauge:setOverflowCharacter(character): character must be a single character")
|
||||||
|
self.overflowCharacter = character
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the character to use for the 'full' part of the gauge
|
||||||
|
-- @tparam string character the character to use.
|
||||||
|
function TextGauge:setEmptyCharacter(character)
|
||||||
|
assert(character ~= nil, "TextGauge:setEmptyCharacter(character): character required, got nil")
|
||||||
|
assert(utf8.len(character) == 1, "TextGauge:setEmptyCharacter(character): character must be a single character")
|
||||||
|
self.emptyCharacter = character
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the fill color for the gauge.
|
||||||
|
-- @tparam string color the color to use for the full portion of the gauge. Will be run through Geyser.Golor
|
||||||
|
function TextGauge:setFillColor(color)
|
||||||
|
assert(color ~= nil, "TextGauge:setFillColor(color): color required, got nil")
|
||||||
|
self.fillColor = color
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the overflow color for the gauge.
|
||||||
|
-- @tparam string color the color to use for the full portion of the gauge. Will be run through Geyser.Golor
|
||||||
|
function TextGauge:setOverflowColor(color)
|
||||||
|
assert(color ~= nil, "TextGauge:setOverflowColor(color): color required, got nil")
|
||||||
|
self.overflowColor = color
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the empty color for the gauge.
|
||||||
|
-- @tparam string color the color to use for the empty portion of the gauge. Will be run through Geyser.Golor
|
||||||
|
function TextGauge:setEmptyColor(color)
|
||||||
|
assert(color ~= nil, "TextGauge:setEmptyColor(color): color required, got nil")
|
||||||
|
self.emptyColor = color
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the fill color for the gauge.
|
||||||
|
-- @tparam string color the color to use for the numeric value. Will be run through Geyser.Golor
|
||||||
|
function TextGauge:setPercentColor(color)
|
||||||
|
assert(color ~= nil, "TextGauge:setPercentColor(color): color required, got nil")
|
||||||
|
self.percentColor = color
|
||||||
|
end
|
||||||
|
--- Sets the fill color for the gauge.
|
||||||
|
-- @tparam string color the color to use for the numeric value. Will be run through Geyser.Golor
|
||||||
|
function TextGauge:setPercentSymbolColor(color)
|
||||||
|
assert(color ~= nil, "TextGauge:setPercentSymbolColor(color): color required, got nil")
|
||||||
|
self.percentSymbolColor = color
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Enables reversing the fill direction (right to left instead of the usual left to right)
|
||||||
|
function TextGauge:enableReverse()
|
||||||
|
self.reverse = true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Disables reversing the fill direction (go back to the usual left to right)
|
||||||
|
function TextGauge:disableReverse()
|
||||||
|
self.reverse = false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Enables showing the percent value of the gauge
|
||||||
|
function TextGauge:enableShowPercent()
|
||||||
|
self.showPercent = true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Disables showing the percent value of the gauge
|
||||||
|
function TextGauge:disableShowPercent()
|
||||||
|
self.showPercent = false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Enables showing the percent symbol (appears after the value)
|
||||||
|
function TextGauge:enableShowPercentSymbol()
|
||||||
|
self.showPercentSymbol = true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Enables showing the percent symbol (appears after the value)
|
||||||
|
function TextGauge:disableShowPercentSymbol()
|
||||||
|
self.showPercentSymbol = false
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextGauge:getColorType(format)
|
||||||
|
format = format or self.format
|
||||||
|
local dec = {"d", "decimal", "dec", "decho"}
|
||||||
|
local hex = {"h", "hexidecimal", "hex", "hecho"}
|
||||||
|
local col = {"c", "color", "colour", "col", "name", "cecho"}
|
||||||
|
if table.contains(col, format) then
|
||||||
|
return "c"
|
||||||
|
elseif table.contains(dec, format) then
|
||||||
|
return "d"
|
||||||
|
elseif table.contains(hex, format) then
|
||||||
|
return "h"
|
||||||
|
else
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- internal function, used at instantiation to ensure some colors are set
|
||||||
|
function TextGauge:setDefaultColors()
|
||||||
|
local colorType = self:getColorType()
|
||||||
|
if colorType == "c" then
|
||||||
|
self.percentColor = self.percentColor or "white"
|
||||||
|
self.percentSymbolColor = self.percentSymbolColor or self.percentColor
|
||||||
|
self.fillColor = self.fillColor or "DarkOrange"
|
||||||
|
self.emptyColor = self.emptyColor or "white"
|
||||||
|
self.resetColor = "<reset>"
|
||||||
|
elseif colorType == "d" then
|
||||||
|
self.percentColor = self.percentColor or "<255,255,255>"
|
||||||
|
self.percentSymbolColor = self.percentSymbolColor or self.percentColor
|
||||||
|
self.fillColor = self.fillColor or "<255,140,0>"
|
||||||
|
self.emptyColor = self.emptyColor or "<255,255,255>"
|
||||||
|
self.resetColor = "<r>"
|
||||||
|
elseif colorType == "h" then
|
||||||
|
self.percentColor = self.percentColor or "#ffffff"
|
||||||
|
self.percentSymbolColor = self.percentSymbolColor or self.percentColor
|
||||||
|
self.fillColor = self.fillColor or "#ff8c00"
|
||||||
|
self.emptyColor = self.emptyColor or "#ffffff"
|
||||||
|
self.resetColor = "#r"
|
||||||
|
else
|
||||||
|
self.percentColor = self.percentColor or ""
|
||||||
|
self.percentSymbolColor = self.percentSymbolColor or self.percentColor
|
||||||
|
self.fillColor = self.fillColor or ""
|
||||||
|
self.emptyColor = self.emptyColor or ""
|
||||||
|
self.resetColor = ""
|
||||||
|
end
|
||||||
|
self.overflowColor = self.overflowColor or self.fillColor
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Internal function used to route Geyser.Color based on internally stored format
|
||||||
|
function TextGauge:getColor(color)
|
||||||
|
local colorType = self:getColorType()
|
||||||
|
if colorType == "c" then
|
||||||
|
return string.format("<%s>", color) -- pass the color back in <> for cecho
|
||||||
|
elseif colorType == "d" then
|
||||||
|
return Geyser.Color.hdec(color) -- return it in decho format
|
||||||
|
elseif colorType == "h" then
|
||||||
|
return Geyser.Color.hex(color) -- return it in hex format
|
||||||
|
else
|
||||||
|
return "" -- return an empty string for noncolored output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Used to set the gauge's value and return the string representation of the gauge
|
||||||
|
-- @tparam[opt] number current current value. If no value is passed it will use the stored value. Defaults to 50 to prevent errors.
|
||||||
|
-- @tparam[opt] number max maximum value. If not passed, the internally stored one will be used. Defaults to 100 so that it can be used with single values as a percent
|
||||||
|
-- @usage myGauge:setValue(55) -- sets the gauge to 55% full
|
||||||
|
-- @usage myGauge:setValue(2345, 2780) -- will figure out what the percentage fill is based on the given current/max values
|
||||||
|
function TextGauge:setValue(current, max)
|
||||||
|
current = current or self.value
|
||||||
|
assert(type(current) == "number", "TextGauge:setValue(current,max) current as number expected, got " .. type(current))
|
||||||
|
assert(max == nil or type(max) == "number", "TextGauge:setValue(current, max) option max as number expected, got " .. type(max))
|
||||||
|
if current < 0 then
|
||||||
|
current = 0
|
||||||
|
end
|
||||||
|
max = max or 100
|
||||||
|
local value = math.floor(current / max * 100)
|
||||||
|
self.value = value
|
||||||
|
local width = self.width
|
||||||
|
local percentString = ""
|
||||||
|
local percentSymbolString = ""
|
||||||
|
local fillCharacter = self.fillCharacter
|
||||||
|
local overflowCharacter = self.overflowCharacter or fillCharacter
|
||||||
|
local emptyCharacter = self.emptyCharacter
|
||||||
|
local fillColor = self:getColor(self.fillColor)
|
||||||
|
local overflowColor = self:getColor(self.overflowColor)
|
||||||
|
local emptyColor = self:getColor(self.emptyColor)
|
||||||
|
local percentColor = self:getColor(self.percentColor)
|
||||||
|
local percentSymbolColor = self:getColor(self.percentSymbolColor)
|
||||||
|
local resetColor = self.resetColor
|
||||||
|
if self.showPercent then
|
||||||
|
percentString = string.format("%s%02d%s", percentColor, value, resetColor)
|
||||||
|
width = width - 2
|
||||||
|
end
|
||||||
|
if self.showPercentSymbol then
|
||||||
|
percentSymbolString = string.format("%s%s%s", percentSymbolColor, "%", resetColor)
|
||||||
|
width = width - 1
|
||||||
|
end
|
||||||
|
local perc = value / 100
|
||||||
|
local overflow = perc - 1
|
||||||
|
if overflow < 0 then
|
||||||
|
overflow = 0
|
||||||
|
end
|
||||||
|
if overflow > 1 then
|
||||||
|
perc = 2
|
||||||
|
overflow = 1
|
||||||
|
end
|
||||||
|
local overflowWidth = math.floor(overflow * width)
|
||||||
|
local fillWidth = math.floor((perc - overflow) * width)
|
||||||
|
local emptyWidth = width - fillWidth
|
||||||
|
fillWidth = fillWidth - overflowWidth
|
||||||
|
if value >= 100 and self.showPercent then
|
||||||
|
fillWidth = fillWidth - 1
|
||||||
|
end
|
||||||
|
if value >= 200 and self.showPercent then
|
||||||
|
overflowWidth = overflowWidth - 1
|
||||||
|
end
|
||||||
|
local result = ""
|
||||||
|
if self.reverse then
|
||||||
|
result = string.format("%s%s%s%s%s%s%s%s%s%s%s", emptyColor, string.rep(emptyCharacter, emptyWidth), resetColor,fillColor, string.rep(fillCharacter, fillWidth), resetColor, overflowColor, string.rep(overflowCharacter, overflowWidth), resetColor, percentString, percentSymbolString, resetColor)
|
||||||
|
else
|
||||||
|
result = string.format("%s%s%s%s%s%s%s%s%s%s%s", overflowColor, string.rep(overflowCharacter, overflowWidth), fillColor,
|
||||||
|
string.rep(fillCharacter, fillWidth), resetColor, emptyColor, string.rep(emptyCharacter, emptyWidth), resetColor,
|
||||||
|
percentString, percentSymbolString, resetColor)
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Synonym for setValue
|
||||||
|
function TextGauge:print(...)
|
||||||
|
self:setValue(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
return TextGauge
|
529
src/resources/MDK/timergauge.lua
Executable file
529
src/resources/MDK/timergauge.lua
Executable file
@ -0,0 +1,529 @@
|
|||||||
|
--- Animated countdown timer, extends <a href="https://www.mudlet.org/geyser/files/geyser/GeyserGauge.html">Geyser.Gauge</a>
|
||||||
|
-- @classmod TimerGauge
|
||||||
|
-- @author Damian Monogue <demonnic@gmail.com>
|
||||||
|
-- @copyright 2020 Damian Monogue
|
||||||
|
-- @license MIT, see LICENSE.lua
|
||||||
|
local TimerGauge = {
|
||||||
|
name = "TimerGaugeClass",
|
||||||
|
active = true,
|
||||||
|
showTime = true,
|
||||||
|
prefix = "",
|
||||||
|
timeFormat = "S.t",
|
||||||
|
suffix = "",
|
||||||
|
updateTime = "10",
|
||||||
|
autoHide = true,
|
||||||
|
autoShow = true,
|
||||||
|
manageContainer = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimerGauge:setStyleSheet(cssFront, cssBack, cssText)
|
||||||
|
cssFront = cssFront or self.cssFront
|
||||||
|
cssBack = cssBack or self.cssBack
|
||||||
|
cssBack = cssBack or self.cssFront .. "background-color: black;"
|
||||||
|
cssText = cssText or self.cssText
|
||||||
|
if cssFront then
|
||||||
|
self.front:setStyleSheet(cssFront)
|
||||||
|
end
|
||||||
|
if cssBack then
|
||||||
|
self.back:setStyleSheet(cssBack)
|
||||||
|
end
|
||||||
|
if cssText then
|
||||||
|
self.text:setStyleSheet(cssText)
|
||||||
|
end
|
||||||
|
-- self.gauge:setStyleSheet(cssFront, cssBack, cssText)
|
||||||
|
self.cssFront = cssFront
|
||||||
|
self.cssBack = cssBack
|
||||||
|
self.cssText = cssText
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Shows the TimerGauge. If the manageContainer property is true, then will add it back to its container
|
||||||
|
function TimerGauge:show2()
|
||||||
|
if self.manageContainer and self.savedContainer then
|
||||||
|
self.savedContainer:add(self)
|
||||||
|
self.savedContainer = nil
|
||||||
|
end
|
||||||
|
self:show()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Hides the TimerGauge. If manageContainer property is true, then it will remove it from its container and if the container is an HBox or VBox it will initiate size/position management
|
||||||
|
function TimerGauge:hide2()
|
||||||
|
if self.manageContainer and self.container.name ~= Geyser.name then
|
||||||
|
self.savedContainer = self.container
|
||||||
|
Geyser:add(self)
|
||||||
|
self.savedContainer:remove(self)
|
||||||
|
if self.savedContainer.type == "VBox" or self.savedContainer.type == "HBox" then
|
||||||
|
if self.savedContainer.organize then
|
||||||
|
self.savedContainer:organize()
|
||||||
|
else
|
||||||
|
self.savedContainer:reposition()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:hide()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Starts the timergauge. Works whether the timer is stopped or not. Does not start a timer which is already at 0
|
||||||
|
-- @tparam[opt] boolean show override the autoShow property. True will always show, false will never show.
|
||||||
|
-- @usage myTimerGauge:start() --starts the timer, will show or not based on autoShow property
|
||||||
|
-- myTimerGauge:start(false) --starts the timer, will not change hidden status, regardless of autoShow property
|
||||||
|
-- myTimerGauge:start(true) --starts the timer, will show it regardless of autoShow property
|
||||||
|
function TimerGauge:start(show)
|
||||||
|
if show == nil then
|
||||||
|
show = self.autoShow
|
||||||
|
end
|
||||||
|
self.active = true
|
||||||
|
if self.timer then
|
||||||
|
killTimer(self.timer)
|
||||||
|
self.timer = nil
|
||||||
|
end
|
||||||
|
startStopWatch(self.stopWatchName)
|
||||||
|
self:update()
|
||||||
|
self.timer = tempTimer(self.updateTime / 1000, function()
|
||||||
|
self:update()
|
||||||
|
end, true)
|
||||||
|
if show then
|
||||||
|
self:show2()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Stops the timergauge. Works whether the timer is started or not.
|
||||||
|
-- @tparam[opt] boolean hide override the autoHide property. True will always hide, false will never hide.
|
||||||
|
-- @usage myTimerGauge:stop() --stops the timer, will hide or not based on autoHide property
|
||||||
|
-- myTimerGauge:stop(false) --stops the timer, will not change hidden status, regardless of autoHide property
|
||||||
|
-- myTimerGauge:stop(true) --stops the timer, will hide it regardless of autoHide property
|
||||||
|
function TimerGauge:stop(hide)
|
||||||
|
if hide == nil then
|
||||||
|
hide = self.autoHide
|
||||||
|
end
|
||||||
|
self.active = false
|
||||||
|
if self.timer then
|
||||||
|
killTimer(self.timer)
|
||||||
|
self.timer = nil
|
||||||
|
end
|
||||||
|
stopStopWatch(self.stopWatchName)
|
||||||
|
if hide then
|
||||||
|
self:hide2()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Alias for stop.
|
||||||
|
-- @tparam[opt] boolean hide override the autoHide property. True will always hide, false will never hide.
|
||||||
|
function TimerGauge:pause(hide)
|
||||||
|
self:stop(hide)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Resets the time on the timergauge to its original value. Does not alter the running state of the timer
|
||||||
|
function TimerGauge:reset()
|
||||||
|
resetStopWatch(self.stopWatchName)
|
||||||
|
adjustStopWatch(self.stopWatchName, self.time * -1)
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Resets and starts the timergauge.
|
||||||
|
-- @tparam[opt] boolean show override the autoShow property. true will always show, false will never show
|
||||||
|
-- @usage myTimerGauge:restart() --restarts the timer, will show or not based on autoShow property
|
||||||
|
-- myTimerGauge:restart(false) --restarts the timer, will not change hidden status, regardless of autoShow property
|
||||||
|
-- myTimerGauge:restart(true) --restarts the timer, will show it regardless of autoShow property
|
||||||
|
function TimerGauge:restart(show)
|
||||||
|
self:reset()
|
||||||
|
self:start(show)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the amount of time remaining on the timer, in seconds
|
||||||
|
-- @tparam string format Format string for how to return the time. If not provided defaults to self.timeFormat(which defaults to "S.t").<br>
|
||||||
|
-- If "" is passed will return "" as the time. See below table for formatting codes<br>
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <tr>
|
||||||
|
-- <th>format code</th>
|
||||||
|
-- <th>what it is replaced with</th>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">S</td>
|
||||||
|
-- <td class="tg-1">Time left in seconds, unbroken down. Does not include milliseconds.<br>
|
||||||
|
-- IE a timer with 2 minutes left it would replace S with 120
|
||||||
|
-- </td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">dd</td>
|
||||||
|
-- <td class="tg-2">Days, with 1 leading 0 (0, 01, 02-...)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">d</td>
|
||||||
|
-- <td class="tg-1">Days, with no leading 0 (1,2,3-...)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">hh</td>
|
||||||
|
-- <td class="tg-2">hours, with leading 0 (00-24)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">h</td>
|
||||||
|
-- <td class="tg-1">hours, without leading 0 (0-24)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">MM</td>
|
||||||
|
-- <td class="tg-2">minutes, with a leading 0 (00-59)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">M</td>
|
||||||
|
-- <td class="tg-1">minutes, no leading 0 (0-59)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">ss</td>
|
||||||
|
-- <td class="tg-2">seconds, with leading 0 (00-59)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">s</td>
|
||||||
|
-- <td class="tg-1">seconds, no leading 0 (0-59)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">t</td>
|
||||||
|
-- <td class="tg-2">tenths of a second<br>
|
||||||
|
-- timer with 12.345 seconds left, t would<br>
|
||||||
|
-- br replaced by 3.
|
||||||
|
-- </td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">mm</td>
|
||||||
|
-- <td class="tg-1">milliseconds with leadings 0s (000-999)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">m</td>
|
||||||
|
-- <td class="tg-2">milliseconds with no leading 0s (0-999)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- </table><br>
|
||||||
|
-- @usage myTimerGauge:getTime() --returns the time using myTimerGauge.format
|
||||||
|
-- myTimerGauge:getTime("hh:MM:ss") --returns the time as hours, minutes, and seconds, with leading 0s (01:23:04)
|
||||||
|
-- myTimerGauge:getTime("S.mm") --returns the time as the total number of seconds, including milliseconds (114.004)
|
||||||
|
function TimerGauge:getTime(format)
|
||||||
|
format = format or self.timeFormat
|
||||||
|
local time = getStopWatchTime(self.stopWatchName)
|
||||||
|
local timerTable = getStopWatchBrokenDownTime(self.stopWatchName)
|
||||||
|
if time > 0 then
|
||||||
|
self:stop(self.autoHide)
|
||||||
|
resetStopWatch(self.stopWatchName)
|
||||||
|
time = getStopWatchTime(self.stopWatchName)
|
||||||
|
timerTable = getStopWatchBrokenDownTime(self.stopWatchName)
|
||||||
|
self.active = false
|
||||||
|
end
|
||||||
|
if format == "" then
|
||||||
|
return format
|
||||||
|
end
|
||||||
|
local totalSeconds = string.split(math.abs(time), "%.")[1]
|
||||||
|
local tenths = string.sub(string.format("%03d", timerTable.milliSeconds), 1, 1)
|
||||||
|
format = format:gsub("S", totalSeconds)
|
||||||
|
format = format:gsub("t", tenths)
|
||||||
|
format = format:gsub("mm", string.format("%03d", timerTable.milliSeconds))
|
||||||
|
format = format:gsub("m", timerTable.milliSeconds)
|
||||||
|
format = format:gsub("MM", string.format("%02d", timerTable.minutes))
|
||||||
|
format = format:gsub("M", timerTable.minutes)
|
||||||
|
format = format:gsub("dd", string.format("%02d", timerTable.days))
|
||||||
|
format = format:gsub("d", timerTable.days)
|
||||||
|
format = format:gsub("ss", string.format("%02d", timerTable.seconds))
|
||||||
|
format = format:gsub("s", timerTable.seconds)
|
||||||
|
format = format:gsub("hh", string.format("%02d", timerTable.hours))
|
||||||
|
format = format:gsub("h", timerTable.hours)
|
||||||
|
return format
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Execute the timer's hook, if there is one. Internal function
|
||||||
|
function TimerGauge:executeHook()
|
||||||
|
local hook = self.hook
|
||||||
|
if not hook then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local hooktype = type(hook)
|
||||||
|
if hooktype == "string" then
|
||||||
|
local f, e = loadstring("return " .. hook)
|
||||||
|
if not f then
|
||||||
|
f, e = loadstring(hook)
|
||||||
|
end
|
||||||
|
if not f then
|
||||||
|
debugc(string.format("TimerGauge encountered an error while executing the hook for TimerGauge with name: %s error: %s", self.name, tostring(e)))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
hook = f
|
||||||
|
end
|
||||||
|
hooktype = type(hook)
|
||||||
|
if hooktype ~= "function" then
|
||||||
|
debugc(string.format(
|
||||||
|
"TimerGauge with name: %s was given a hook which is neither a function nor a string which can be made into one. Provided type was %s",
|
||||||
|
self.name, hooktype))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local worked, err = pcall(hook)
|
||||||
|
if not worked then
|
||||||
|
debugc(string.format("TimerGauge named %s encountered the following error while executing its hook: %s", self.name, err))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the timer's remaining time to 0, stops it, and executes the hook if one exists.
|
||||||
|
-- @tparam[opt] boolean skipHook use true to have it set the timer to 0 and stop, but not execute the hook.
|
||||||
|
-- @usage myTimerGauge:finish() --executes the hook if it has one
|
||||||
|
-- myTimerGauge:finish(false) --will not execute the hook
|
||||||
|
function TimerGauge:finish(skipHook)
|
||||||
|
resetStopWatch(self.stopWatchName)
|
||||||
|
self:update(skipHook)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Internal function, no ldoc
|
||||||
|
-- Updates the gauge based on time remaining.
|
||||||
|
-- @tparam[opt] boolean skipHook use true if you do not want to execute the hook if the timer is at 0.
|
||||||
|
function TimerGauge:update(skipHook)
|
||||||
|
local time = self.showTime and self:getTime(self.timeFormat) or ""
|
||||||
|
local current = tonumber(self:getTime("S.mm"))
|
||||||
|
local suffix = self.suffix or ""
|
||||||
|
local prefix = self.prefix or ""
|
||||||
|
local text = string.format("%s%s%s", prefix, time, suffix)
|
||||||
|
self:setValue(current, self.time, text)
|
||||||
|
if current == 0 then
|
||||||
|
if self.timer then
|
||||||
|
killTimer(self.timer)
|
||||||
|
self.timer = nil
|
||||||
|
end
|
||||||
|
if not skipHook then
|
||||||
|
self:executeHook()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the amount of time the timer will run for. Make sure to call :reset() or :restart()
|
||||||
|
-- if you want to cause the timer to run for that amount of time. If you set it to a time lower
|
||||||
|
-- than the time left on the timer currently, it will reset the current time, otherwise it is left alone
|
||||||
|
-- @tparam number time how long in seconds the timer should run for
|
||||||
|
-- @usage myTimerGauge:setTime(50) -- sets myTimerGauge's max time to 50.
|
||||||
|
function TimerGauge:setTime(time)
|
||||||
|
local timetype = type(time)
|
||||||
|
if timetype ~= "number" then
|
||||||
|
local err = string.format("TimerGauge:setTime(time): time as number expected, got %s", timetype)
|
||||||
|
debugc(err)
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
time = math.abs(time)
|
||||||
|
if time == 0 then
|
||||||
|
local err = "TimerGauge:setTime(time): you cannot pass in 0 as the max time for the timer"
|
||||||
|
debugc(err)
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
local currentTime = tonumber(self:getTime("S.t"))
|
||||||
|
self.time = time
|
||||||
|
if time < currentTime then
|
||||||
|
self:reset()
|
||||||
|
else
|
||||||
|
self:update(currentTime == 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Changes the time between gauge updates.
|
||||||
|
-- @tparam number updateTime amount of time in milliseconds between gauge updates. Must be a positive number.
|
||||||
|
function TimerGauge:setUpdateTime(updateTime)
|
||||||
|
local updateTimeType = type(updateTime)
|
||||||
|
assert(updateTimeType == "number",
|
||||||
|
string.format("TimerGauge:setUpdateTime(updateTime): name: %s updateTime as number expected, got %s", self.name, updateTimeType))
|
||||||
|
assert(updateTime > 0,
|
||||||
|
string.format("TimerGauge:setUpdateTime(updateTime): name: %s updateTime must be a positive number. You gave %d", self.name, updateTime))
|
||||||
|
self.updateTime = updateTime
|
||||||
|
if self.timer then
|
||||||
|
killTimer(self.timer)
|
||||||
|
self.timer = nil
|
||||||
|
end
|
||||||
|
if self.active then
|
||||||
|
self.timer = tempTimer(updateTime / 1000, function()
|
||||||
|
self:update()
|
||||||
|
end, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
TimerGauge.parent = Geyser.Gauge
|
||||||
|
setmetatable(TimerGauge, Geyser.Gauge)
|
||||||
|
--- Creates a new TimerGauge instance.
|
||||||
|
-- @tparam table cons a table of options (or constraints) for how the TimerGauge will behave. Valid options include:
|
||||||
|
-- <br>
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <tr>
|
||||||
|
-- <th>name</th>
|
||||||
|
-- <th>description</th>
|
||||||
|
-- <th>default</th>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">time</td>
|
||||||
|
-- <td class="tg-1">how long the timer should run for</td>
|
||||||
|
-- <td class="tg-1"></td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">active</td>
|
||||||
|
-- <td class="tg-2">whether the timer should run or not</td>
|
||||||
|
-- <td class="tg-2">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">showTime</td>
|
||||||
|
-- <td class="tg-1">should we show the time remaining on the gauge?</td>
|
||||||
|
-- <td class="tg-1">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">prefix</td>
|
||||||
|
-- <td class="tg-2">text you want shown before the time.</td>
|
||||||
|
-- <td class="tg-2">""</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">suffix</td>
|
||||||
|
-- <td class="tg-1">text you want shown after the time.</td>
|
||||||
|
-- <td class="tg-1">""</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">timerCaption</td>
|
||||||
|
-- <td class="tg-2">Alias for suffix. Deprecated and may be remove in the future</td>
|
||||||
|
-- <td class="tg-2"/>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">updateTime</td>
|
||||||
|
-- <td class="tg-1">number of milliseconds between gauge updates.</td>
|
||||||
|
-- <td class="tg-1">10</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">autoHide</td>
|
||||||
|
-- <td class="tg-2">should the timer :hide() itself when it runs out/you stop it?</td>
|
||||||
|
-- <td class="tg-2">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">autoShow</td>
|
||||||
|
-- <td class="tg-1">should the timer :show() itself when you start it?</td>
|
||||||
|
-- <td class="tg-1">true</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">manageContainer</td>
|
||||||
|
-- <td class="tg-2">should the timer remove itself from its container when you call <br>:hide() and add itself back when you call :show()?</td>
|
||||||
|
-- <td class="tg-2">false</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">timeFormat</td>
|
||||||
|
-- <td class="tg-1">how should the time be displayed/returned if you call :getTime()? <br>See table below for more information</td>
|
||||||
|
-- <td class="tg-1">"S.t"</td>
|
||||||
|
-- </tr>
|
||||||
|
-- </table>
|
||||||
|
-- <br>Table of time format options
|
||||||
|
-- <table class="tg">
|
||||||
|
-- <tr>
|
||||||
|
-- <th>format code</th>
|
||||||
|
-- <th>what it is replaced with</th>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">S</td>
|
||||||
|
-- <td class="tg-1">Time left in seconds, unbroken down. Does not include milliseconds.<br>
|
||||||
|
-- IE a timer with 2 minutes left it would replace S with 120
|
||||||
|
-- </td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">dd</td>
|
||||||
|
-- <td class="tg-2">Days, with 1 leading 0 (0, 01, 02-...)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">d</td>
|
||||||
|
-- <td class="tg-1">Days, with no leading 0 (1,2,3-...)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">hh</td>
|
||||||
|
-- <td class="tg-2">hours, with leading 0 (00-24)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">h</td>
|
||||||
|
-- <td class="tg-1">hours, without leading 0 (0-24)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">MM</td>
|
||||||
|
-- <td class="tg-2">minutes, with a leading 0 (00-59)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">M</td>
|
||||||
|
-- <td class="tg-1">minutes, no leading 0 (0-59)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">ss</td>
|
||||||
|
-- <td class="tg-2">seconds, with leading 0 (00-59)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">s</td>
|
||||||
|
-- <td class="tg-1">seconds, no leading 0 (0-59)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">t</td>
|
||||||
|
-- <td class="tg-2">tenths of a second<br>
|
||||||
|
-- timer with 12.345 seconds left, t would<br>
|
||||||
|
-- br replaced by 3.
|
||||||
|
-- </td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-1">mm</td>
|
||||||
|
-- <td class="tg-1">milliseconds with leadings 0s (000-999)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- <tr>
|
||||||
|
-- <td class="tg-2">m</td>
|
||||||
|
-- <td class="tg-2">milliseconds with no leading 0s (0-999)</td>
|
||||||
|
-- </tr>
|
||||||
|
-- </table><br>
|
||||||
|
-- @param parent The Geyser parent for this TimerGauge
|
||||||
|
-- @usage
|
||||||
|
-- local TimerGauge = require("MDK.timergauge")
|
||||||
|
-- myTimerGauge = TimerGauge:new({
|
||||||
|
-- name = "testGauge",
|
||||||
|
-- x = 100,
|
||||||
|
-- y = 100,
|
||||||
|
-- height = 40,
|
||||||
|
-- width = 200,
|
||||||
|
-- time = 10
|
||||||
|
-- })
|
||||||
|
function TimerGauge:new(cons, parent)
|
||||||
|
-- type checking and error handling
|
||||||
|
local consType = type(cons)
|
||||||
|
if consType ~= "table" then
|
||||||
|
local err = string.format("TimerGauge:new(options, parent): options must be provided as a table, received: %s", consType)
|
||||||
|
debugc(err)
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
local timetype = type(cons.time)
|
||||||
|
local time = tonumber(cons.time)
|
||||||
|
if not time then
|
||||||
|
local err = string.format(
|
||||||
|
"TimerGauge:new(options, parent): options table must include a time entry, which must be a number. We received: %s which is type: %s",
|
||||||
|
cons.time or tostring(cons.time), timetype)
|
||||||
|
debugc(err)
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
cons.time = math.abs(time)
|
||||||
|
if cons.time == 0 then
|
||||||
|
local err = "TimerGauge:new(options, parent): time entry in options table must be non-0"
|
||||||
|
debugc(err)
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
|
||||||
|
if cons.timerCaption and (not cons.suffix) then
|
||||||
|
cons.suffix = cons.timerCaption
|
||||||
|
end
|
||||||
|
cons.type = cons.type or "timergauge"
|
||||||
|
-- call parent constructor
|
||||||
|
local me = self.parent:new(cons, parent)
|
||||||
|
-- add TimerGauge as the metatable/index
|
||||||
|
setmetatable(me, self)
|
||||||
|
self.__index = self
|
||||||
|
|
||||||
|
-- apply any styling requested
|
||||||
|
if me.cssFront then
|
||||||
|
if not me.cssBack then
|
||||||
|
me.cssBack = me.cssFront .. "background-color: black;"
|
||||||
|
end
|
||||||
|
me:setStyleSheet(me.cssFront, me.cssBack, me.cssText)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create and reset the driving stopwatch
|
||||||
|
me.stopWatchName = me.name .. "_timergauge"
|
||||||
|
createStopWatch(me.stopWatchName)
|
||||||
|
me:reset()
|
||||||
|
|
||||||
|
-- start it up?
|
||||||
|
if me.active then
|
||||||
|
me:start()
|
||||||
|
end
|
||||||
|
me:update()
|
||||||
|
return me
|
||||||
|
end
|
||||||
|
|
||||||
|
return TimerGauge
|
33
src/scripts/autostudy/autostudy.lua
Normal file
33
src/scripts/autostudy/autostudy.lua
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
function autoStudyStartup()
|
||||||
|
cecho("\n<dodger_blue>Autostudy loaded. 'studyhelp' for more info.")
|
||||||
|
end
|
||||||
|
|
||||||
|
function autostudy.checkNextMove(index)
|
||||||
|
if studyIndex <= 4 then
|
||||||
|
return "w"
|
||||||
|
elseif studyIndex == 5 then
|
||||||
|
return "s"
|
||||||
|
elseif studyIndex <= 7 then
|
||||||
|
return "se"
|
||||||
|
elseif studyIndex == 8 then
|
||||||
|
return "e"
|
||||||
|
elseif studyIndex == 9 then
|
||||||
|
return "n"
|
||||||
|
elseif studyIndex == 10 then
|
||||||
|
return "ne"
|
||||||
|
elseif studyIndex == 11 then
|
||||||
|
return "se"
|
||||||
|
elseif studyIndex == 12 then
|
||||||
|
return "s"
|
||||||
|
elseif studyIndex == 13 then
|
||||||
|
return "e"
|
||||||
|
elseif studyIndex <= 15 then
|
||||||
|
return "ne"
|
||||||
|
elseif studyIndex == 16 then
|
||||||
|
return "n"
|
||||||
|
else
|
||||||
|
return "w"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
registerAnonymousEventHandler("sysLoadEvent", "autoStudyStartup")
|
5
src/scripts/autostudy/scripts.json
Normal file
5
src/scripts/autostudy/scripts.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "autostudy"
|
||||||
|
}
|
||||||
|
]
|
@ -227,6 +227,7 @@ function lotj.mapper.startMapping(areaName)
|
|||||||
end
|
end
|
||||||
|
|
||||||
lotj.mapper.mappingArea = areaName
|
lotj.mapper.mappingArea = areaName
|
||||||
|
lotj.mapper.lastMoveDirs = {}
|
||||||
lotj.mapper.processCurrentRoom()
|
lotj.mapper.processCurrentRoom()
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -344,8 +345,11 @@ function lotj.mapper.setup()
|
|||||||
loadMap(getMudletHomeDir().."/@PKGNAME@/starter-map.dat")
|
loadMap(getMudletHomeDir().."/@PKGNAME@/starter-map.dat")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
lotj.mapper.loadShipData()
|
||||||
|
|
||||||
lotj.setup.registerEventHandler("sysDataSendRequest", lotj.mapper.handleSentCommand)
|
lotj.setup.registerEventHandler("sysDataSendRequest", lotj.mapper.handleSentCommand)
|
||||||
lotj.setup.registerEventHandler("gmcp.Room.Info", lotj.mapper.onEnterRoom)
|
lotj.setup.registerEventHandler("gmcp.Room.Info", lotj.mapper.onEnterRoom)
|
||||||
|
lotj.setup.registerEventHandler("sysExitEvent", lotj.mapper.saveShipData)
|
||||||
end
|
end
|
||||||
|
|
||||||
function lotj.mapper.teardown()
|
function lotj.mapper.teardown()
|
||||||
@ -353,11 +357,28 @@ function lotj.mapper.teardown()
|
|||||||
geyserMapper:hide()
|
geyserMapper:hide()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function lotj.mapper.loadShipData()
|
||||||
|
local location = getMudletHomeDir() .. "/lotjmapper_ship.lua"
|
||||||
|
lotj.mapper.shipLastRoom = lotj.mapper.shipLastRoom or {}
|
||||||
|
if io.exists(location) then table.load(location, lotj.mapper.shipLastRoom) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lotj.mapper.saveShipData()
|
||||||
|
if lotj.mapper.shipLastRoom then
|
||||||
|
local location = getMudletHomeDir() .. "/lotjmapper_ship.lua"
|
||||||
|
table.save(location, lotj.mapper.shipLastRoom)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Track the most recent movement command so we know which direction we moved when automapping
|
-- Track the most recent movement command so we know which direction we moved when automapping
|
||||||
function lotj.mapper.handleSentCommand(event, cmd)
|
function lotj.mapper.handleSentCommand(event, cmd)
|
||||||
-- If we're not mapping, don't bother
|
-- If we're not mapping, store the last direction only for ships
|
||||||
if lotj.mapper.mappingArea == nil then
|
if lotj.mapper.mappingArea == nil then
|
||||||
|
if not gmcp.Room.Info.planet then
|
||||||
|
-- only store movement if we're actually on a ship
|
||||||
|
lotj.mapper.shipMovement = lotj.mapper.shipMovement or {}
|
||||||
|
table.insert(lotj.mapper.shipMovement, dirObj(trim(cmd)))
|
||||||
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -380,6 +401,32 @@ function lotj.mapper.popMoveDir()
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Function used to handle virtual ship maps. This will process the room
|
||||||
|
-- as a ship and attempt to position a player on an existing map of the same ship.
|
||||||
|
function lotj.mapper.processCurrentRoomAsShip(roomData, movement)
|
||||||
|
local roomVnum = table.keys(roomData)[1]
|
||||||
|
local matchesMany = #table.keys(roomData)
|
||||||
|
|
||||||
|
if matchesMany == 1 then
|
||||||
|
-- Only one room matched.
|
||||||
|
lotj.mapper.shipLastRoom = {actual = gmcp.Room.Info.vnum, virtual = roomVnum}
|
||||||
|
centerview(roomVnum)
|
||||||
|
elseif matchesMany > 1 then
|
||||||
|
-- Multiple matches
|
||||||
|
if lotj.mapper.shipLastRoom ~= nil and lotj.mapper.shipLastRoom.actual == gmcp.Room.Info.vnum then
|
||||||
|
-- This is likely a reboot. The last room and the current room match.
|
||||||
|
centerview(lotj.mapper.shipLastRoom.virtual)
|
||||||
|
elseif lotj.mapper.shipLastRoom ~= nil and movement then
|
||||||
|
-- position based on movement
|
||||||
|
local nextRoom = getRoomExits(lotj.mapper.shipLastRoom.virtual)
|
||||||
|
if table.contains(nextRoom, movement.long) then
|
||||||
|
lotj.mapper.shipLastRoom = {acutal = gmcp.Room.Info.vnum, virtual = nextRoom[movement.long]}
|
||||||
|
centerview(lotj.mapper.shipLastRoom.virtual)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
-- Function used to handle a room that we've moved into. This will use the data on
|
-- Function used to handle a room that we've moved into. This will use the data on
|
||||||
-- lotj.mapper.current, compared with lotj.mapper.last, to potentially create a new room and
|
-- lotj.mapper.current, compared with lotj.mapper.last, to potentially create a new room and
|
||||||
@ -388,6 +435,25 @@ function lotj.mapper.processCurrentRoom()
|
|||||||
local vnum = lotj.mapper.current.vnum
|
local vnum = lotj.mapper.current.vnum
|
||||||
local moveDir = lotj.mapper.popMoveDir()
|
local moveDir = lotj.mapper.popMoveDir()
|
||||||
local room = lotj.mapper.getRoomByVnum(vnum)
|
local room = lotj.mapper.getRoomByVnum(vnum)
|
||||||
|
local searchData = searchRoom(gmcp.Room.Info.name, false, true)
|
||||||
|
|
||||||
|
-- Only virtualize ships when we aren't actively mapping
|
||||||
|
if not gmcp.Room.Info.planet and lotj.mapper.mappingArea == nil then
|
||||||
|
if room == nil then
|
||||||
|
-- Don't virtualize if it's actually mapped
|
||||||
|
if not table.is_empty(searchData) then
|
||||||
|
if lotj.mapper.shipMovement then
|
||||||
|
lotj.mapper.processCurrentRoomAsShip(searchData, table.remove(lotj.mapper.shipMovement,1))
|
||||||
|
else
|
||||||
|
lotj.mapper.processCurrentRoomAsShip(searchData, nil)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- if we're not virtualizing, make sure this is empty
|
||||||
|
lotj.mapper.shipMovement = {}
|
||||||
|
end
|
||||||
|
|
||||||
if lotj.mapper.mappingArea == nil and room == nil then
|
if lotj.mapper.mappingArea == nil and room == nil then
|
||||||
lotj.mapper.logDebug("Room not found, but mapper not running.")
|
lotj.mapper.logDebug("Room not found, but mapper not running.")
|
||||||
@ -399,6 +465,26 @@ function lotj.mapper.processCurrentRoom()
|
|||||||
lastRoom = lotj.mapper.getRoomByVnum(lotj.mapper.last.vnum)
|
lastRoom = lotj.mapper.getRoomByVnum(lotj.mapper.last.vnum)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Try to account for moving between visible and non-visible rooms
|
||||||
|
if moveDir ~= nil then
|
||||||
|
if not table.contains(gmcp.Room.Info.exits, revDirObj(moveDir.long).long) then
|
||||||
|
-- There was no return exit in this room matching the movement
|
||||||
|
if not table.is_empty(lotj.mapper.lastMoveDirs) then
|
||||||
|
-- There are additional movements in the table so test those
|
||||||
|
while not table.is_empty(lotj.mapper.lastMoveDirs) do
|
||||||
|
local tempDir = lotj.mapper.popMoveDir()
|
||||||
|
|
||||||
|
if table.contains(gmcp.Room.Info.exits, revDirObj(tempDir.long).long) then
|
||||||
|
-- This seems to be a match so use this one and empty out the last room as it is incorrect
|
||||||
|
moveDir = tempDir
|
||||||
|
lotj.mapper.last = nil
|
||||||
|
lastRoom = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Create the room if we don't have it yet
|
-- Create the room if we don't have it yet
|
||||||
if room == nil then
|
if room == nil then
|
||||||
lotj.mapper.log("Added new room: <yellow>"..lotj.mapper.current.name.."<reset>")
|
lotj.mapper.log("Added new room: <yellow>"..lotj.mapper.current.name.."<reset>")
|
||||||
|
5
src/scripts/slayn/scripts.json
Normal file
5
src/scripts/slayn/scripts.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "slaynBase"
|
||||||
|
}
|
||||||
|
]
|
35
src/scripts/slayn/slaynBase.lua
Normal file
35
src/scripts/slayn/slaynBase.lua
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
slayn = slayn or {}
|
||||||
|
slayn.events = slayn.events or {}
|
||||||
|
slayn.engineering = slayn.engineering or {}
|
||||||
|
slayn.combat = slayn.combat or {}
|
||||||
|
|
||||||
|
function slayn.setup()
|
||||||
|
end
|
||||||
|
|
||||||
|
function slayn.tearDown()
|
||||||
|
|
||||||
|
for _, pid in ipairs(slayn.events) do
|
||||||
|
killAnonymousEventHandler(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
slayn = nil
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
function slayn.installed()
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(
|
||||||
|
slayn.events,
|
||||||
|
registerAnonymousEventHandler("sysLoadEvent", "slayn.setup")
|
||||||
|
)
|
||||||
|
|
||||||
|
table.insert(
|
||||||
|
slayn.events,
|
||||||
|
registerAnonymousEventHandler("sysInstallPackage", "slayn.installed")
|
||||||
|
)
|
||||||
|
|
||||||
|
table.insert(
|
||||||
|
slayn.events,
|
||||||
|
registerAnonymousEventHandler("sysUninstallPackage", "slayn.tearDown")
|
||||||
|
)
|
@ -33,8 +33,8 @@
|
|||||||
"name": "autoresearch.grabSkills.featsLine",
|
"name": "autoresearch.grabSkills.featsLine",
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"pattern": "^-+ Feats -+$",
|
"pattern": "To see a shorter practice list, type PRACTICE <class name>.",
|
||||||
"type": "regex"
|
"type": "startOfLine"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"script": "lotj.autoResearch.startOnPracticeEnd = true; disableTrigger(\"autoresearch.grabSkills\")"
|
"script": "lotj.autoResearch.startOnPracticeEnd = true; disableTrigger(\"autoresearch.grabSkills\")"
|
||||||
|
3
src/triggers/autostudy/study.botStart.lua
Normal file
3
src/triggers/autostudy/study.botStart.lua
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
send("afk")
|
||||||
|
send("bot start")
|
||||||
|
send("study " .. studyList[studyIndex])
|
1
src/triggers/autostudy/study.copyOver.lua
Normal file
1
src/triggers/autostudy/study.copyOver.lua
Normal file
@ -0,0 +1 @@
|
|||||||
|
send("study " .. studyList[studyIndex])
|
1
src/triggers/autostudy/study.lua
Normal file
1
src/triggers/autostudy/study.lua
Normal file
@ -0,0 +1 @@
|
|||||||
|
send("study " .. studyList[studyIndex])
|
4
src/triggers/autostudy/study.next.lua
Normal file
4
src/triggers/autostudy/study.next.lua
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
studyIndex = studyIndex + 1
|
||||||
|
|
||||||
|
send(autostudy.checkNextMove(studyIndex))
|
||||||
|
send("study " .. studyList[studyIndex])
|
1
src/triggers/autostudy/study.nextMove.lua
Normal file
1
src/triggers/autostudy/study.nextMove.lua
Normal file
@ -0,0 +1 @@
|
|||||||
|
send(autostudy.checkNextMove(studyIndex))
|
56
src/triggers/autostudy/triggers.json
Normal file
56
src/triggers/autostudy/triggers.json
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "study",
|
||||||
|
"isActive": "no",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "You study it for some time,",
|
||||||
|
"type": "startOfLine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "After some time studying",
|
||||||
|
"type": "startOfLine"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "study.next",
|
||||||
|
"isActive": "no",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "^You are now an adept of (?!study)",
|
||||||
|
"type": "regex"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "study.botStart",
|
||||||
|
"isActive": "no",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "You may now bot again.",
|
||||||
|
"type": "exactMatch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "study.nextMove",
|
||||||
|
"isActive": "no",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "You don't see anything like that nearby to study.",
|
||||||
|
"type": "exactMatch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "study.copyOver",
|
||||||
|
"isActive": "no",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "Copyover recovery complete.",
|
||||||
|
"type": "exactMatch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
3
src/triggers/combat/antidisarm.lua
Normal file
3
src/triggers/combat/antidisarm.lua
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
send("get all")
|
||||||
|
send("wield pistol")
|
||||||
|
send("wield pistol")
|
23
src/triggers/combat/triggers.json
Normal file
23
src/triggers/combat/triggers.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "antidisarm",
|
||||||
|
"isActive": "yes",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "DISARMS you!",
|
||||||
|
"type": "substring"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "antigrapple",
|
||||||
|
"isActive": "yes",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "grabs ahold of you!",
|
||||||
|
"type": "substring"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"command": "struggle"
|
||||||
|
}
|
||||||
|
]
|
2
src/triggers/skill-training/CloseHatch.lua
Normal file
2
src/triggers/skill-training/CloseHatch.lua
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
send("close " .. pickShip)
|
||||||
|
send("pick " .. pickShip)
|
2
src/triggers/skill-training/DecryptFile.lua
Normal file
2
src/triggers/skill-training/DecryptFile.lua
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
edStage = "decrypt_file self"
|
||||||
|
send(edStage)
|
2
src/triggers/skill-training/EncryptFile.lua
Normal file
2
src/triggers/skill-training/EncryptFile.lua
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
edStage = "encrypt_file self"
|
||||||
|
send(edStage)
|
1
src/triggers/skill-training/PickLock.lua
Normal file
1
src/triggers/skill-training/PickLock.lua
Normal file
@ -0,0 +1 @@
|
|||||||
|
send("pick " .. pickShip)
|
2
src/triggers/skill-training/Recharge.lua
Normal file
2
src/triggers/skill-training/Recharge.lua
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
send("rem datapad")
|
||||||
|
send("energize datapad battery")
|
2
src/triggers/skill-training/Recharged.lua
Normal file
2
src/triggers/skill-training/Recharged.lua
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
send("hold datapad")
|
||||||
|
send(edStage)
|
1
src/triggers/skill-training/SkillFailure.lua
Normal file
1
src/triggers/skill-training/SkillFailure.lua
Normal file
@ -0,0 +1 @@
|
|||||||
|
send(edStage)
|
94
src/triggers/skill-training/triggers.json
Normal file
94
src/triggers/skill-training/triggers.json
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "SliceSecure",
|
||||||
|
"isActive": "no",
|
||||||
|
"isFolder": "yes",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "SkillFailure",
|
||||||
|
"isActive": "yes",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "You fail to call up the right info on the datapad.",
|
||||||
|
"type": "startOfLine"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DecryptFile",
|
||||||
|
"isActive": "yes",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "File encrypted to rating",
|
||||||
|
"type": "startOfLine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "After all that, you fail to decrypt the file.",
|
||||||
|
"type": "exactMatch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "EncryptFile",
|
||||||
|
"isActive": "yes",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "File Decrypted",
|
||||||
|
"type": "startOfLine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "After all that, you fail to encrypt the file.",
|
||||||
|
"type": "exactMatch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Recharge",
|
||||||
|
"isActive": "yes",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "Your datapad is out of charge.",
|
||||||
|
"type": "exactMatch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Recharged",
|
||||||
|
"isActive": "yes",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "You successfully recharge your datapad",
|
||||||
|
"type": "startOfLine"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PickShipLock",
|
||||||
|
"isActive": "no",
|
||||||
|
"isFolder": "yes",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "PickLock",
|
||||||
|
"isActive": "yes",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "You failed to pick the lock.",
|
||||||
|
"type": "exactMatch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CloseHatch",
|
||||||
|
"isActive": "yes",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"pattern": "You pick the lock and open the hatch",
|
||||||
|
"type": "startOfLine"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user