Compare commits

..

4 Commits
v1.2 ... main

Author SHA1 Message Date
1651a53528
Rearrange files. [SKIP CI] 2024-10-11 08:50:10 +00:00
c36e812c03
Further Work on 2.4
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
* Added more trigger groups
* Initial version of slayn scripts
* Allow CI to run this time to verify if folder structure works
2024-10-11 08:41:55 +00:00
954dc3c607 * Add Demonic MDK.
* Inject autostudy plugin into the UI directly.
* Add a TLC automator for study.
[SKIP CI] Do not run CI for this.
2024-10-09 23:12:21 +00:00
0ebb9e6007
Just a minor readme change.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-03-30 12:08:48 -04:00
54 changed files with 11626 additions and 71 deletions

View File

@ -199,3 +199,5 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -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)
## Features
### 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.
* Virtual Ship Maps
* Integrated Autostudy
* TLC Automator
* Engineering Suite
* Skill Training Suite

2
mfile
View File

@ -1,4 +1,4 @@
{
"package": "lotj-ui",
"version": "v2.3.3"
"version": "v2.4"
}

View 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$"
}
]

View File

@ -0,0 +1,3 @@
studyList = studyList or {}
table.insert(studyList, matches[2])
cecho(matches[2] .. "<dodger_blue> added.")

View 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")

View File

@ -0,0 +1,5 @@
studyList = []
studyIndex = 0
disableTrigger("autostudy")
cecho("<dodger_blue>Cleared list of study items.")

View File

View 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())

View 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

View 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
View 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
View 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
View 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
View 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('&lt;' .. displayString .. '&gt;')
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

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
View 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

File diff suppressed because it is too large Load Diff

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
View 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)

View 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

View 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
View 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

View 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

View File

@ -0,0 +1 @@
2.10.0

View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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")

View File

@ -0,0 +1,5 @@
[
{
"name": "autostudy"
}
]

View File

@ -0,0 +1,5 @@
[
{
"name": "slaynBase"
}
]

View 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")
)

View File

@ -33,8 +33,8 @@
"name": "autoresearch.grabSkills.featsLine",
"patterns": [
{
"pattern": "^-+ Feats -+$",
"type": "regex"
"pattern": "To see a shorter practice list, type PRACTICE <class name>.",
"type": "startOfLine"
}
],
"script": "lotj.autoResearch.startOnPracticeEnd = true; disableTrigger(\"autoresearch.grabSkills\")"

View File

@ -0,0 +1,3 @@
send("afk")
send("bot start")
send("study " .. studyList[studyIndex])

View File

@ -0,0 +1 @@
send("study " .. studyList[studyIndex])

View File

@ -0,0 +1 @@
send("study " .. studyList[studyIndex])

View File

@ -0,0 +1,4 @@
studyIndex = studyIndex + 1
send(autostudy.checkNextMove(studyIndex))
send("study " .. studyList[studyIndex])

View File

@ -0,0 +1 @@
send(autostudy.checkNextMove(studyIndex))

View 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"
}
]
}
]

View File

@ -0,0 +1,3 @@
send("get all")
send("wield pistol")
send("wield pistol")

View 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"
}
]

View File

@ -0,0 +1,2 @@
send("close " .. pickShip)
send("pick " .. pickShip)

View File

@ -0,0 +1,2 @@
edStage = "decrypt_file self"
send(edStage)

View File

@ -0,0 +1,2 @@
edStage = "encrypt_file self"
send(edStage)

View File

@ -0,0 +1 @@
send("pick " .. pickShip)

View File

@ -0,0 +1,2 @@
send("rem datapad")
send("energize datapad battery")

View File

@ -0,0 +1,2 @@
send("hold datapad")
send(edStage)

View File

@ -0,0 +1 @@
send(edStage)

View 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"
}
]
}
]
}
]