Initial commit

This commit is contained in:
Sica 2025-03-30 23:09:58 +02:00
commit fd04310c79
136 changed files with 36870 additions and 0 deletions

273
README.md Normal file
View File

@ -0,0 +1,273 @@
# RollFor
A World of Warcraft (1.12.1 and 2.5.2) addon that manages rolling for items.
## New in this fork
This version includes the following new features:
* New option `/rf config auto-class-announce` Toggle replace normal roll message with classes for items that has class restrictions.
* New option `/rf config auto-tmog-disable` Toggle automatically disable tmog roll option on trash loot.
* New option `/rf config loot-frame-cursor` Toggle loot frame being positioned at cursor location.
* New popup to keep track of winners. Shift-click map icon or type `/rfw` to access.
* Right click headers to customize filters
* Click headers to sort the list
* Tracks raid trades
* Show roll popup for all group/raid members who have the addon installed when loot master starts a roll.
* Enable with `/rf config client show-roll Eligible` or `Always`
* `/rf config client` to view additional client options
## Demo
### NEW
**Classic Look**
<img src="docs/classic-look.png?v=2">
Enable: `/rf config classic-look`
See the classic-look in action: https://youtu.be/G37j5XXBKxs
### Overview
In this example, the addon shows the soft-ressed items in the loot list.
The Master Looter raid-rolls the trash item, then rolls non-SR items.
Then the SR items are rolled.
<img src="docs/bindings.gif?v=2" alt="overview" style="width:1350px;height:350">
View better quality (fullscreen): https://youtu.be/f5nY-CxreIM
### Tie roll
In this example, the addon automatically detects that the item is soft-reserved by two players.
It restricts rolling for the item to these players only and resolves any tie automatically.
The Master Looter then assigns the item directly to the winner.
<img src="docs/gui-sr-tie.gif" alt="soft-res rolling" style="width:1024;height:380">
### Two top rolls win
In this example, two identical items drop. When clicked on any of these, the addon selects
both and performs a "two-top-rolls-win" roll. Then the Master Looter is presented with
individual award buttons for each winner.
<img src="docs/two-items.gif" alt="two top rolls win" style="width:1100px;height:306">
## Features
### Fully SR-integrated loot list
<img src="docs/loot-list.gif" alt="SR-integrated loot list" style="width:720px;height:350">
---
### Automatically enables Master Loot when a boss is targeted
Disable this feature with:
```
/rf config auto-master-loot
```
---
### Shows the loot that dropped (and who soft reserved)
<img src="docs/dropped-loot.gif" alt="Shows dropped loot" style="width:720px;height:350">
---
### Makes Master Loot window pretty and safe
* one window with players sorted by class
* adds confirmation window
<img src="docs/master-loot-window.gif" alt="Pretty Master Loot window" style="width:720px;height:350">
---
### Fully automated
* Detects if someone rolls too many times and ignores extra rolls.
* If multiple players roll the same number, it automatically shows it and
waits for these players to re-roll.
<img src="docs/tie-winners.gif" alt="Tie winners" style="width:720px;height:350">
---
### Soft res integration
* Integrates with https://raidres.fly.dev (1.12.1).
* Integrates with https://softres.it (2.5.2) via Gargul Export.
* Minimap icon shows soft res status and who did not soft res.
* Fully automated (shows who soft ressed, only accepts rolls from players who SR).
---
### And more
* Supports "**two top rolls win**" rolling.
* Supports **raid rolls**.
* Supports offspec rolls (`/roll 99`).
* Supports transmog rolls (`/roll 98`) (1.12.1).
* Automatically resolves tied rolls.
* Highly customizable - see `/rf config` and `/rf config help`.
<img src="docs/raid-roll.gif" alt="Raid roll" style="width:720px;height:350">
---
### See it in action
https://youtu.be/vZdafun0nYo
## Usage
### Roll item
```
/rf <item link>
```
---
### Raid-roll item from your bags
```
/rr <item link>
```
---
### Insta Raid-roll item from your bags
```
/irr <item link>
```
---
### Roll for 2 items (two top rolls win)
```
/rf 2x<item link>
```
---
### Ignore SR and allow everyone to roll
If the item is SRed, the addon will only watch rolls for players who SRed.
However, if you want everyone to roll, even if the item is SRed, use `/arf`
instead of `/rf`. "arf" stands for "All Roll For".
---
## Soft-Res setup
1. Create a Soft Res list at https://raidres.fly.dev (1.12.1) or https://softres.it (2.5.2).
2. Ask raiders to add their items.
3. When ready, lock the raid and click on **RollFor export** (raidres.fly.dev) or **Gargul Export** (softes.it) button.
<img src="docs/raidres-export.jpg" alt="Raidres export" style="width:720px;height:350">
4. Click on **Copy RollFor data to clipboard** buton.
<img src="docs/raidres-copy-to-clipboard.jpg" alt="Raidres copy to clipboard" style="width:720px;height:350">
5. Click on the minimap icon or type `/sr`.
6. Paste the data into the window.
7. Click **Import!**.
<img src="docs/softres-import.jpg" alt="softres-import" style="width:720px;height:350">
The addon will tell you the status of SR import.
Hovering over the minimap icon will tell you who did not soft-res.
The minimap icon will be **green** if everyone in the group is soft-ressing.
The minimap icon will be **orange** if someone has not soft-ressed.
The minimap icon will be **red** if you have an outdated soft-res data.
The minimap icon will be **white** if there is no soft-res data.
To show the SR items type:
```
/srs
```
If someone needs to update their items, repeat the process and copy the data again.
### Soft-Res data format
The SR data from *Raidres* is a **Base64** encoded **JSON**. Decode it to see what's inside.
---
### Fixing mistyped player names in SR setup
When using soft-res, the players sometimes mistype their nickname, e.g.
`Johnny` in game will be `Jonnhy` in the raidres.fly.dev website.
The addon is smart enough to fix simple typos like that for you.
It will also deal with special characters in player names.
However, sometimes there's so many typos and the addon can't match the
player's name - you have to fix it manually.
`/sro` (stands for SR Override) is the command to do this.
---
### Finish rolls early
```
/fr
```
---
### Cancel rolls
```
/cr
```
---
### Show soft-ressed items
```
/srs
```
---
### Check soft-res status (to see if everyone is soft-ressing)
```
/src
```
---
### Clear soft-res data
Click on the minimap icon and click **Clear** or type:
```
/sr init
```
---
## Shoutouts
Thank you to:
* **Turtle WoW devs** for amazing content. You cunts should switch to a better client.
* **Itamedruids** for *Raidres* and adding the export function. Love your work.
* My fellow raiders (there's too many to mention).
* All bug reporters, testers and feature suggesters.
## Need more help?
The best way to contact me is to message me on Discord.
Username: **Obszczymucha**
My character **Jogobobek** will no longer be available on Turtle WoW.
I'm switching to Netherwing 3.0, perhaps under a different name :P
Thanks Turtle for fun.

94
RollFor-BCC.toc Normal file
View File

@ -0,0 +1,94 @@
## Interface: 20502
## Title: RollFor
## Author: Obszczymucha
## Version: 4.7.2
## Notes: An automated item roller with soft ressing support via softres.it.
## SavedVariables: RollForDb
## SavedVariablesPerCharacter: RollForCharDb
libs\bcc\LibStub\LibStub.lua
libs\bcc\Libs.xml
src\bcc\compat.lua
src\bcc\Json.lua
src\modules.lua
src\DebugBuffer.lua
src\Module.lua
src\Db.lua
src\Types.lua
src\Interface.lua
src\WowApi.lua
src\Config.lua
src\ItemUtils.lua
src\EventFrame.lua
src\LootFacade.lua
src\RollingLogicUtils.lua
src\DroppedLoot.lua
src\DroppedLootAnnounce.lua
src\TradeTracker.lua
src\SoftResDataTransformer.lua
src\SoftRes.lua
src\SoftResAwardedLootDecorator.lua
src\SoftResAbsentPlayersDecorator.lua
src\SoftResPresentPlayersDecorator.lua
src\SoftResMatchedNameDecorator.lua
src\AwardedLoot.lua
src\GroupRoster.lua
src\NameAutoMatcher.lua
src\NameManualMatcher.lua
src\NameMatchReport.lua
src\EventHandler.lua
src\SoftResGui.lua
src\VersionBroadcast.lua
src\MasterLoot.lua
src\MasterLootCandidateSelectionFrame.lua
src\SoftResCheck.lua
src\SoftResRollingLogic.lua
src\NonSoftResRollingLogic.lua
src\TieRollingLogic.lua
src\RaidRollRollingLogic.lua
src\UsagePrinter.lua
src\MinimapButton.lua
src\MasterLootWarning.lua
src\AutoLoot.lua
src\WinnerTracker.lua
src\FrameBuilder.lua
src\PopupBuilder.lua
src\LootAwardPopup.lua
src\MasterLootCandidates.lua
src\NewGroupEvent.lua
src\AutoGroupLoot.lua
src\BossList.lua
src\AutoMasterLoot.lua
src\RollTracker.lua
src\RollController.lua
src\RollingPopup.lua
src\SoftResRollGuiData.lua
src\TieRollGuiData.lua
src\RollingPopupContentTransformer.lua
src\GuiElements.lua
src\WelcomePopup.lua
src\InstaRaidRollRollingLogic.lua
src\EventBus.lua
src\LootList.lua
src\SoftResLootListDecorator.lua
src\LootFrame.lua
src\RollForAd.lua
src\RollingStrategyFactory.lua
src\RollingLogic.lua
src\ArgsParser.lua
src\RollResultAnnouncer.lua
src\ChatApi.lua
src\Chat.lua
src\PlayerInfo.lua
src\LootAwardCallback.lua
src\LootFacadeListener.lua
src\LootController.lua
src\TooltipReader.lua
src\UiReloadPopup.lua
src\Sandbox.lua
src\ModernLootFrameSkin.lua
src\OgLootFrameSkin.lua
main.lua

100
RollFor.toc Normal file
View File

@ -0,0 +1,100 @@
## Interface: 11200
## Title: RollFor
## Author: Obszczymucha
## Version: 4.7.2
## Notes: An automated item roller with soft ressing support via raidres.fly.dev.
## SavedVariables: RollForDb
## SavedVariablesPerCharacter: RollForCharDb
libs\vanilla\LibStub\LibStub.lua
libs\vanilla\Libs.xml
src\vanilla\backport.lua
src\vanilla\compat.lua
src\vanilla\Json.lua
src\modules.lua
src\DebugBuffer.lua
src\Module.lua
src\Db.lua
src\Types.lua
src\Interface.lua
src\WowApi.lua
src\Config.lua
src\ItemUtils.lua
src\EventFrame.lua
src\LootFacade.lua
src\RollingLogicUtils.lua
src\DroppedLoot.lua
src\DroppedLootAnnounce.lua
src\TradeTracker.lua
src\SoftResDataTransformer.lua
src\SoftRes.lua
src\SoftResAwardedLootDecorator.lua
src\SoftResAbsentPlayersDecorator.lua
src\SoftResPresentPlayersDecorator.lua
src\SoftResMatchedNameDecorator.lua
src\AwardedLoot.lua
src\GroupRoster.lua
src\NameAutoMatcher.lua
src\NameManualMatcher.lua
src\NameMatchReport.lua
src\EventHandler.lua
src\SoftResGui.lua
src\VersionBroadcast.lua
src\MasterLoot.lua
src\MasterLootCandidateSelectionFrame.lua
src\SoftResCheck.lua
src\SoftResRollingLogic.lua
src\NonSoftResRollingLogic.lua
src\TieRollingLogic.lua
src\RaidRollRollingLogic.lua
src\UsagePrinter.lua
src\MinimapButton.lua
src\MasterLootWarning.lua
src\AutoLoot.lua
src\WinnerTracker.lua
src\FrameBuilder.lua
src\PopupBuilder.lua
src\LootAwardPopup.lua
src\MasterLootCandidates.lua
src\NewGroupEvent.lua
src\AutoGroupLoot.lua
src\BossList.lua
src\AutoMasterLoot.lua
src\RollTracker.lua
src\RollController.lua
src\RollingPopup.lua
src\WinnersPopup.lua
src\WinnersPopupGui.lua
src\ConfirmPopup.lua
src\SoftResRollGuiData.lua
src\TieRollGuiData.lua
src\RollingPopupContentTransformer.lua
src\GuiElements.lua
src\WelcomePopup.lua
src\InstaRaidRollRollingLogic.lua
src\EventBus.lua
src\LootList.lua
src\SoftResLootListDecorator.lua
src\LootFrame.lua
src\RollForAd.lua
src\RollingStrategyFactory.lua
src\RollingLogic.lua
src\ArgsParser.lua
src\RollResultAnnouncer.lua
src\ChatApi.lua
src\Chat.lua
src\PlayerInfo.lua
src\LootAwardCallback.lua
src\LootFacadeListener.lua
src\LootController.lua
src\TooltipReader.lua
src\UiReloadPopup.lua
src\Sandbox.lua
src\ModernLootFrameSkin.lua
src\OgLootFrameSkin.lua
src\Client.lua
src\ClientBroadcast.lua
main.lua

4
TODO.md Normal file
View File

@ -0,0 +1,4 @@
# TODO
1. Add configurable loot frame colors with color pickers.
2. Add non-master looter popup display with an ability to roll.

BIN
assets/col.tga Normal file

Binary file not shown.

BIN
assets/icon-green.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/icon-orange.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/icon-red.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/icon-white.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/icon-white2.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/info.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/og-loot-frame.tga Normal file

Binary file not shown.

BIN
assets/resize-grip.tga Normal file

Binary file not shown.

BIN
assets/star-gold.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/tiny-button-down.tga Normal file

Binary file not shown.

BIN
assets/tiny-button-up.tga Normal file

Binary file not shown.

BIN
assets/titlebar-top.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/titlebar-topleft.tga Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,278 @@
--- **AceTimer-3.0** provides a central facility for registering timers.
-- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
-- data structure that allows easy dispatching and fast rescheduling. Timers can be registered
-- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
-- AceTimer is currently limited to firing timers at a frequency of 0.01s as this is what the WoW timer API
-- restricts us to.
--
-- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
-- need to cancel the timer you just registered.
--
-- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceTimer itself.\\
-- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceTimer.
-- @class file
-- @name AceTimer-3.0
-- @release $Id: AceTimer-3.0.lua 1284 2022-09-25 09:15:30Z nevcairiel $
local MAJOR, MINOR = "AceTimer-3.0", 17 -- Bump minor on changes
local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceTimer then return end -- No upgrade needed
AceTimer.activeTimers = AceTimer.activeTimers or {} -- Active timer list
local activeTimers = AceTimer.activeTimers -- Upvalue our private data
-- Lua APIs
local type, unpack, next, error, select = type, unpack, next, error, select
-- WoW APIs
local GetTime, C_TimerAfter = GetTime, C_Timer.After
local function new(self, loop, func, delay, ...)
if delay < 0.01 then
delay = 0.01 -- Restrict to the lowest time that the C_Timer API allows us
end
local timer = {
object = self,
func = func,
looping = loop,
argsCount = select("#", ...),
delay = delay,
ends = GetTime() + delay,
...
}
activeTimers[timer] = timer
-- Create new timer closure to wrap the "timer" object
timer.callback = function()
if not timer.cancelled then
if type(timer.func) == "string" then
-- We manually set the unpack count to prevent issues with an arg set that contains nil and ends with nil
-- e.g. local t = {1, 2, nil, 3, nil} print(#t) will result in 2, instead of 5. This fixes said issue.
timer.object[timer.func](timer.object, unpack(timer, 1, timer.argsCount))
else
timer.func(unpack(timer, 1, timer.argsCount))
end
if timer.looping and not timer.cancelled then
-- Compensate delay to get a perfect average delay, even if individual times don't match up perfectly
-- due to fps differences
local time = GetTime()
local ndelay = timer.delay - (time - timer.ends)
-- Ensure the delay doesn't go below the threshold
if ndelay < 0.01 then ndelay = 0.01 end
C_TimerAfter(ndelay, timer.callback)
timer.ends = time + ndelay
else
activeTimers[timer.handle or timer] = nil
end
end
end
C_TimerAfter(delay, timer.callback)
return timer
end
--- Schedule a new one-shot timer.
-- The timer will fire once in `delay` seconds, unless canceled before.
-- @param callback Callback function for the timer pulse (funcref or method name).
-- @param delay Delay for the timer, in seconds.
-- @param ... An optional, unlimited amount of arguments to pass to the callback function.
-- @usage
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
--
-- function MyAddOn:OnEnable()
-- self:ScheduleTimer("TimerFeedback", 5)
-- end
--
-- function MyAddOn:TimerFeedback()
-- print("5 seconds passed")
-- end
function AceTimer:ScheduleTimer(func, delay, ...)
if not func or not delay then
error(MAJOR..": ScheduleTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
end
if type(func) == "string" then
if type(self) ~= "table" then
error(MAJOR..": ScheduleTimer(callback, delay, args...): 'self' - must be a table.", 2)
elseif not self[func] then
error(MAJOR..": ScheduleTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
end
end
return new(self, nil, func, delay, ...)
end
--- Schedule a repeating timer.
-- The timer will fire every `delay` seconds, until canceled.
-- @param callback Callback function for the timer pulse (funcref or method name).
-- @param delay Delay for the timer, in seconds.
-- @param ... An optional, unlimited amount of arguments to pass to the callback function.
-- @usage
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
--
-- function MyAddOn:OnEnable()
-- self.timerCount = 0
-- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
-- end
--
-- function MyAddOn:TimerFeedback()
-- self.timerCount = self.timerCount + 1
-- print(("%d seconds passed"):format(5 * self.timerCount))
-- -- run 30 seconds in total
-- if self.timerCount == 6 then
-- self:CancelTimer(self.testTimer)
-- end
-- end
function AceTimer:ScheduleRepeatingTimer(func, delay, ...)
if not func or not delay then
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
end
if type(func) == "string" then
if type(self) ~= "table" then
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'self' - must be a table.", 2)
elseif not self[func] then
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
end
end
return new(self, true, func, delay, ...)
end
--- Cancels a timer with the given id, registered by the same addon object as used for `:ScheduleTimer`
-- Both one-shot and repeating timers can be canceled with this function, as long as the `id` is valid
-- and the timer has not fired yet or was canceled before.
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
function AceTimer:CancelTimer(id)
local timer = activeTimers[id]
if not timer then
return false
else
timer.cancelled = true
activeTimers[id] = nil
return true
end
end
--- Cancels all timers registered to the current addon object ('self')
function AceTimer:CancelAllTimers()
for k,v in next, activeTimers do
if v.object == self then
AceTimer.CancelTimer(self, k)
end
end
end
--- Returns the time left for a timer with the given id, registered by the current addon object ('self').
-- This function will return 0 when the id is invalid.
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
-- @return The time left on the timer.
function AceTimer:TimeLeft(id)
local timer = activeTimers[id]
if not timer then
return 0
else
return timer.ends - GetTime()
end
end
-- ---------------------------------------------------------------------
-- Upgrading
-- Upgrade from old hash-bucket based timers to C_Timer.After timers.
if oldminor and oldminor < 10 then
-- disable old timer logic
AceTimer.frame:SetScript("OnUpdate", nil)
AceTimer.frame:SetScript("OnEvent", nil)
AceTimer.frame:UnregisterAllEvents()
-- convert timers
for object,timers in next, AceTimer.selfs do
for handle,timer in next, timers do
if type(timer) == "table" and timer.callback then
local newTimer
if timer.delay then
newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.callback, timer.delay, timer.arg)
else
newTimer = AceTimer.ScheduleTimer(timer.object, timer.callback, timer.when - GetTime(), timer.arg)
end
-- Use the old handle for old timers
activeTimers[newTimer] = nil
activeTimers[handle] = newTimer
newTimer.handle = handle
end
end
end
AceTimer.selfs = nil
AceTimer.hash = nil
AceTimer.debug = nil
elseif oldminor and oldminor < 17 then
-- Upgrade from old animation based timers to C_Timer.After timers.
AceTimer.inactiveTimers = nil
AceTimer.frame = nil
local oldTimers = AceTimer.activeTimers
-- Clear old timer table and update upvalue
AceTimer.activeTimers = {}
activeTimers = AceTimer.activeTimers
for handle, timer in next, oldTimers do
local newTimer
-- Stop the old timer animation
local duration, elapsed = timer:GetDuration(), timer:GetElapsed()
timer:GetParent():Stop()
if timer.looping then
newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.func, duration, unpack(timer.args, 1, timer.argsCount))
else
newTimer = AceTimer.ScheduleTimer(timer.object, timer.func, duration - elapsed, unpack(timer.args, 1, timer.argsCount))
end
-- Use the old handle for old timers
activeTimers[newTimer] = nil
activeTimers[handle] = newTimer
newTimer.handle = handle
end
-- Migrate transitional handles
if oldminor < 13 and AceTimer.hashCompatTable then
for handle, id in next, AceTimer.hashCompatTable do
local t = activeTimers[id]
if t then
activeTimers[id] = nil
activeTimers[handle] = t
t.handle = handle
end
end
AceTimer.hashCompatTable = nil
end
end
-- ---------------------------------------------------------------------
-- Embed handling
AceTimer.embeds = AceTimer.embeds or {}
local mixins = {
"ScheduleTimer", "ScheduleRepeatingTimer",
"CancelTimer", "CancelAllTimers",
"TimeLeft"
}
function AceTimer:Embed(target)
AceTimer.embeds[target] = true
for _,v in next, mixins do
target[v] = AceTimer[v]
end
return target
end
-- AceTimer:OnEmbedDisable(target)
-- target (object) - target object that AceTimer is embedded in.
--
-- cancel all timers registered for the object
function AceTimer:OnEmbedDisable(target)
target:CancelAllTimers()
end
for addon in next, AceTimer.embeds do
AceTimer:Embed(addon)
end

View File

@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceTimer-3.0.lua"/>
</Ui>

View File

@ -0,0 +1,207 @@
--[[ $Id: CallbackHandler-1.0.lua 1284 2022-09-25 09:15:30Z nevcairiel $ ]]
local MAJOR, MINOR = "CallbackHandler-1.0", 7
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
if not CallbackHandler then return end -- No upgrade needed
local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end}
-- Lua APIs
local error = error
local setmetatable, rawget = setmetatable, rawget
local next, select, pairs, type, tostring = next, select, pairs, type, tostring
local xpcall = xpcall
local function errorhandler(err)
return geterrorhandler()(err)
end
local function Dispatch(handlers, ...)
local index, method = next(handlers)
if not method then return end
repeat
xpcall(method, errorhandler, ...)
index, method = next(handlers, index)
until not method
end
--------------------------------------------------------------------------
-- CallbackHandler:New
--
-- target - target object to embed public APIs in
-- RegisterName - name of the callback registration API, default "RegisterCallback"
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
function CallbackHandler.New(_self, target, RegisterName, UnregisterName, UnregisterAllName)
RegisterName = RegisterName or "RegisterCallback"
UnregisterName = UnregisterName or "UnregisterCallback"
if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
UnregisterAllName = "UnregisterAllCallbacks"
end
-- we declare all objects and exported APIs inside this closure to quickly gain access
-- to e.g. function names, the "target" parameter, etc
-- Create the registry object
local events = setmetatable({}, meta)
local registry = { recurse=0, events=events }
-- registry:Fire() - fires the given event/message into the registry
function registry:Fire(eventname, ...)
if not rawget(events, eventname) or not next(events[eventname]) then return end
local oldrecurse = registry.recurse
registry.recurse = oldrecurse + 1
Dispatch(events[eventname], eventname, ...)
registry.recurse = oldrecurse
if registry.insertQueue and oldrecurse==0 then
-- Something in one of our callbacks wanted to register more callbacks; they got queued
for event,callbacks in pairs(registry.insertQueue) do
local first = not rawget(events, event) or not next(events[event]) -- test for empty before. not test for one member after. that one member may have been overwritten.
for object,func in pairs(callbacks) do
events[event][object] = func
-- fire OnUsed callback?
if first and registry.OnUsed then
registry.OnUsed(registry, target, event)
first = nil
end
end
end
registry.insertQueue = nil
end
end
-- Registration of a callback, handles:
-- self["method"], leads to self["method"](self, ...)
-- self with function ref, leads to functionref(...)
-- "addonId" (instead of self) with function ref, leads to functionref(...)
-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
if type(eventname) ~= "string" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
end
method = method or eventname
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
if type(method) ~= "string" and type(method) ~= "function" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
end
local regfunc
if type(method) == "string" then
-- self["method"] calling style
if type(self) ~= "table" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
elseif self==target then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
elseif type(self[method]) ~= "function" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
end
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
local arg=select(1,...)
regfunc = function(...) self[method](self,arg,...) end
else
regfunc = function(...) self[method](self,...) end
end
else
-- function ref with self=object or self="addonId" or self=thread
if type(self)~="table" and type(self)~="string" and type(self)~="thread" then
error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2)
end
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
local arg=select(1,...)
regfunc = function(...) method(arg,...) end
else
regfunc = method
end
end
if events[eventname][self] or registry.recurse<1 then
-- if registry.recurse<1 then
-- we're overwriting an existing entry, or not currently recursing. just set it.
events[eventname][self] = regfunc
-- fire OnUsed callback?
if registry.OnUsed and first then
registry.OnUsed(registry, target, eventname)
end
else
-- we're currently processing a callback in this registry, so delay the registration of this new entry!
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
registry.insertQueue = registry.insertQueue or setmetatable({},meta)
registry.insertQueue[eventname][self] = regfunc
end
end
-- Unregister a callback
target[UnregisterName] = function(self, eventname)
if not self or self==target then
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
end
if type(eventname) ~= "string" then
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
end
if rawget(events, eventname) and events[eventname][self] then
events[eventname][self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(events[eventname]) then
registry.OnUnused(registry, target, eventname)
end
end
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
registry.insertQueue[eventname][self] = nil
end
end
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
if UnregisterAllName then
target[UnregisterAllName] = function(...)
if select("#",...)<1 then
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
end
if select("#",...)==1 and ...==target then
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
end
for i=1,select("#",...) do
local self = select(i,...)
if registry.insertQueue then
for eventname, callbacks in pairs(registry.insertQueue) do
if callbacks[self] then
callbacks[self] = nil
end
end
end
for eventname, callbacks in pairs(events) do
if callbacks[self] then
callbacks[self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(callbacks) then
registry.OnUnused(registry, target, eventname)
end
end
end
end
end
end
return registry
end
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
-- try to upgrade old implicit embeds since the system is selfcontained and
-- relies on closures to work.

View File

@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="CallbackHandler-1.0.lua"/>
</Ui>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
## Interface: 80300
## Title: Lib: LibDeflate
## Notes: Compressor and decompressor with high compression ratio using DEFLATE/zlib format.
## Author: Haoqian He (WoW: Safetyy at Illidan-US (Horde))
## Version: @project-version@
## X-Website: https://wow.curseforge.com/projects/libdeflate
## X-Category: Library
## X-License: zlib
LibStub\LibStub.lua
lib.xml

View File

@ -0,0 +1,122 @@
--[[
zlib License
(C) 2018-2020 Haoqian He
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
--]]
--- LibDeflate usage example
-- @author Haoqian He
-- @file example.lua
local LibDeflate
if LibStub then -- You are using LibDeflate as WoW addon
LibDeflate = LibStub:GetLibrary("LibDeflate")
else
-- You are using LibDeflate as Lua library.
-- Setup the path to locate LibDeflate.lua,
-- if 'require("LibDeflate")' fails, for example:
-- package.path = ("?.lua;../?.lua;")..(package.path or "")
LibDeflate = require("LibDeflate")
end
local example_input = "12123123412345123456123456712345678123456789"
-- Compress using raw deflate format
local compress_deflate = LibDeflate:CompressDeflate(example_input)
-- decompress
local decompress_deflate = LibDeflate:DecompressDeflate(compress_deflate)
-- Check if the first return value of DecompressXXXX is non-nil to know if the
-- decompression succeeds.
if decompress_deflate == nil then
error("Decompression fails.")
else
-- Decompression succeeds.
assert(example_input == decompress_deflate)
end
-- If it is to transmit through WoW addon channel,
-- compressed data must be encoded so NULL ("\000") is not transmitted.
local data_to_trasmit_WoW_addon = LibDeflate:EncodeForWoWAddonChannel(
compress_deflate)
-- When the receiver gets the data, decoded it first.
local data_decoded_WoW_addon = LibDeflate:DecodeForWoWAddonChannel(
data_to_trasmit_WoW_addon)
-- Then decomrpess it
assert(LibDeflate:DecompressDeflate(data_decoded_WoW_addon) == example_input)
-- The compressed output is not printable. EncodeForPrint will convert to
-- a printable format, in case you want to export to the user to
-- copy and paste. This encoding will make the data 25% bigger.
local printable_compressed = LibDeflate:EncodeForPrint(compress_deflate)
-- DecodeForPrint to convert back.
-- DecodeForPrint will remove prefixed and trailing control or space characters
-- in the string before decode it.
assert(LibDeflate:DecodeForPrint(printable_compressed) == compress_deflate)
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
--- Compress and decompress using zlib format
local compress_zlib = LibDeflate:CompressZlib(example_input)
local decompress_zlib = LibDeflate:DecompressZlib(compress_zlib)
assert(decompress_zlib == example_input)
--- Control the compression level
-- NOTE: High compression level does not make a difference here,
-- because the input data is very small
local compress_deflate_with_level = LibDeflate:CompressDeflate(example_input
, {level = 9})
local decompress_deflate_with_level = LibDeflate:DecompressDeflate(
compress_deflate_with_level)
assert(decompress_deflate_with_level == example_input)
-- Compress with a preset dictionary
local dict_str = "121231234" -- example preset dictionary string.
-- print(LibDeflate:Adler32(dict_str), #dict_str)
-- 9 147325380
-- hardcode the print result above, the ensure it is not modified
-- accidenttaly during the program development.
--
-- WARNING: The compressor and decompressor must use the same dictionary.
-- You should be aware of this when tranmitting compressed data over the
-- internet.
local dict = LibDeflate:CreateDictionary(dict_str, 9, 147325380)
-- Using the dictionary with raw deflate format
local compress_deflate_with_dict = LibDeflate:CompressDeflateWithDict(
example_input, dict)
assert(#compress_deflate_with_dict < #compress_deflate)
local decompress_deflate_with_dict = LibDeflate:DecompressDeflateWithDict(
compress_deflate_with_dict, dict)
assert(decompress_deflate_with_dict == example_input)
-- Using the dictionary with zlib format, specifying compression level
local compress_zlib_with_dict = LibDeflate:CompressZlibWithDict(
example_input, dict, {level = 9})
assert(#compress_zlib_with_dict < #compress_zlib)
local decompress_zlib_with_dict = LibDeflate:DecompressZlibWithDict(
compress_zlib_with_dict, dict)
assert(decompress_zlib_with_dict == example_input)

View File

@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="LibDeflate.lua" />
</Ui>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
local LibStub = _G[LIBSTUB_MAJOR]
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or {libs = {}, minors = {} }
_G[LIBSTUB_MAJOR] = LibStub
LibStub.minor = LIBSTUB_MINOR
function LibStub:NewLibrary(major, minor)
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
minor = assert(tonumber(string.match(minor, "%d+")), "Minor version must either be a number or contain a number.")
local oldminor = self.minors[major]
if oldminor and oldminor >= minor then return nil end
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
return self.libs[major], oldminor
end
function LibStub:GetLibrary(major, silent)
if not self.libs[major] and not silent then
error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
end
return self.libs[major], self.minors[major]
end
function LibStub:IterateLibraries() return pairs(self.libs) end
setmetatable(LibStub, { __call = LibStub.GetLibrary })
end

6
libs/bcc/Libs.xml Normal file
View File

@ -0,0 +1,6 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Include file="CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
<Include file="AceTimer-3.0\AceTimer-3.0.xml"/>
<Include file="LibDeflate\lib.xml"/>
</Ui>

View File

@ -0,0 +1,209 @@
local ACECORE_MAJOR, ACECORE_MINOR = "AceCore-3.0", 2
local AceCore, oldminor = LibStub:NewLibrary(ACECORE_MAJOR, ACECORE_MINOR)
if not AceCore then return end -- No upgrade needed
AceCore._G = AceCore._G or getfenv()
local _G = AceCore._G
local strsub, strgsub, strfind = string.sub, string.gsub, string.find
local tremove, tconcat = table.remove, table.concat
local tgetn, tsetn = table.getn, table.setn
local new, del
do
local list = setmetatable({}, {__mode = "k"})
function new()
local t = next(list)
if not t then
return {}
end
list[t] = nil
return t
end
function del(t)
setmetatable(t, nil)
for k in pairs(t) do
t[k] = nil
end
tsetn(t,0)
list[t] = true
end
print = print or function(text)
DEFAULT_CHAT_FRAME:AddMessage(text)
end
-- debug
function AceCore.listcount()
local count = 0
for k in list do
count = count + 1
end
return count
end
end -- AceCore.new, AceCore.del
AceCore.new, AceCore.del = new, del
local function errorhandler(err)
return geterrorhandler()(err)
end
AceCore.errorhandler = errorhandler
local function CreateSafeDispatcher(argCount)
local code = [[
local errorhandler = LibStub("AceCore-3.0").errorhandler
local method, UP_ARGS
local function call()
local func, ARGS = method, UP_ARGS
method, UP_ARGS = nil, NILS
return func(ARGS)
end
return function(func, ARGS)
method, UP_ARGS = func, ARGS
return xpcall(call, errorhandler)
end
]]
local c = 4*argCount-1
local s = "b01,b02,b03,b04,b05,b06,b07,b08,b09,b10,b11,b12,b13,b14,b15,b16,b17,b18,b19,b20"
code = strgsub(code, "UP_ARGS", string.sub(s,1,c))
s = "a01,a02,a03,a04,a05,a06,a07,a08,a09,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20"
code = strgsub(code, "ARGS", string.sub(s,1,c))
s = "nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil"
code = strgsub(code, "NILS", string.sub(s,1,c))
return assert(loadstring(code, "safecall SafeDispatcher["..tostring(argCount).."]"))()
end
local SafeDispatchers = setmetatable({}, {__index=function(self, argCount)
local dispatcher
if not tonumber(argCount) then dbg(debugstack()) end
if argCount > 0 then
dispatcher = CreateSafeDispatcher(argCount)
else
dispatcher = function(func) return xpcall(func,errorhandler) end
end
rawset(self, argCount, dispatcher)
return dispatcher
end})
local function safecall(func,argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
-- we check to see if the func is passed is actually a function here and don't error when it isn't
-- this safecall is used for optional functions like OnInitialize OnEnable etc. When they are not
-- present execution should continue without hinderance
if type(func) == "function" then
return SafeDispatchers[argc](func,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
end
end
AceCore.safecall = safecall
local function CreateDispatcher(argCount)
local code = [[
return function(func,ARGS)
return func(ARGS)
end
]]
local s = "a01,a02,a03,a04,a05,a06,a07,a08,a09,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20"
code = strgsub(code, "ARGS", string.sub(s,1,4*argCount-1))
return assert(loadstring(code, "call Dispatcher["..tostring(argCount).."]"))()
end
AceCore.Dispatchers = setmetatable({}, {__index=function(self, argCount)
local dispatcher
if argCount > 0 then
dispatcher = CreateDispatcher(argCount)
else
dispatcher = function(func) return func() end
end
rawset(self, argCount, dispatcher)
return dispatcher
end})
-- some string functions
-- vanilla available string operations:
-- sub, gfind, rep, gsub, char, dump, find, upper, len, format, byte, lower
-- we will just replace every string.match with string.find in the code
function AceCore.strtrim(s)
return strgsub(s, "^%s*(.-)%s*$", "%1")
end
local function strsplit(delim, s, n)
if n and n < 2 then return s end
beg = beg or 1
local i,j = string.find(s,delim,beg)
if not i then
return s, nil
end
return string.sub(s,1,j-1), strsplit(delim, string.sub(s,j+1), n and n-1 or nil)
end
AceCore.strsplit = strsplit
-- Ace3v: fonctions copied from AceHook-2.1
local protFuncs = {
CameraOrSelectOrMoveStart = true, CameraOrSelectOrMoveStop = true,
TurnOrActionStart = true, TurnOrActionStop = true,
PitchUpStart = true, PitchUpStop = true,
PitchDownStart = true, PitchDownStop = true,
MoveBackwardStart = true, MoveBackwardStop = true,
MoveForwardStart = true, MoveForwardStop = true,
Jump = true, StrafeLeftStart = true,
StrafeLeftStop = true, StrafeRightStart = true,
StrafeRightStop = true, ToggleMouseMove = true,
ToggleRun = true, TurnLeftStart = true,
TurnLeftStop = true, TurnRightStart = true,
TurnRightStop = true,
}
local function issecurevariable(x)
return protFuncs[x] and 1 or nil
end
AceCore.issecurevariable = issecurevariable
local function hooksecurefunc(arg1, arg2, arg3)
if type(arg1) == "string" then
arg1, arg2, arg3 = _G, arg1, arg2
end
local orig = arg1[arg2]
if type(orig) ~= "function" then
error("The function "..arg2.." does not exist", 2)
end
arg1[arg2] = function(...)
local tmp = {orig(unpack(arg))}
arg3(unpack(arg))
return unpack(tmp)
end
end
AceCore.hooksecurefunc = hooksecurefunc
-- pickfirstset() - picks the first non-nil value and returns it
local function pickfirstset(argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
if (argc <= 1) or (a1 ~= nil) then
return a1
else
return pickfirstset(argc-1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
end
end
AceCore.pickfirstset = pickfirstset
local function countargs(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
if (a1 == nil) then return 0 end
return 1 + countargs(a2,a3,a4,a5,a6,a7,a8,a9,a10)
end
AceCore.countargs = countargs
-- wipe preserves metatable
function AceCore.wipe(t)
for k,v in pairs(t) do t[k] = nil end
tsetn(t,0)
return t
end
function AceCore.truncate(t,e)
e = e or tgetn(t)
for i=1,e do
if t[i] == nil then
tsetn(t,i-1)
return
end
end
tsetn(t,e)
end

View File

@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceCore-3.0.lua"/>
</Ui>

View File

@ -0,0 +1,379 @@
--- **AceTimer-3.0** provides a central facility for registering timers.
-- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
-- data structure that allows easy dispatching and fast rescheduling. Timers can be registered
-- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
-- AceTimer is currently limited to firing timers at a frequency of 0.01s as this is what the WoW timer API
-- restricts us to.
--
-- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
-- need to cancel the timer you just registered.
--
-- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceTimer itself.\\
-- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceTimer.
-- @class file
-- @name AceTimer-3.0
-- @release $Id: AceTimer-3.0.lua 1119 2014-10-14 17:23:29Z nevcairiel $
local MAJOR, MINOR = "AceTimer-3.0", 18 -- Bump minor on changes
local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceTimer then return end -- No upgrade needed
local AceCore = LibStub("AceCore-3.0")
local safecall = AceCore.safecall
AceTimer.counter = AceTimer.counter or {}
AceTimer.hash = AceTimer.hash or {} -- Array of [1..BUCKETS] = linked list of timers (using .next member)
AceTimer.activeTimers = AceTimer.activeTimers or {} -- Active timer list
AceTimer.frame = AceTimer.frame or CreateFrame("Frame", "AceTimer30Frame")
local counter = AceTimer.counter
local activeTimers = AceTimer.activeTimers -- Upvalue our private data
local timerFrame = AceTimer.frame
-- Lua APIs
local type, unpack, next, error = type, unpack, next, error
local floor, max, min, mod = math.floor, math.max, math.min, math.mod
local tostring = tostring
-- WoW APIs
local GetTime = GetTime
--[[
Timers will not be fired more often than HZ-1 times per second.
Keep at intended speed PLUS ONE or we get bitten by floating point rounding errors (n.5 + 0.1 can be n.599999)
If this is ever LOWERED, all existing timers need to be enforced to have a delay >= 1/HZ on lib upgrade.
If this number is ever changed, all entries need to be rehashed on lib upgrade.
]]
local HZ = 11
local minDelay = 1/(HZ-1)
--[[
Prime for good distribution
If this number is ever changed, all entries need to be rehashed on lib upgrade.
]]
local BUCKETS = 131
local hash = AceTimer.hash
for i=1,BUCKETS do
hash[i] = hash[i] or false -- make it an integer-indexed array; it's faster than hashes
end
local new, del
do
local list = setmetatable({}, {__mode = "k"})
function new(self, loop, func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
local name = loop and "ScheduleRepeatingTimer" or "ScheduleTimer"
if self == AceTimer then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): use your own 'self'", 3)
end
if not func or not delay then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): 'callback' and 'delay' must have set values.", 3)
end
if argc and (type(argc) ~= "number" or floor(argc) ~= argc) then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): 'argc' must be an integer.", 3)
end
if type(func) == "string" then
if type(self) ~= "table" then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): 'self' - must be a table.", 3)
elseif type(self[func]) ~= "function" then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): Tried to register '"..func.."' as the callback, but it is not a method.", 3)
end
elseif type(func) ~= "function" then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): Tried to register '"..tostring(func).."' as the callback, but it is not a function.", 3)
end
if delay < minDelay then
delay = minDelay
end
-- Create and stuff timer in the correct hash bucket
local now = GetTime()
local timer = next(list) or {}
list[timer] = nil
timer.object = self
timer.func = func
timer.delay = delay
timer.status = loop and "loop" or "once"
timer.ends = now + delay
timer.argsCount = argc or 0
timer[1] = a1
timer[2] = a2
timer[3] = a3
timer[4] = a4
timer[5] = a5
timer[6] = a6
timer[7] = a7
timer[8] = a8
timer[9] = a9
timer[10] = a10
local bucket = floor(mod((now+delay)*HZ,BUCKETS)) + 1
timer.next = hash[bucket]
hash[bucket] = timer
local id = tostring(timer) -- user has only access to the id but not the table itself
activeTimers[id] = timer
counter[self] = (counter[self] or 0) + 1
timerFrame:Show()
return id
end
function del(t)
local id = tostring(t)
activeTimers[id] = nil
if not next(activeTimers) then
timerFrame:Hide()
end
local self = t.object
for k in pairs(t) do t[k] = nil end
list[t] = true
if counter[self] then
counter[self] = counter[self] - 1
else
counter[self] = nil
end
end
end -- new, del
--- Schedule a new one-shot timer.
-- The timer will fire once in `delay` seconds, unless canceled before.
-- @param callback Callback function for the timer pulse (funcref or method name).
-- @param delay Delay for the timer, in seconds.
-- @param argc The numbers of arguments to be passed to the callback function
-- @param a1,...,a10 The arguments
-- @usage
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
--
-- function MyAddOn:OnEnable()
-- self:ScheduleTimer("TimerFeedback", 5)
-- end
--
-- function MyAddOn:TimerFeedback()
-- print("5 seconds passed")
-- end
function AceTimer:ScheduleTimer(func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
return new(self, nil, func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
end
--- Schedule a repeating timer.
-- The timer will fire every `delay` seconds, until canceled.
-- @param callback Callback function for the timer pulse (funcref or method name).
-- @param delay Delay for the timer, in seconds.
-- @param argc The numbers of arguments to be passed to the callback function
-- @param a1,...,a10 The arguments
-- @usage
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
--
-- function MyAddOn:OnEnable()
-- self.timerCount = 0
-- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
-- end
--
-- function MyAddOn:TimerFeedback()
-- self.timerCount = self.timerCount + 1
-- print(("%d seconds passed"):format(5 * self.timerCount))
-- -- run 30 seconds in total
-- if self.timerCount == 6 then
-- self:CancelTimer(self.testTimer)
-- end
-- end
function AceTimer:ScheduleRepeatingTimer(func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
return new(self, true, func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
end
--- Cancels a timer with the given id, registered by the same addon object as used for `:ScheduleTimer`
-- Both one-shot and repeating timers can be canceled with this function, as long as the `id` is valid
-- and the timer has not fired yet or was canceled before.
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
function AceTimer:CancelTimer(id)
local timer = activeTimers[id]
if not timer then
return false
else
-- Ace3v: the timer will always be collected in the next update but not here
-- this is necessary for AceBucket to determinate if the bucket has been unregistered
-- in the callback
timer.status = nil
activeTimers[id] = nil
return true
end
end
--- Cancels all timers registered to the current addon object ('self')
function AceTimer:CancelAllTimers()
if type(self) ~= "table" then
error(MAJOR..": CancelAllTimers(): 'self' - must be a table",2)
end
if self == AceTimer then
error(MAJOR..": CancelAllTimers(): supply a meaningful 'self'", 2)
end
for k,v in pairs(activeTimers) do
if v.object == self then
AceTimer.CancelTimer(self, k)
end
end
end
--- Returns the time left for a timer with the given id, registered by the current addon object ('self').
-- This function will return 0 when the id is invalid.
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
-- @return The time left on the timer.
function AceTimer:TimeLeft(id)
local timer = activeTimers[id]
if not timer then
return 0
else
return timer.ends - GetTime()
end
end
function AceTimer:TimerStatus(id)
local timer = activeTimers[id]
if not timer then
return nil
else
return timer.status
end
end
-- ---------------------------------------------------------------------
-- Embed handling
AceTimer.embeds = AceTimer.embeds or {}
local mixins = {
"ScheduleTimer", "ScheduleRepeatingTimer",
"CancelTimer", "CancelAllTimers",
"TimeLeft", "TimerStatus"
}
function AceTimer:Embed(target)
AceTimer.embeds[target] = true
for _,v in pairs(mixins) do
target[v] = AceTimer[v]
end
return target
end
-- AceTimer:OnEmbedDisable(target)
-- target (object) - target object that AceTimer is embedded in.
--
-- cancel all timers registered for the object
function AceTimer:OnEmbedDisable(target)
target:CancelAllTimers()
end
for addon in pairs(AceTimer.embeds) do
AceTimer:Embed(addon)
end
-- --------------------------------------------------------------------
-- OnUpdate handler
--
-- traverse buckets, always chasing "now", and fire timers that have expired
local lastint = floor(GetTime() * HZ)
local function OnUpdate()
local now = GetTime()
local nowint = floor(now * HZ)
-- Have we passed into a new hash bucket?
if nowint == lastint then return end
local soon = now + 1 -- +1 is safe as long as 1 < HZ < BUCKETS/2
-- Pass through each bucket at most once
-- Happens on e.g. instance loads, but COULD happen on high local load situations also
for curint = (max(lastint, nowint-BUCKETS) + 1), nowint do -- loop until we catch up with "now", usually only 1 iteratio
local curbucket = mod(curint,BUCKETS) + 1 -- Ace3v: both int so no floor here
-- Yank the list of timers out of the bucket and empty it. This allows reinsertion in the currently-processed bucket from callbacks.
local nexttimer = hash[curbucket]
hash[curbucket] = false -- false rather than nil to prevent the array from becoming a hash
while nexttimer do
local timer = nexttimer
nexttimer = timer.next
local status = timer.status
if not status then
del(timer)
else
local ends = timer.ends
if (status == "loop" or status == "once") and ends < soon then
local object = timer.object
local callback = timer.func
if type(callback) == "string" then
callback = (type(object) == "table") and object[callback]
if type(callback) == "function" then
safecall(callback, timer.argsCount+1, object,
timer[1], timer[2], timer[3], timer[4], timer[5],
timer[6], timer[7], timer[8], timer[9], timer[10])
else
status = "once"
end
elseif type(callback) == "function" then
safecall(callback, timer.argsCount,
timer[1], timer[2], timer[3], timer[4], timer[5],
timer[6], timer[7], timer[8], timer[9], timer[10])
else
-- probably nilled out by CancelTimer
status = "once" -- don't reschedule it
end
if status == "once" then
del(timer)
else
local delay = timer.delay
local newends = ends + delay
if newends < now then -- Keep lag from making us firing a timer unnecessarily. (Note that this still won't catch too-short-delay timers though.)
newends = now + delay
end
timer.ends = newends
-- add next timer execution to the correct bucket
local bucket = floor(mod(newends*HZ,BUCKETS)) + 1
timer.next = hash[bucket]
hash[bucket] = timer
end
else
-- reinsert (yeah, somewhat expensive, but shouldn't be happening too often either due to hash distribution)
timer.next = hash[curbucket]
hash[curbucket] = timer
end
end
end
end
lastint = nowint
end
local lastchecked = nil
local function OnEvent()
if event ~= "PLAYER_REGEN_ENABLED" then return end
local addon = next(counter, lastchecked)
if not addon then
addon = next(counter)
end
lastchecked = addon
if not addon then -- should only happen if counter is empty
return
end
local n = counter[addon]
if n > BUCKETS then
DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: The addon/module '"..tostring(addon).."' has "..tostring(n).." live timers. Surely that's not intended?")
end
end
timerFrame:SetScript("OnUpdate", OnUpdate)
timerFrame:SetScript("OnEvent", OnEvent)
timerFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
timerFrame:Hide()

View File

@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceTimer-3.0.lua"/>
</Ui>

View File

@ -0,0 +1,280 @@
--[[ $Id: CallbackHandler-1.0.lua 1131 2015-06-04 07:29:24Z nevcairiel $ ]]
local MAJOR, MINOR = "CallbackHandler-1.0", 6
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
if not CallbackHandler then return end -- No upgrade needed
-- Lua APIs
local tconcat, tinsert, tgetn, tsetn = table.concat, table.insert, table.getn, table.setn
local assert, error, loadstring = assert, error, loadstring
local setmetatable, rawset, rawget = setmetatable, rawset, rawget
local next, pairs, type, tostring = next, pairs, type, tostring
local strgsub = string.gsub
local new, del
do
local list = setmetatable({}, {__mode = "k"})
function new()
local t = next(list)
if not t then
return {}
end
list[t] = nil
return t
end
function del(t)
setmetatable(t, nil)
for k in pairs(t) do
t[k] = nil
end
tsetn(t,0)
list[t] = true
end
end
local meta = {__index = function(tbl, key) rawset(tbl, key, new()) return tbl[key] end}
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
-- List them here for Mikk's FindGlobals script
-- GLOBALS: geterrorhandler
local function errorhandler(err)
return geterrorhandler()(err)
end
CallbackHandler.errorhandler = errorhandler
local function CreateDispatcher(argCount)
local code = [[
local xpcall, errorhandler = xpcall, LibStub("CallbackHandler-1.0").errorhandler
local method, UP_ARGS
local function call()
local func, ARGS = method, UP_ARGS
method, UP_ARGS = nil, NILS
return func(ARGS)
end
return function(handlers, ARGS)
local index
index, method = next(handlers)
if not method then return end
repeat
UP_ARGS = ARGS
xpcall(call, errorhandler)
index, method = next(handlers, index)
until not method
end
]]
local c = 4*argCount-1
local s = "b01,b02,b03,b04,b05,b06,b07,b08,b09,b10"
code = strgsub(code, "UP_ARGS", string.sub(s,1,c))
s = "a01,a02,a03,a04,a05,a06,a07,a08,a09,a10"
code = strgsub(code, "ARGS", string.sub(s,1,c))
s = "nil,nil,nil,nil,nil,nil,nil,nil,nil,nil"
code = strgsub(code, "NILS", string.sub(s,1,c))
return assert(loadstring(code, "safecall Dispatcher["..tostring(argCount).."]"))()
end
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
local dispatcher = CreateDispatcher(argCount)
rawset(self, argCount, dispatcher)
return dispatcher
end})
--------------------------------------------------------------------------
-- CallbackHandler:New
--
-- target - target object to embed public APIs in
-- RegisterName - name of the callback registration API, default "RegisterCallback"
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName)
RegisterName = RegisterName or "RegisterCallback"
UnregisterName = UnregisterName or "UnregisterCallback"
if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
UnregisterAllName = "UnregisterAllCallbacks"
end
-- we declare all objects and exported APIs inside this closure to quickly gain access
-- to e.g. function names, the "target" parameter, etc
-- Create the registry object
local events = setmetatable({}, meta)
local registry = { recurse=0, events=events }
-- registry:Fire() - fires the given event/message into the registry
function registry:Fire(eventname, argc, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)
if not rawget(events, eventname) or not next(events[eventname]) then return end
local oldrecurse = registry.recurse
registry.recurse = oldrecurse + 1
argc = argc or 0
Dispatchers[argc+1](events[eventname], eventname, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)
registry.recurse = oldrecurse
if registry.insertQueue and oldrecurse==0 then
-- Something in one of our callbacks wanted to register more callbacks; they got queued
for eventname,callbacks in pairs(registry.insertQueue) do
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
for self,func in pairs(callbacks) do
events[eventname][self] = func
-- fire OnUsed callback?
if first and registry.OnUsed then
registry.OnUsed(registry, target, eventname)
first = nil
end
end
del(callbacks)
end
del(registry.insertQueue)
registry.insertQueue = nil
end
end
-- Registration of a callback, handles:
-- self["method"], leads to self["method"](self, ...)
-- self with function ref, leads to functionref(...)
-- "addonId" (instead of self) with function ref, leads to functionref(...)
-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
target[RegisterName] = function(self, eventname, method, ...)
if type(eventname) ~= "string" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
end
method = method or eventname
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
if type(method) ~= "string" and type(method) ~= "function" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'method' - string or function expected.", 2)
end
local regfunc
local a1 = arg[1]
if type(method) == "string" then
-- self["method"] calling style
if type(self) ~= "table" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): self was not a table?", 2)
elseif self==target then
error("Usage: "..RegisterName.."(eventname, method[, arg]): do not use Library:"..RegisterName.."(), use your own 'self'.", 2)
elseif type(self[method]) ~= "function" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'method' - method '"..tostring(method).."' not found on 'self'.", 2)
end
if tgetn(arg) >= 1 then
regfunc = function (...) return self[method](self,a1,unpack(arg)) end
else
regfunc = function (...) return self[method](self,unpack(arg)) end
end
else
-- function ref with self=object or self="addonId"
if type(self)~="table" and type(self)~="string" then
error("Usage: "..RegisterName.."(self or addonId, eventname, method[, arg]): 'self or addonId': table or string expected.", 2)
end
if tgetn(arg) >= 1 then
regfunc = function (...) return method(a1, unpack(arg)) end
else
regfunc = method
end
end
if events[eventname][self] or registry.recurse<1 then
-- if registry.recurse<1 then
-- we're overwriting an existing entry, or not currently recursing. just set it.
events[eventname][self] = regfunc
-- fire OnUsed callback?
if registry.OnUsed and first then
registry.OnUsed(registry, target, eventname)
end
else
-- we're currently processing a callback in this registry, so delay the registration of this new entry!
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
registry.insertQueue = registry.insertQueue or setmetatable(new(),meta)
registry.insertQueue[eventname][self] = regfunc
end
end
-- Unregister a callback
target[UnregisterName] = function(self, eventname)
if not self or self==target then
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
end
if type(eventname) ~= "string" then
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
end
if rawget(events, eventname) and events[eventname][self] then
events[eventname][self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(events[eventname]) then
registry.OnUnused(registry, target, eventname)
end
if rawget(events, eventname) and not next(events[eventname]) then
del(events[eventname])
events[eventname] = nil
end
end
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
registry.insertQueue[eventname][self] = nil
end
end
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
if UnregisterAllName then
target[UnregisterAllName] = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
if not a1 then
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or 'addonId' to unregister events for.", 2)
end
if a1 == target then
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or 'addonId'", 2)
end
-- use our registry table as argument table
registry[1] = a1
registry[2] = a2
registry[3] = a3
registry[4] = a4
registry[5] = a5
registry[6] = a6
registry[7] = a7
registry[8] = a8
registry[9] = a9
registry[10] = a10
for i=1,10 do
local self = registry[i]
registry[i] = nil
if self then
if registry.insertQueue then
for eventname, callbacks in pairs(registry.insertQueue) do
if callbacks[self] then
callbacks[self] = nil
end
end
end
for eventname, callbacks in pairs(events) do
if callbacks[self] then
callbacks[self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(callbacks) then
registry.OnUnused(registry, target, eventname)
end
end
end
end
end
end
end
return registry
end
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
-- try to upgrade old implicit embeds since the system is selfcontained and
-- relies on closures to work.

View File

@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="CallbackHandler-1.0.lua"/>
</Ui>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
## Interface: 80300
## Title: Lib: LibDeflate
## Notes: Compressor and decompressor with high compression ratio using DEFLATE/zlib format.
## Author: Haoqian He (WoW: Safetyy at Illidan-US (Horde))
## Version: @project-version@
## X-Website: https://wow.curseforge.com/projects/libdeflate
## X-Category: Library
## X-License: zlib
LibStub\LibStub.lua
lib.xml

View File

@ -0,0 +1,122 @@
--[[
zlib License
(C) 2018-2020 Haoqian He
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
--]]
--- LibDeflate usage example
-- @author Haoqian He
-- @file example.lua
local LibDeflate
if LibStub then -- You are using LibDeflate as WoW addon
LibDeflate = LibStub:GetLibrary("LibDeflate")
else
-- You are using LibDeflate as Lua library.
-- Setup the path to locate LibDeflate.lua,
-- if 'require("LibDeflate")' fails, for example:
-- package.path = ("?.lua;../?.lua;")..(package.path or "")
LibDeflate = require("LibDeflate")
end
local example_input = "12123123412345123456123456712345678123456789"
-- Compress using raw deflate format
local compress_deflate = LibDeflate:CompressDeflate(example_input)
-- decompress
local decompress_deflate = LibDeflate:DecompressDeflate(compress_deflate)
-- Check if the first return value of DecompressXXXX is non-nil to know if the
-- decompression succeeds.
if decompress_deflate == nil then
error("Decompression fails.")
else
-- Decompression succeeds.
assert(example_input == decompress_deflate)
end
-- If it is to transmit through WoW addon channel,
-- compressed data must be encoded so NULL ("\000") is not transmitted.
local data_to_trasmit_WoW_addon = LibDeflate:EncodeForWoWAddonChannel(
compress_deflate)
-- When the receiver gets the data, decoded it first.
local data_decoded_WoW_addon = LibDeflate:DecodeForWoWAddonChannel(
data_to_trasmit_WoW_addon)
-- Then decomrpess it
assert(LibDeflate:DecompressDeflate(data_decoded_WoW_addon) == example_input)
-- The compressed output is not printable. EncodeForPrint will convert to
-- a printable format, in case you want to export to the user to
-- copy and paste. This encoding will make the data 25% bigger.
local printable_compressed = LibDeflate:EncodeForPrint(compress_deflate)
-- DecodeForPrint to convert back.
-- DecodeForPrint will remove prefixed and trailing control or space characters
-- in the string before decode it.
assert(LibDeflate:DecodeForPrint(printable_compressed) == compress_deflate)
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
--- Compress and decompress using zlib format
local compress_zlib = LibDeflate:CompressZlib(example_input)
local decompress_zlib = LibDeflate:DecompressZlib(compress_zlib)
assert(decompress_zlib == example_input)
--- Control the compression level
-- NOTE: High compression level does not make a difference here,
-- because the input data is very small
local compress_deflate_with_level = LibDeflate:CompressDeflate(example_input
, {level = 9})
local decompress_deflate_with_level = LibDeflate:DecompressDeflate(
compress_deflate_with_level)
assert(decompress_deflate_with_level == example_input)
-- Compress with a preset dictionary
local dict_str = "121231234" -- example preset dictionary string.
-- print(LibDeflate:Adler32(dict_str), #dict_str)
-- 9 147325380
-- hardcode the print result above, the ensure it is not modified
-- accidenttaly during the program development.
--
-- WARNING: The compressor and decompressor must use the same dictionary.
-- You should be aware of this when tranmitting compressed data over the
-- internet.
local dict = LibDeflate:CreateDictionary(dict_str, 9, 147325380)
-- Using the dictionary with raw deflate format
local compress_deflate_with_dict = LibDeflate:CompressDeflateWithDict(
example_input, dict)
assert(#compress_deflate_with_dict < #compress_deflate)
local decompress_deflate_with_dict = LibDeflate:DecompressDeflateWithDict(
compress_deflate_with_dict, dict)
assert(decompress_deflate_with_dict == example_input)
-- Using the dictionary with zlib format, specifying compression level
local compress_zlib_with_dict = LibDeflate:CompressZlibWithDict(
example_input, dict, {level = 9})
assert(#compress_zlib_with_dict < #compress_zlib)
local decompress_zlib_with_dict = LibDeflate:DecompressZlibWithDict(
compress_zlib_with_dict, dict)
assert(decompress_zlib_with_dict == example_input)

View File

@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="LibDeflate.lua" />
</Ui>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
local _G = getfenv()
local strfind, strfmt = string.find, string.format
local LibStub = _G[ LIBSTUB_MAJOR ]
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or { libs = {}, minors = {} }
_G[ LIBSTUB_MAJOR ] = LibStub
LibStub.minor = LIBSTUB_MINOR
function LibStub:NewLibrary( major, minor )
assert( type( major ) == "string", "Bad argument #2 to `NewLibrary' (string expected)" )
local _, _, num = strfind( minor, "(%d+)" )
minor = assert( tonumber( num ), "Minor version must either be a number or contain a number." )
local oldminor = self.minors[ major ]
if oldminor and oldminor >= minor then return nil end
self.minors[ major ], self.libs[ major ] = minor, self.libs[ major ] or {}
return self.libs[ major ], oldminor
end
function LibStub:GetLibrary( major, silent )
if not self.libs[ major ] and not silent then
error( strfmt( "Cannot find a library instance of %q.", tostring( major ) ), 2 )
end
return self.libs[ major ], self.minors[ major ]
end
function LibStub:IterateLibraries() return pairs( self.libs ) end
setmetatable( LibStub, { __call = LibStub.GetLibrary } )
end

7
libs/vanilla/Libs.xml Normal file
View File

@ -0,0 +1,7 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Include file="CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
<Include file="AceCore-3.0\AceCore-3.0.xml"/>
<Include file="AceTimer-3.0\AceTimer-3.0.xml"/>
<!--<Include file="LibDeflate\lib.xml"/>-->
</Ui>

794
main.lua Normal file
View File

@ -0,0 +1,794 @@
RollFor = RollFor or {}
local m = RollFor
---@diagnostic disable-next-line: undefined-global
local lib_stub = LibStub
local version = m.get_addon_version()
local M = {}
local getn = m.getn
local info = m.pretty_print
local hl = m.colors.highlight
local RollSlashCommand = m.Types.RollSlashCommand
local function clear_data()
M.softres_gui.clear()
M.name_matcher.clear( true )
M.softres.clear( true )
M.minimap_button.set_icon( M.minimap_button.ColorType.White )
M.winner_tracker.clear()
end
local function update_minimap_icon()
local result = M.softres_check.check_softres( true )
if result == M.softres_check.ResultType.NoItemsFound then
M.minimap_button.set_icon( M.minimap_button.ColorType.White )
elseif result == M.softres_check.ResultType.SomeoneIsNotSoftRessing then
M.minimap_button.set_icon( M.minimap_button.ColorType.Orange )
elseif result == M.softres_check.ResultType.FoundOutdatedData then
M.minimap_button.set_icon( M.minimap_button.ColorType.Red )
else
M.minimap_button.set_icon( M.minimap_button.ColorType.Green )
end
end
local function on_softres_status_changed()
update_minimap_icon()
end
local function on_raid_trade( giver_name, recipient_name, item_name )
local item_id = M.dropped_loot.get_dropped_item_id( item_name )
if item_id then
local quality, _ = m.get_item_quality_and_texture( m.api, item_id )
local item_link = m.fetch_item_link( item_id, quality )
M.loot_award_callback.on_loot_awarded( item_id, item_link, recipient_name, nil, true )
if item_id and M.awarded_loot.has_item_been_awarded( giver_name, item_id ) then
info( string.format( "%s traded %s to %s.", hl( giver_name ), item_link, hl( recipient_name ) ) )
M.awarded_loot.unaward( giver_name, item_id )
end
end
end
local function trade_complete_callback( recipient_name, items_given, items_received )
for i = 1, getn( items_given ) do
local item = items_given[ i ]
if item then
local item_id = M.item_utils.get_item_id( item.link )
local item_name = item_id and M.dropped_loot.get_dropped_item_name( item_id )
if item_id and item_name then
M.loot_award_callback.on_loot_awarded( item_id, item.link, recipient_name )
end
end
end
for i = 1, getn( items_received ) do
local item = items_received[ i ]
if item then
local item_id = M.item_utils.get_item_id( item.link )
if item_id and M.awarded_loot.has_item_been_awarded( recipient_name, item_id ) then
M.unaward_item( recipient_name, item_id, item.link )
end
end
end
end
local function create_components()
---@type AceTimer
M.ace_timer = lib_stub( "AceTimer-3.0" )
local db = m.Db.new( M.char_db )
---@type EventBus
M.config_event_bus = m.EventBus.new()
---@type Config
M.config = m.Config.new( db( "config" ), M.config_event_bus )
local classic = M.config.classic_look()
local popup_bottom_margin, popup_bottom_button_margin = classic and 37 or 24, classic and 14 or 7
local popup_side_margin = classic and 50 or 35
local popup_builder_factory = classic and m.PopupBuilder.classic or m.PopupBuilder.modern
---@type fun(): PopupBuilder
---@param bottom_margin number?
---@param side_margin number?
local function popup_builder( bottom_margin, side_margin )
return popup_builder_factory( m.FrameBuilder, bottom_margin or popup_bottom_margin, popup_bottom_button_margin, side_margin or popup_side_margin )
end
---@type UiReloadPopup
M.ui_reload_popup = m.UiReloadPopup.new( popup_builder( classic and 37 or 27 ), M.config )
---@type ConfirmPopup
M.confirm_popup = m.ConfirmPopup.new( popup_builder( classic and 37 or 27 ), M.config )
M.api = function() return m.api end
---@type PlayerInfo
M.player_info = m.PlayerInfo.new( M.api() )
---@type GroupRoster
M.group_roster = m.GroupRoster.new( M.api(), M.player_info )
M.chat_api = m.ChatApi.new()
---@type Chat
M.chat = m.Chat.new( M.chat_api, M.group_roster, M.player_info )
---@alias GroupAwareSoftResFn fun ( softres: SoftRes ): GroupAwareSoftRes
---@type GroupAwareSoftResFn
M.present_softres = function( softres ) return m.SoftResPresentPlayersDecorator.new( M.group_roster, softres ) end
---@type GroupAwareSoftResFn
M.absent_softres = function( softres ) return m.SoftResAbsentPlayersDecorator.new( M.group_roster, softres ) end
---@type ItemUtils
M.item_utils = m.ItemUtils
---@type TooltipReader
M.tooltip_reader = m.TooltipReader.new( M.api() )
-- TODO: Add type.
M.version_broadcast = m.VersionBroadcast.new( db( "version_broadcast" ), M.player_info, version.str )
---@type AwardedLoot
M.awarded_loot = m.AwardedLoot.new( db( "awarded_loot" ), M.group_roster, M.config )
-- TODO: Add type.
M.softres_db = db( "softres" )
-- TODO: Add type.
M.unfiltered_softres = m.SoftRes.new( M.softres_db )
-- TODO: Add type.
M.name_matcher = m.NameManualMatcher.new(
db( "name_matcher" ), M.api,
M.absent_softres( M.unfiltered_softres ),
m.NameAutoMatcher.new( M.group_roster, M.unfiltered_softres, 0.57, 0.4 ),
on_softres_status_changed
)
---@type SoftRes
M.matched_name_softres = m.SoftResMatchedNameDecorator.new( M.name_matcher, M.unfiltered_softres )
---@type SoftRes
M.awarded_loot_softres = m.SoftResAwardedLootDecorator.new( M.awarded_loot, M.matched_name_softres )
---@type GroupAwareSoftRes
M.softres = M.present_softres( M.awarded_loot_softres )
---@type DroppedLoot
M.dropped_loot = m.DroppedLoot.new( db( "dropped_loot" ) )
M.softres_check = m.SoftResCheck.new( M.matched_name_softres, M.group_roster, M.name_matcher, M.ace_timer,
M.absent_softres, db( "softres_check" ) )
---@type WinnerTracker
M.winner_tracker = m.WinnerTracker.new( db( "winner_tracker" ) )
---@type LootFacade
M.loot_facade = m.LootFacade.new( m.EventFrame.new( m.api ), m.api )
-- TODO: Add type.
---@diagnostic disable-next-line: unused-local, unused-function
local function get_dummy_items()
---@diagnostic disable-next-line: unused-function
local function item_link( name, id, quality )
local color = m.api.ITEM_QUALITY_COLORS[ quality ].hex or "|cffffffff"
return string.format( "%s|Hitem:%s::::::::20:257::::::|h[%s]|h|r", color, id or "3299", name )
end
-- local ids = { 17204, 16961, 18842, 16961, 16961, 18842, 16865, 16961, 17109, 16961, 18466, 11980, 12820, 3676 }
local ids = { 17109, 17109, 17109, 3676 }
local result = {}
---@type MakeDroppedItemFn
local make_dropped_item = m.ItemUtils.make_dropped_item
local boe = m.ItemUtils.BindType.BindOnEquip
---@diagnostic disable-next-line: unused-local
for i, item_id in ipairs( ids ) do
local name, tooltip_link, quality, texture
if m.vanilla then
name, tooltip_link, quality, _, _, _, _, _, texture = m.api.GetItemInfo( item_id )
else
name, tooltip_link, quality, _, _, _, _, _, _, texture = m.api.GetItemInfo( item_id )
end
local link = item_link( name, item_id, quality )
local item = make_dropped_item( item_id, name, link, tooltip_link, quality, 1, texture, boe, nil, true )
table.insert( result, item )
end
table.sort( result, function( a, b ) return a.quality > b.quality end )
return result
end
-- Enable this for testing in game. It will replace dropped items with the above.
local mock_items = false
---@type LootList
M.raw_loot_list = m.LootList.new( M.loot_facade, M.item_utils, M.tooltip_reader, m.BossList.zones, mock_items and get_dummy_items or nil )
---@type SoftResLootList
M.loot_list = m.SoftResLootListDecorator.new( M.raw_loot_list, M.softres )
---@type MasterLootCandidates
M.master_loot_candidates = m.MasterLootCandidates.new( M.api(), M.group_roster, M.raw_loot_list ) -- remove group_roster for testing (dummy candidates)
---@type MasterLootCandidateSelectionFrame
M.player_selection_frame = m.MasterLootCandidateSelectionFrame.new( m.FrameBuilder, M.config )
local rolling_popup_db = db( "rolling_popup" )
---@type RollingPopupContentTransformer
local rolling_popup_content_transformer = m.RollingPopupContentTransformer.new( M.config )
---@type RollingPopup
M.rolling_popup = m.RollingPopup.new(
popup_builder(),
rolling_popup_content_transformer,
rolling_popup_db,
M.config
)
---@type LootFrameSkin
local skin = M.config.classic_look() and m.OgLootFrameSkin.new( m.FrameBuilder ) or m.ModernLootFrameSkin.new( m.FrameBuilder )
---@type LootFrame
M.loot_frame = m.LootFrame.new(
skin,
db( "loot_frame" ),
M.config
)
---@type LootAwardPopup
M.loot_award_popup = m.LootAwardPopup.new(
popup_builder( classic and 38 or 30, classic and 65 or 55 ),
M.config,
M.rolling_popup
)
---@type RollController
M.roll_controller = m.RollController.new(
M.master_loot_candidates,
M.softres,
M.loot_list,
M.config,
M.rolling_popup,
M.loot_award_popup,
M.player_selection_frame
)
---@type LootAwardCallback
M.loot_award_callback = m.LootAwardCallback.new( M.awarded_loot, M.roll_controller, M.winner_tracker, M.group_roster, M.softres )
---@type MasterLoot
M.master_loot = m.MasterLoot.new(
M.master_loot_candidates,
M.loot_award_callback,
M.loot_list,
M.roll_controller
)
---@type AutoLoot
M.auto_loot = m.AutoLoot.new( M.loot_list, M.api, db( "auto_loot" ), M.config, M.player_info )
---@type DroppedLootAnnounce
M.dropped_loot_announce = m.DroppedLootAnnounce.new(
M.loot_list,
M.chat,
M.dropped_loot,
M.softres,
M.winner_tracker,
M.player_info,
M.auto_loot,
M.config
)
---@type WinnersPopup
M.winners_popup = m.WinnersPopup.new(
popup_builder (),
m.FrameBuilder,
db( "winners_popup" ),
M.awarded_loot,
M.roll_controller,
M.confirm_popup,
M.config
)
-- TODO: Add type.
M.softres_gui = m.SoftResGui.new( M.api, M.import_encoded_softres_data, M.softres_check, M.softres, clear_data, M.dropped_loot_announce.reset )
-- TODO: Add type.
M.trade_tracker = m.TradeTracker.new( M.ace_timer, M.chat, trade_complete_callback )
-- TODO: Add type.
M.usage_printer = m.UsagePrinter.new( M.chat )
-- TODO: Add type.
M.minimap_button = m.MinimapButton.new( M.api, db( "minimap_button" ), M.softres_gui.toggle, M.winners_popup.toggle, M.softres_check, M.config )
-- TODO: Add type.
M.master_loot_warning = m.MasterLootWarning.new( M.api, M.config, m.BossList.zones, M.player_info )
-- TODO: Add type.
M.new_group_event = m.NewGroupEvent.new( M.group_roster )
-- TODO: Add type.
M.auto_group_loot = m.AutoGroupLoot.new( M.loot_list, M.config, m.BossList.zones, M.player_info )
-- TODO: Add type.
M.auto_master_loot = m.AutoMasterLoot.new( M.config, m.BossList.zones, M.player_info )
-- TODO: Add type.
M.softres_roll_gui_data = m.SoftResRollGuiData.new( M.softres, M.group_roster )
-- TODO: Add type.
M.tie_roll_gui_data = m.TieRollGuiData.new( M.group_roster )
-- TODO: Add type.
M.welcome_popup = m.WelcomePopup.new( m.FrameBuilder, M.ace_timer, db( "welcome_popup" ) )
-- TODO: Add type.
M.roll_for_ad = m.RollForAd.new( M.player_info )
---@type RollingStrategyFactory
M.rolling_strategy_factory = m.RollingStrategyFactory.new(
M.group_roster,
M.loot_list,
M.master_loot_candidates,
M.chat,
M.ace_timer,
M.winner_tracker,
M.config,
M.softres,
M.player_info
)
---@type RollingLogic
M.rolling_logic = m.RollingLogic.new(
M.chat,
M.ace_timer,
M.roll_controller,
M.rolling_strategy_factory,
M.master_loot_candidates,
M.winner_tracker,
M.config
)
M.loot_controller = m.LootController.new(
M.player_info,
M.loot_facade,
M.loot_list,
M.loot_frame,
M.roll_controller,
M.softres,
M.rolling_logic,
M.chat
)
---@type ArgsParser
M.args_parser = m.ArgsParser.new( m.ItemUtils, M.config )
-- TODO: Add type.
M.roll_result_announcer = m.RollResultAnnouncer.new( M.chat, M.roll_controller, M.softres, M.config )
M.loot_facade_listener = m.LootFacadeListener.new(
M.loot_facade,
M.auto_loot,
M.dropped_loot_announce,
M.master_loot,
M.auto_group_loot,
M.roll_controller,
M.player_info
)
---@type ClientBroadcast
M.client_broadcast = m.ClientBroadcast.new(
M.roll_controller,
M.softres,
M.config
)
---@type Client
M.client = m.Client.new(
M.ace_timer,
M.player_info,
M.rolling_popup,
M.config
)
M.sandbox = m.Sandbox.new()
end
local function subscribe_for_component_events()
M.config.subscribe( "show_ml_warning", function( enabled )
if enabled then
M.master_loot_warning.on_player_target_changed()
else
M.master_loot_warning.hide()
end
end )
M.new_group_event.subscribe( function()
M.awarded_loot.clear()
M.dropped_loot.clear()
end )
M.config_event_bus.subscribe( "config_change_requires_ui_reload", function()
M.ui_reload_popup.show()
end )
end
function M.import_softres_data( softres_data )
M.unfiltered_softres.import( softres_data )
M.name_matcher.auto_match()
end
function M.import_encoded_softres_data( data, data_loaded_callback )
local sr = m.SoftRes
local softres_data = sr.decode( data )
if not softres_data and data and string.len( data ) > 0 then
info( "Could not load soft-res data!", m.colors.red )
return
elseif not softres_data then
M.minimap_button.set_icon( M.minimap_button.ColorType.White )
return
end
M.import_softres_data( softres_data )
info( "Soft-res data loaded successfully!" )
if data_loaded_callback then data_loaded_callback( softres_data ) end
update_minimap_icon()
end
local function on_roll_command( roll_slash_command )
return function( args )
if M.rolling_logic.is_rolling() then
M.chat.info( "Rolling is in progress." )
return
end
if string.find( args, "^debug" ) then
m.DebugBuffer.on_command( args )
return
end
if string.find( args, "^config" ) then
M.config.on_command( args )
return
end
if args == "versioncheck guild" then
M.version_broadcast.guild_version_request()
return
end
if not M.api().IsInGroup() then
M.chat.info( "Not in a group." )
return
end
if args == "versioncheck" then
M.version_broadcast.group_version_request()
return
end
if string.find( args, "^client enable" ) and M.player_info.is_master_looter() then
M.client_broadcast.enable_roll_popup()
return
end
local item, count, seconds, message = M.args_parser.parse( args )
if not item then
M.usage_printer.print_usage( roll_slash_command )
return
end
local strategy_type = m.Types.slash_command_to_strategy_type( roll_slash_command )
if not strategy_type then
info( string.format( "Unsupported command: %s", hl( roll_slash_command and roll_slash_command.slash_command or "?" ) ) )
return
end
if M.softres.is_item_hardressed( item.id ) then
M.roll_controller.preview( item, count )
return
end
M.roll_controller.start( strategy_type, item, count, seconds, message )
end
end
local function on_show_sorted_rolls_command( args )
if M.rolling_logic.is_rolling() then
info( "Rolling is in progress." )
return
end
if args then
for limit in string.gmatch( args, "(%d+)" ) do
M.rolling_logic.show_sorted_rolls( tonumber( limit ) )
return
end
end
M.rolling_logic.show_sorted_rolls( 5 )
end
local function is_rolling_check( f )
---@diagnostic disable-next-line: unused-vararg
return function( ... )
if not M.rolling_logic.is_rolling() then
M.chat.info( "Rolling not in progress." )
return
end
f( unpack( arg ) )
end
end
local function in_group_check( f )
return m.in_group_check( M.api(), M.chat, f )
end
local function setup_storage()
-- Reset old AceDB configuration. I don't give a fuck :)
if RollForDb and RollForDb.global and RollForDb.global.version then
RollForDb = nil
end
RollForDb = RollForDb or {}
RollForCharDb = RollForCharDb or {}
M.db = RollForDb
M.char_db = RollForCharDb
if not M.db.version then
M.db.version = version.str
end
end
local function on_softres_command( args )
if args == "init" then
clear_data()
end
M.softres_gui.toggle()
end
local function on_roll( player_name, roll, min, max )
local player = M.group_roster.find_player( player_name )
if not player then
m.err( string.format( "Player %s could not be found.", hl( player_name ) ) )
return
end
M.rolling_logic.on_roll( player, roll, min, max )
end
local function on_loot_method_changed()
M.master_loot_warning.on_party_loot_method_changed()
end
local function on_master_looter_changed( player_name )
if M.player_info.get_name() == player_name and m.is_master_loot() then
M.ace_timer.ScheduleTimer( M, M.config.print_raid_roll_settings, 0.1 )
end
end
function M.on_chat_msg_system( message )
for player_name, roll, min, max in string.gmatch( message, "([^%s]+) rolls (%d+) %((%d+)%-(%d+)%)" ) do
on_roll( player_name, tonumber( roll ), tonumber( min ), tonumber( max ) )
return
end
if string.find( message, "^Looting changed to" ) then
on_loot_method_changed()
return
end
for player_name in string.gmatch( message, "(.-) is now the loot master%." ) do
on_master_looter_changed( player_name )
return
end
for giver_name, item_name, recipient_name in string.gmatch( message, "([^%s]+) trades item (.+) to ([^%s]+)%." ) do
on_raid_trade( giver_name, recipient_name, item_name )
return
end
end
-- TODO: this can now be replaced by mocking LootList
---@diagnostic disable-next-line: unused-local, unused-function
local function simulate_loot_dropped( args )
---@diagnostic disable-next-line: unused-function
local function mock_table_function( name, values )
M.api()[ name ] = function( key )
local value = values[ key ]
if type( value ) == "function" then
return value()
else
return value
end
end
end
---@diagnostic disable-next-line: unused-function
local function make_loot_slot_info( count, quality )
local result = {}
for i = 1, count do
table.insert( result, function()
if i == count then
m.api = m.real_api
m.real_api = nil
end
return nil, nil, nil, quality or 4
end )
end
return result
end
local item_links = M.item_utils.parse_all_links( args )
if m.real_api then
info( "Mocking in progress." )
return
end
m.real_api = m.api
m.api = m.clone( m.api )
M.api()[ "GetNumLootItems" ] = function() return getn( item_links ) end
M.api()[ "UnitName" ] = function() return tostring( m.lua.time() ) end
M.api()[ "GetLootThreshold" ] = function() return 4 end
mock_table_function( "GetLootSlotLink", item_links )
mock_table_function( "GetLootSlotInfo", make_loot_slot_info( getn( item_links ), 4 ) )
M.dropped_loot_announce.on_loot_opened()
end
local function show_how_to_roll()
M.chat.announce( "How to roll:" )
local ms = M.config.ms_roll_threshold() ~= 100 and string.format( " (%s)", M.config.ms_roll_threshold() or "100" ) or ""
local sr = M.softres.get_all_rollers()
local sr_count = getn( sr )
M.chat.announce( string.format( "For main-spec%s, type: /roll%s", sr_count > 0 and " and soft-res" or "", ms ) )
M.chat.announce( string.format( "For off-spec, type: /roll %s", M.config.os_roll_threshold() ) )
if M.config.tmog_rolling_enabled() then
M.chat.announce( string.format( "For transmog, type: /roll %s", M.config.tmog_roll_threshold() ) )
end
end
local function on_reset_dropped_loot_announce_command()
M.dropped_loot_announce.reset()
end
local function setup_slash_commands()
-- Roll For commands
SLASH_RF1 = RollSlashCommand.NormalRoll
M.api().SlashCmdList[ "RF" ] = on_roll_command( RollSlashCommand.NormalRoll )
SLASH_ARF1 = RollSlashCommand.NoSoftResRoll
M.api().SlashCmdList[ "ARF" ] = in_group_check( on_roll_command( RollSlashCommand.NoSoftResRoll ) )
SLASH_RR1 = RollSlashCommand.RaidRoll
M.api().SlashCmdList[ "RR" ] = in_group_check( on_roll_command( RollSlashCommand.RaidRoll ) )
SLASH_IRR1 = RollSlashCommand.InstaRaidRoll
M.api().SlashCmdList[ "IRR" ] = in_group_check( on_roll_command( RollSlashCommand.InstaRaidRoll ) )
SLASH_HTR1 = "/htr"
M.api().SlashCmdList[ "HTR" ] = in_group_check( show_how_to_roll )
SLASH_CR1 = "/cr"
M.api().SlashCmdList[ "CR" ] = is_rolling_check( M.roll_controller.cancel_rolling )
SLASH_FR1 = "/fr"
M.api().SlashCmdList[ "FR" ] = is_rolling_check( M.roll_controller.finish_rolling_early )
SLASH_SSR1 = "/ssr"
M.api().SlashCmdList[ "SSR" ] = on_show_sorted_rolls_command
SLASH_RFR1 = "/rfr"
M.api().SlashCmdList[ "RFR" ] = on_reset_dropped_loot_announce_command
-- Soft Res commands
SLASH_SR1 = "/sr"
M.api().SlashCmdList[ "SR" ] = on_softres_command
SLASH_SRS1 = "/srs"
M.api().SlashCmdList[ "SRS" ] = M.softres_check.show_softres
SLASH_SRC1 = "/src"
M.api().SlashCmdList[ "SRC" ] = M.softres_check.check_softres
SLASH_SRO1 = "/sro"
M.api().SlashCmdList[ "SRO" ] = M.name_matcher.manual_match
SLASH_RFW1 = "/rfw"
M.api().SlashCmdList[ "RFW" ] = M.winners_popup.show
SLASH_RFT1 = "/rft"
M.api().SlashCmdList[ "RFT" ] = M.sandbox.run
--SLASH_DROPPED1 = "/DROPPED"
--M.api().SlashCmdList[ "DROPPED" ] = simulate_loot_dropped
end
function M.on_player_login()
setup_storage()
create_components()
subscribe_for_component_events()
setup_slash_commands()
info( string.format( "Loaded (%s).", hl( string.format( "v%s", version.str ) ) ) )
M.version_broadcast.broadcast()
M.import_encoded_softres_data( M.softres_db.data )
M.softres_gui.load( M.softres_db.data )
if M.welcome_popup.should_show() then
M.welcome_popup.show()
end
---@diagnostic disable-next-line: undefined-global
LootFrame:UnregisterAllEvents()
---@diagnostic disable-next-line: undefined-global
if pfLootFrame then pfLootFrame:UnregisterAllEvents() end
end
---@diagnostic disable-next-line: unused-local, unused-function
local function on_party_message( message, player )
for name, roll in string.gmatch( message, "(%a+) rolls (%d+)" ) do
on_roll( name, tonumber( roll ), 1, 100 )
end
for name, roll in string.gmatch( message, "(%a+) rolls os (%d+)" ) do
on_roll( name, tonumber( roll ), 1, 99 )
end
end
function M.unaward_item( player_name, item_id, item_link )
M.awarded_loot.unaward( player_name, item_id )
info( string.format( "%s returned %s.", hl( player_name ), item_link ) )
end
function M.on_group_changed()
M.name_matcher.auto_match()
update_minimap_icon()
end
function M.on_chat_msg_addon( name, message, _, sender )
if name ~= "RollFor" or not message then return end
for ver in string.gmatch( message, "VERSION::(.*)" ) do
M.version_broadcast.on_version( ver )
return
end
for channel, requesting_player_name in string.gmatch( message, "VERSION_REQUEST::(.-)::(.*)" ) do
M.version_broadcast.on_version_request( channel, requesting_player_name )
return
end
for requesting_player_name, channel, their_name, their_class, their_version in string.gmatch( message, "VERSION_RESPONSE::(.-)::(.-)::(.-)::(.-)::(.*)" ) do
M.version_broadcast.on_version_response( requesting_player_name, channel, their_name, their_class, their_version )
return
end
for data in string.gmatch( message, "ROLL::(.*)" ) do
M.client.on_message ( data, sender )
return
end
end
m.EventHandler.handle_events( M )
return M

42
src/ArgsParser.lua Normal file
View File

@ -0,0 +1,42 @@
RollFor = RollFor or {}
local m = RollFor
if m.ArgsParser then return end
local M = {}
---@type MakeItemFn
local make_item = m.ItemUtils.make_item
---@alias ParseArgsFn fun( args: string ):
--- Item,
--- number, -- item count
--- number, -- seconds
--- string -- message
---@class ArgsParser
---@field parse ParseArgsFn
---@return ArgsParser
function M.new( item_utils, config )
local function parse( args )
for item_count, link, seconds, message in string.gmatch( args, "(%d*)[xX]?%s*(|%w+|Hitem.+|r)%s*(%d*)%s*(.*)" ) do
local count = (not item_count or item_count == "") and 1 or tonumber( item_count )
local id = item_utils.get_item_id( link )
local name = item_utils.get_item_name( link )
local quality, texture = m.get_item_quality_and_texture( m.api, id )
local item = make_item( id, name, link, quality, texture )
local secs = seconds and seconds ~= "" and seconds ~= " " and tonumber( seconds ) or config.default_rolling_time_seconds()
return item, count, secs < 4 and 4 or secs > 15 and 15 or secs, message
end
end
return {
parse = parse
}
end
m.ArgsParser = M
return M

59
src/AutoGroupLoot.lua Normal file
View File

@ -0,0 +1,59 @@
RollFor = RollFor or {}
local m = RollFor
if m.AutoGroupLoot then return end
local M = {}
local getn = m.getn
local ignore_zones = {
"Blackwing Lair"
}
---@class AutoGroupLoot
---@field on_loot_opened fun()
---@field on_loot_slot_cleared fun()
---@param loot_list LootList
---@param config Config
---@param boss_list BossList
---@param player_info PlayerInfo
function M.new( loot_list, config, boss_list, player_info )
local m_target_name
local m_item_count
local function on_loot_opened()
m_target_name = m.target_name()
m_item_count = getn( loot_list.get_items() )
end
local function on_loot_slot_cleared()
if m_item_count == nil then
-- In case this is called before on_loot_opened
return
end
m_item_count = m_item_count - 1
if m_item_count > 0 then return end
if not m_item_count or m_item_count > 0 then return end
local zone_name = m.api.GetRealZoneText()
if m.table_contains_value( ignore_zones, zone_name ) then return end
local bosses = boss_list[ zone_name ] or {}
local is_a_boss = m.table_contains_value( bosses, m_target_name )
if is_a_boss and config.auto_group_loot() and m.is_master_loot() and player_info.is_leader() then
m.api.SetLootMethod( "group" )
end
end
---@type AutoGroupLoot
return {
on_loot_opened = on_loot_opened,
on_loot_slot_cleared = on_loot_slot_cleared
}
end
m.AutoGroupLoot = M
return M

232
src/AutoLoot.lua Normal file
View File

@ -0,0 +1,232 @@
RollFor = RollFor or {}
local m = RollFor
if m.AutoLoot then return end
local item_utils = m.ItemUtils
local info = m.pretty_print
local hl = m.colors.hl
local grey = m.colors.grey
local M = {}
local getn = m.getn
M.interface = {
on_loot_opened = "function",
loot_item = "function"
}
local button_visible = false
local _G = getfenv( 0 )
---@class AutoLoot
---@field is_auto_looted fun( item: DroppedItem ): boolean
---@field on_loot_opened fun()
---@field add fun( item_link: string )
---@field remove fun( item_link: string )
---@field clear fun()
---@field loot_item fun( slot: number )
---@param loot_list LootList
---@param api function
---@param db table
---@param config Config
function M.new( loot_list, api, db, config, player_info )
db.items = db.items or {}
local frame
local items = db.items
local function find_my_candidate_index( slot )
for i = 1, 40 do
if m.vanilla then
local name = m.api.GetMasterLootCandidate( i )
if name == api().UnitName( "player" ) then
return i
end
else
local name = m.api.GetMasterLootCandidate( slot, i )
if name == api().UnitName( "player" ) then
return i
end
end
end
end
local function is_auto_looted( item )
if not config.auto_loot() then
return false
end
local zone_name = api().GetRealZoneText()
local item_ids = items[ zone_name ] or {}
local threshold = api().GetLootThreshold()
local quality = item.quality or 0
if item_ids[ item.id ] then
return true
end
if item.bind == item_utils.BindType.BindOnPickup or item.bind == item_utils.BindType.Quest then
return false
end
if quality < threshold then
return true
end
return false
end
local function on_auto_loot()
if not player_info.is_master_looter() or not config.auto_loot() then
return
end
for _, item in ipairs( loot_list.get_items() ) do
local slot = loot_list.get_slot( item.id )
-- Looting coins is hidden under a secure button and cannot be done
-- through vanilla API. If the user has the SuperWoW mod, we can call an
-- extra function instead.
if config.superwow_auto_loot_coins() and api().SUPERWOW_VERSION and item.type == item_utils.LootType.Coin then
api().LootSlot( slot, 1 )
local coin = item --[[@as Coin]]
local amount = string.gsub( string.gsub( coin.amount_text, "\n", " " ), " $", "" )
if config.auto_loot_messages() then
info( string.format( "Auto-looting %s.", grey( amount ) ) )
end
end
if item.id and slot then
if is_auto_looted( item ) then
local index = find_my_candidate_index( slot )
if index then
api().GiveMasterLoot( slot, index )
if config.auto_loot_messages() then
info( string.format( "Auto-looting %s.", item.link ) )
end
end
end
end
end
end
local function create_frame()
frame = api().CreateFrame( "BUTTON", nil, api().LootFrame, "UIPanelButtonTemplate" )
frame:SetWidth( 90 )
frame:SetHeight( 23 )
frame:SetText( "Auto Loot" )
frame:SetPoint( "TOPRIGHT", api().LootFrame, "TOPRIGHT", -75, -44 )
frame:SetScript( "OnClick", on_auto_loot )
frame:Show()
end
local function on_loot_opened()
if button_visible then
if not frame then create_frame() end
local zone_name = api().GetRealZoneText()
local item_ids = items[ zone_name ]
if not item_ids or getn( item_ids ) == 0 then
frame:Hide()
else
frame:Show()
end
end
if not m.is_shift_key_down() then on_auto_loot() end
end
local function show_usage()
info( string.format( "Usage: %s %s", hl( "/rfal <add||remove>" ), grey( "<item_link>" ) ) )
end
local function add( item_link )
local item_id = item_utils.get_item_id( item_link )
if not item_id then
show_usage()
return
end
local zone_name = api().GetRealZoneText()
if not items[ zone_name ] then
items[ zone_name ] = {}
end
items[ zone_name ][ item_id ] = {
item_name = item_utils.get_item_name( item_link ),
item_link = item_link
}
info( string.format( "%s added.", item_link ), "auto-loot" )
end
local function remove( item_link )
local item_id = item_utils.get_item_id( item_link )
if not item_id then
show_usage()
return
end
local zone_name = api().GetRealZoneText()
if not items[ zone_name ] or not items[ zone_name ][ item_id ] then
return
end
items[ zone_name ][ item_id ] = nil
info( string.format( "%s removed.", item_link ), "auto-loot" )
end
local function clear()
end
local function on_command( args )
for item_link in string.gmatch( args, "add (.*)" ) do
add( item_link )
return
end
for item_link in string.gmatch( args, "remove (.*)" ) do
remove( item_link )
return
end
show_usage()
end
local function loot_item( slot )
local index = find_my_candidate_index()
if index then
api().GiveMasterLoot( slot, index )
end
end
_G[ "SLASH_RFAL1" ] = "/rfal"
_G[ "SlashCmdList" ][ "RFAL" ] = on_command
---@type AutoLoot
return {
is_auto_looted = is_auto_looted,
on_loot_opened = on_loot_opened,
add = add,
remove = remove,
clear = clear,
loot_item = loot_item
}
end
m.AutoLoot = M
return M

40
src/AutoMasterLoot.lua Normal file
View File

@ -0,0 +1,40 @@
RollFor = RollFor or {}
local m = RollFor
if m.AutoMasterLoot then return end
local M = {}
---@class AutoMasterLoot
---@field on_player_target_changed fun( arg1: string )
---@param config Config
---@param boss_list BossList
---@param player_info PlayerInfo
function M.new( config, boss_list, player_info )
local function on_player_target_changed( arg1 )
if not config.auto_master_loot() then return end
local target_name = m.target_name()
if not target_name or m.target_dead() then return end
local zone_name = m.api.GetRealZoneText()
local bosses = boss_list[ zone_name ] or {}
local is_a_boss = m.table_contains_value( bosses, target_name )
-- On Turtle, PLAYER_TARGET_CHANGED gets emitted with some float number as an argument automatically.
-- We don't want to respond to these events.
local auto_target = tonumber( arg1 )
if is_a_boss and not auto_target and not m.is_master_loot() and player_info.is_leader() then
m.api.SetLootMethod( "master", player_info.get_name() )
end
end
return {
on_player_target_changed = on_player_target_changed
}
end
m.AutoMasterLoot = M
return M

136
src/AwardedLoot.lua Normal file
View File

@ -0,0 +1,136 @@
RollFor = RollFor or {}
local m = RollFor
if m.AwardedLoot then return end
local M = m.Module.new( "AwardedLoot" )
local getn = m.getn
---@class AwardedLoot
---@field award fun( player_name: string, item_id: number, roll_data: RollData?, rolling_strategy: RollingStrategyType?, item_link: ItemLink?, player_class: PlayerClass?, sr_plus: number? )
---@field unaward fun( player_name: string, item_id: number )
---@field get_winners fun()
---@field has_item_been_awarded fun( player_name: string, item_id: number ): boolean
---@field has_item_been_awarded_to_any_player fun( item_id: ItemId ): boolean
---@field clear fun( force: boolean?)
---@field subscribe fun( event_type: string, callback: fun( data: any ) )
---@param db table
---@param group_roster GroupRoster
---@param config Config
function M.new( db, group_roster, config )
db.awarded_items = db.awarded_items or {}
local callbacks = {}
---@param player_name string
---@param item_id number
---@param roll_data RollData?
---@param rolling_strategy RollingStrategyType?
---@param item_link ItemLink?
---@param player_class PlayerClass?
---@param sr_plus number?
local function award( player_name, item_id, roll_data, rolling_strategy, item_link, player_class, sr_plus )
M.debug.add( "award" )
if not player_class then
if roll_data and roll_data.player_class then
player_class = roll_data.player_class
else
local player = group_roster.find_player( player_name )
player_class = player and player.class
end
end
local quality, _ = m.get_item_quality_and_texture( m.api, item_id )
if not item_link then
item_link = m.fetch_item_link( item_id, quality )
end
table.insert( db.awarded_items, {
player_name = player_name,
player_class = player_class,
item_id = item_id,
item_link = item_link,
quality = quality,
rolling_strategy = rolling_strategy,
roll_type = roll_data and roll_data.roll_type,
winning_roll = roll_data and roll_data.roll,
sr_plus = sr_plus
} )
end
local function subscribe( event_type, callback )
callbacks[ event_type ] = callbacks[ event_type ] or {}
table.insert( callbacks[ event_type ], callback )
end
local function notify_subscribers( event_type, data )
M.debug.add( event_type )
for _, callback in ipairs( callbacks[ event_type ] or {} ) do
callback( data )
end
end
---@return table
local function get_winners()
return db.awarded_items
end
---@param player_name string
---@param item_id number
---@return boolean
local function has_item_been_awarded( player_name, item_id )
for _, item in pairs( db.awarded_items ) do
if item.player_name == player_name and item.item_id == item_id then return true end
end
return false
end
---@param item_id ItemId
---@return boolean
local function has_item_been_awarded_to_any_player( item_id )
for _, item in pairs( db.awarded_items ) do
if item.item_id == item_id then return true end
end
return false
end
local function clear( force )
M.debug.add( "clear" )
if not config.keep_award_data() or force then
m.clear_table( db.awarded_items )
notify_subscribers( 'award_data_updated' )
end
end
---@param player_name string
---@param item_id number
local function unaward( player_name, item_id )
M.debug.add( "unaward" )
for i = getn( db.awarded_items ), 1, -1 do
local awarded_item = db.awarded_items[ i ]
if awarded_item.player_name == player_name and awarded_item.item_id == item_id then
table.remove( db.awarded_items, i )
notify_subscribers( 'award_data_updated' )
return
end
end
end
---@type AwardedLoot
return {
award = award,
unaward = unaward,
get_winners = get_winners,
has_item_been_awarded = has_item_been_awarded,
has_item_been_awarded_to_any_player = has_item_been_awarded_to_any_player,
clear = clear,
subscribe = subscribe
}
end
m.AwardedLoot = M
return M

114
src/BossList.lua Normal file
View File

@ -0,0 +1,114 @@
RollFor = RollFor or {}
local m = RollFor
if m.BossList then return end
---@class BossList
---@field zones table<string, string[]>
local M = {}
M.zones = {
[ "Durotar" ] = {
"Elder Mottled Boar"
},
[ "Tower of Karazhan" ] = {
"Master Blacksmith Rolfen",
"Brood Queen Araxxna",
"Grizikil",
"Clawlord Howlfang",
"Lord Blackwald II",
"Moroes"
},
[ "Zul'Gurub" ] = {
"High Priestess Jeklik",
"High Priest Venoxis",
"Witherbark Speaker",
"High Priestess Mar'li",
"Vilebranch Speaker",
"Broodlord Mandokir",
"Ohgan",
"Gri'lek",
"Hazza'rah",
"Renataki",
"Wushoolay",
"Gahz'ranka",
"High Priest Thekal",
"Zealot Zath",
"Zealot Lor'Khan",
"High Priestess Arlokk",
"Jin'do the Hexxer",
"Hakkar"
},
[ "Ruins of Ahn'Qiraj" ] = {
"Kurinnaxx",
"General Rajaxx",
"Moam",
"Buru the Gorger",
"Ayamiss the Hunter",
"Ossirian the Unscarred"
},
[ "Molten Core" ] = {
"Lucifron",
"Magmadar",
"Gehennas",
"Garr",
"Shazzrah",
"Baron Geddon",
"Golemagg the Incinerator",
"Sulfuron Harbinger",
"Majordomo Executus",
"Ragnaros"
},
[ "Blackwing Lair" ] = {
"Razorgore the Untamed",
"Vaelastrasz the Corrupt",
"Broodlord Lashlayer",
"Firemaw",
"Ebonroc",
"Flamegor",
"Chromaggus",
"Nefarian"
},
[ "Onyxia's Lair" ] = {
"Onyxia"
},
[ "Temple of Ahn'Qiraj" ] = {
"The Prophet Skeram",
"Vem",
"Lord Kri",
"Princess Yauj",
"Battle Guard Sartura",
"Fankriss the Unyielding",
"Viscidus",
"Princess Huhuran",
"Emperor Vek'lor",
"Emperor Vek'nilash",
"Ouro",
"C'Thun"
},
[ "Naxxramas" ] = {
"Patchwerk",
"Grobbulus",
"Gluth",
"Thaddius",
"Anub'Rekhan",
"Grand Widow Faerlina",
"Maexxna",
"Noth the Plaguebringer",
"Heigan the Unclean",
"Loatheb",
"Instructor Razuvious",
"Gothik the Harvester",
"Thane Korth'azz",
"Lady Blaumeux",
"Highlord Mograine",
"Sir Zeliek",
"Sapphiron",
"Kel'Thuzad"
}
}
---@type BossList
m.BossList = M
return M

53
src/Chat.lua Normal file
View File

@ -0,0 +1,53 @@
RollFor = RollFor or {}
local m = RollFor
if m.Chat then return end
local M = {}
---@class Chat
---@field announce fun( text: string, use_raid_Warning: boolean? )
---@field info fun( text: string, color_fn: ColorFn?, module_name: string? )
---@param api ChatApi
---@param group_roster GroupRoster
---@param player_info PlayerInfo
function M.new( api, group_roster, player_info )
local function get_group_chat_type()
return group_roster.am_i_in_raid() and "RAID" or "PARTY"
end
local function get_roll_announcement_chat_type( use_raid_warning )
local chat_type = get_group_chat_type()
if not use_raid_warning then return chat_type end
if chat_type == "RAID" and (player_info.is_leader() or player_info.is_assistant()) then
return "RAID_WARNING"
else
return chat_type
end
end
local function announce( text, use_raid_warning )
api.SendChatMessage( text, get_roll_announcement_chat_type( use_raid_warning ) )
end
local function info( message, color_fn, module_name )
if not message then return end
local c = color_fn and type( color_fn ) == "function" and color_fn or color_fn and type( color_fn ) == "string" and m.colors[ color_fn ] or m.colors.blue
local module_str = module_name and string.format( "%s%s%s", c( " [" ), m.colors.white( module_name ), c( "]" ) ) or ""
local frame = api.DEFAULT_CHAT_FRAME
if frame then frame:AddMessage( string.format( "%s%s: %s", c( "RollFor" ), module_str, message ) ) end
end
---@type Chat
return {
announce = announce,
info = info
}
end
m.Chat = M
return M

22
src/ChatApi.lua Normal file
View File

@ -0,0 +1,22 @@
RollFor = RollFor or {}
local m = RollFor
if m.ChatApi then return end
local _G = getfenv( 0 )
local M = {}
---@class ChatApi
---@field SendChatMessage fun( text: string, chat_type: string )
---@field DEFAULT_CHAT_FRAME table
function M.new()
---@type ChatApi
return {
SendChatMessage = _G.SendChatMessage or function() print( "PRINCESS KENI" ) end,
DEFAULT_CHAT_FRAME = _G.DEFAULT_CHAT_FRAME or { AddMessage = function() end }
}
end
m.ChatApi = M
return M

391
src/Client.lua Normal file
View File

@ -0,0 +1,391 @@
RollFor = RollFor or {}
local m = RollFor
if m.Client then return end
---@class Client
---@field on_message fun( data: string, sender: string )
local M = m.Module.new( "Client" )
local IU = m.ItemUtils ---@type ItemUtils
local RT = m.Types.RollType
local RS = m.Types.RollingStrategy
local S = m.Types.RollingStatus
local getn = m.getn
---@param ace_timer AceTimer
---@param player_info PlayerInfo
---@param rolling_popup RollingPopup
---@param config Config
function M.new( ace_timer, player_info, rolling_popup, config )
local roll_tracker ---@type RollTracker
local roll_threshold = {}
local show_rolling = false
local player_can_roll = false
local chunked_messages = {}
local var_names = {
i = "item",
t = "type",
l = "link",
tx = "texture",
q = "quality",
ic = "item_count",
s = "seconds",
sl = "seconds_left",
st = "strategy_type",
pn = "player_name",
pc = "player_class",
rt = "roll_type",
r = "roll",
n = "name",
c = "class",
th = "roll_threshold",
sr = "softressing_players",
ro = "rolls",
p = "players",
cl = "classes",
tm = "tmog"
}
setmetatable( var_names, { __index = function( _, key ) return key end } );
local function parse_table( str )
local function parse_inner( pos )
local tbl = {}
local key
local i = 1
while pos <= string.len( str ) do
local char = string.sub( str, pos, pos )
if char == "{" then
local newTable, newPos = parse_inner( pos + 1 )
if key then
tbl[ var_names[ key ] ] = newTable
key = nil
else
tbl[ i ] = newTable
i = i + 1
end
pos = newPos
elseif char == "}" then
return tbl, pos
elseif char == "[" then
local _, newPos, extracted_key = string.find( str, '%["*(.-)"*%]', pos )
key = tonumber( extracted_key ) and tonumber( extracted_key ) or extracted_key
pos = newPos
elseif char == "=" then
elseif char == "," then
key = nil
else
local _, newPos, raw_value = string.find( str, '([^,%]}]+)', pos )
if raw_value then
local value = tonumber( raw_value ) and tonumber( raw_value ) or raw_value
if key then
tbl[ var_names[ key ] ] = value
key = nil
else
tbl[ i ] = value
i = i + 1
end
pos = newPos
end
end
pos = pos + 1
end
return tbl, pos
end
local final_table = parse_inner( 1 )
return final_table[ 1 ]
end
---@param item_id number
---@param name string
---@param quality number
local function item_link( item_id, name, quality )
if not item_id then return end
local id = tonumber( item_id )
if not id or id == 0 then return end
local details = string.format( "item:%d:0:0:0", id )
return string.format( "%s|H%s|h[%s]|h|r", m.api.ITEM_QUALITY_COLORS[ quality or 0 ].hex, details, name )
end
local function close_rolling()
rolling_popup.hide()
show_rolling = false
end
---@param strategy_type RollingStrategyType
local function roll_buttons( strategy_type )
local buttons = {}
if player_can_roll then
if strategy_type == RS.NormalRoll then
table.insert( buttons, { type = "MSRoll", callback = function() m.api.RandomRoll( 1, roll_threshold[ RT.MainSpec ] ) end } )
table.insert( buttons, { type = "OSRoll", callback = function() m.api.RandomRoll( 1, roll_threshold[ RT.OffSpec ] ) end } )
if roll_threshold[ RT.Transmog ] > 0 then
table.insert( buttons, { type = "TMOGRoll", callback = function() m.api.RandomRoll( 1, roll_threshold[ RT.Transmog ] ) end } )
end
elseif strategy_type == RS.SoftResRoll or strategy_type == RS.TieRoll then
table.insert( buttons, { type = "Roll", callback = function() m.api.RandomRoll( 1, 100 ) end } )
end
end
table.insert( buttons, { type = "Close", callback = function() close_rolling() end } )
return buttons
end
local function tie_content()
if not roll_tracker then
M.debug.add( "roll_tracker not initialized" )
return
end
local tracker_data = roll_tracker.get()
local first_iteration = tracker_data.iterations[ 1 ]
local waiting = tracker_data.status.type == "Waiting" or false
local tie_iterations = {}
for i, iteration in ipairs( tracker_data.iterations ) do
if i > 1 then
table.insert( tie_iterations,
---@type TieIteration
{
tied_roll = iteration.tied_roll,
rolls = iteration.rolls
}
)
end
end
---@type RollingPopupTieData
local rolling_popup_data = {
---@type RollingPopupRollData
roll_data = {
item_link = tracker_data.item.link,
item_tooltip_link = IU.get_tooltip_link( tracker_data.item.link ),
item_texture = tracker_data.item.texture,
item_count = tracker_data.item_count,
rolls = first_iteration.rolls,
winners = tracker_data.winners,
strategy_type = first_iteration.rolling_strategy,
buttons = roll_buttons( RS.TieRoll ),
waiting_for_rolls = waiting or false,
type = "Roll"
},
tie_iterations = tie_iterations,
type = "Tie"
}
rolling_popup:show()
rolling_popup:refresh( rolling_popup_data )
end
---@param type string?
---@param awarded string?
local function roll_content( type, awarded )
if not roll_tracker then
M.debug.add( "roll_tracker not initialized" )
return
end
local tracker_data, current_iteration = roll_tracker.get()
local strategy_type = current_iteration and current_iteration.rolling_strategy
local waiting_for_rolls = tracker_data.status.type == "Waiting" or false
local seconds = not waiting_for_rolls and tracker_data.status.seconds_left or nil
if strategy_type == "TieRoll" then
tie_content()
return
end
---@type RollingPopupRollData
local rolling_popup_data = {
item_link = tracker_data.item.link,
item_tooltip_link = IU.get_tooltip_link( tracker_data.item.link ),
item_texture = tracker_data.item.texture,
item_count = tracker_data.item_count,
seconds_left = seconds,
rolls = current_iteration.rolls,
winners = tracker_data.winners,
awarded = awarded or nil,
buttons = roll_buttons( strategy_type ),
strategy_type = strategy_type,
waiting_for_rolls = waiting_for_rolls,
type = type and type or "Roll"
}
rolling_popup:show()
rolling_popup:refresh( rolling_popup_data )
local color = m.get_popup_border_color( tracker_data.item.quality )
rolling_popup:border_color( color )
end
local function on_command( command, data )
if command == "START_ROLL" then
if getn( data.softressing_players ) == 0 then
data.strategy_type = RS.NormalRoll
player_can_roll = true
elseif m.find( player_info.get_name(), data.softressing_players, 'name' ) then
player_can_roll = true
else
player_can_roll = false
end
if data.item.classes and getn( data.item.classes ) > 0 then
if m.find( player_info.get_class(), data.item.classes ) then
player_can_roll = true
else
player_can_roll = false
end
end
if not player_can_roll and config.client_show_roll_popup() ~= "Always" then
show_rolling = false
rolling_popup.hide()
return
end
show_rolling = true
roll_threshold.MainSpec = data.roll_threshold.ms
roll_threshold.OffSpec = data.roll_threshold.os
roll_threshold.Transmog = data.roll_threshold.tmog
data.item.texture = "Interface\\Icons\\" .. data.item.texture
data.item.name = string.gsub( data.item.name, "_", " " )
data.item.link = item_link( data.item.id, data.item.name, data.item.quality )
roll_tracker = m.RollTracker.new( data.item )
roll_tracker.start( data.strategy_type, data.item_count, data.seconds, nil, data.softressing_players )
if getn( data.softressing_players ) == 1 then
roll_tracker.finish( {} )
roll_tracker.add_winners( data.softressing_players )
end
roll_content()
elseif command == "ENABLE_ROLL_POPUP" then
config.enable_client_roll_popup()
end
if show_rolling then
if command == "ROLL" then
roll_tracker.add( data.player_name, data.player_class, data.roll_type, data.roll )
if data.player_name == player_info.get_name() then
player_can_roll = false
end
roll_content()
elseif command == "TICK" then
if not roll_tracker then
M.debug.add( "roll_tracker not initialized" )
return
end
local tracker_data = roll_tracker.get()
if tracker_data.status.type == S.Finished or tracker_data.status.type == S.Canceled then
return
end
roll_tracker.tick( data.seconds_left )
if data.seconds_left == 1 then
roll_tracker.waiting_for_rolls()
ace_timer.ScheduleTimer( M, function()
on_command( "TICK", { seconds_left = 0 } )
end, 2 )
end
roll_content()
elseif command == "FINISH" then
roll_tracker.finish( {} )
roll_tracker.add_winners( data )
player_can_roll = false
roll_content()
elseif command == "CANCEL_ROLL" then
roll_tracker.rolling_canceled()
player_can_roll = false
if config.client_auto_hide_popup() then
show_rolling = false
rolling_popup.hide()
else
roll_content( "RollingCanceled" )
end
elseif command == "TIE" then
roll_tracker.tie( data.players, data.roll_type, data.roll )
tie_content()
elseif command == "TIESTART" then
roll_tracker.tie_start()
local tracker_data = roll_tracker.get()
local last_iteration = tracker_data.iterations[ getn( tracker_data.iterations ) ]
if m.find( player_info.get_name(), last_iteration.rolls, 'player_name' ) then
player_can_roll = true
end
tie_content()
elseif command == "AWARDED" then
if data.player_name == player_info.get_name() then
m.api.PlaySound( "QUESTCOMPLETED" )
end
if config.client_auto_hide_popup() then
show_rolling = false
rolling_popup.hide()
else
roll_content( "Awarded", data )
end
end
end
end
local function on_message( data_str, sender )
local command = string.match( data_str, "^(.-)::" )
if sender == player_info.get_name() then return end
if config.client_show_roll_popup() == "Off" and command ~= "ENABLE_ROLL_POPUP" then return end
data_str = string.gsub( data_str, "^.-::", "" )
if command == "CHUNK" then
local chunk_num, total_chunks, chunk_content = string.match( data_str, "^(%d+)::(%d+)::(.+)$" )
chunked_messages[ sender ] = chunked_messages[ sender ] or {}
local sender_chunks = chunked_messages[ sender ]
sender_chunks[ tonumber( chunk_num ) ] = chunk_content
M.debug.add( (string.format( "Got chunk %d of %d", tonumber( chunk_num ), tonumber( total_chunks ) )) )
if getn( sender_chunks ) == tonumber( total_chunks ) then
data_str = table.concat( sender_chunks )
command = string.match( data_str, "^(.-)::" )
data_str = string.gsub( data_str, "^.-::", "" )
chunked_messages[ sender ] = nil
else
return
end
end
local data = data_str ~= "" and parse_table( data_str ) or {}
M.debug.add( string.format( "Received command %s", command ) )
on_command( command, data )
end
---@type Client
return {
on_message = on_message
}
end
m.Client = M
return M

185
src/ClientBroadcast.lua Normal file
View File

@ -0,0 +1,185 @@
RollFor = RollFor or {}
local m = RollFor
if m.ClientBroadcast then return end
---@class ClientBroadcast
---@field enable_roll_popup fun()
local M = m.Module.new( "ClientBroadcast" )
local ADDON_NAME = "RollFor"
local RS = m.Types.RollingStrategy
local getn = m.getn
---@param roll_controller RollController
---@param softres GroupAwareSoftRes
---@param config Config
function M.new( roll_controller, softres, config )
---@param command string
---@param data table?
local function broadcast( command, data )
local channel = m.api.IsInRaid() and "RAID" or "PARTY"
local data_str = data and string.gsub( m.dump( data ), "%s+", "" ) or ""
local chunk_size = 220
local function split_message( message )
local chunks = {}
local message_length = string.len( message )
for i = 1, message_length, chunk_size do
local chunk = string.sub( message, i, i + chunk_size - 1 )
table.insert( chunks, chunk )
end
return chunks
end
if string.len( data_str ) > chunk_size then
data_str = command .. "::" .. data_str
local chunks = split_message( data_str )
for i, chunk in ipairs( chunks ) do
M.debug.add( string.format( "Broadcasting %s, chunk %d of %d", command, i, getn( chunks ) ) )
m.api.SendAddonMessage( ADDON_NAME, string.format( "ROLL::CHUNK::%d::%d::%s", i, getn( chunks ), chunk ), channel )
end
else
M.debug.add( string.format( "Broadcasting %s", command ) )
m.api.SendAddonMessage( ADDON_NAME, string.format( "ROLL::%s::%s", command, data_str ), channel )
end
end
---@param data RollControllerStartData
local function on_start( data )
if data.strategy_type ~= RS.NormalRoll and data.strategy_type ~= RS.SoftResRoll then
return
end
local tex = data.item.texture and string.gsub( data.item.texture, "Interface\\Icons\\", "" ) or ""
local tmog_rolling_enabled = config.tmog_rolling_enabled() and (data.item.is_boss_loot or not config.auto_tmog_disable() )
local softressing_players = softres.get( data.item.id )
local sr_players = {}
for _, player in ipairs( softressing_players ) do
table.insert( sr_players, {
t = player.type,
n = player.name,
c = player.class,
ro = player.rolls
} )
end
broadcast( "START_ROLL", {
i = {
t = data.item.type,
id = data.item.id,
n = string.gsub( data.item.name, "%s", "_" ),
tx = tex,
q = data.item.quality,
cl = data.item.classes
},
ic = data.item_count,
s = data.seconds,
st = data.strategy_type,
sr = sr_players,
th = {
ms = config.ms_roll_threshold(),
os = config.os_roll_threshold(),
tm = tmog_rolling_enabled and config.tmog_roll_threshold() or 0
}
} )
end
---@param event_data RollingFinishedData
local function on_finish( event_data )
if event_data.roll_tracker_data.iterations[ 1 ].rolling_strategy ~= RS.NormalRoll and event_data.roll_tracker_data.iterations[ 1 ].rolling_strategy ~= RS.SoftResRoll then
return
end
local data = {}
for _, winner in ipairs( event_data.roll_tracker_data.winners ) do
table.insert( data, {
n = winner.name,
c = winner.class,
rt = winner.roll_type,
r = winner.winning_roll,
} )
end
broadcast( "FINISH", data )
end
---@param data { players: RollingPlayer[], item: Item, item_count: number, roll_type: RollType, roll: number, rerolling: boolean?, top_roll: boolean? }
local function on_tie( data )
local players = {}
for _, player in ipairs( data.players ) do
table.insert( players, {
n = player.name,
c = player.class,
t = player.type,
ro = player.rolls,
} )
end
broadcast( "TIE", {
rt = data.roll_type,
r = data.roll,
p = players
} )
end
---@param event_data TieStartData
local function on_tie_start( event_data )
broadcast( "TIESTART" )
end
---@param data { seconds_left: number }
local function on_tick( data )
broadcast( "TICK", {
sl = data.seconds_left
} )
end
local function cancel_rolling()
broadcast( "CANCEL_ROLL" )
end
---@param data LootAwardedData
local function on_loot_awarded( data )
broadcast( "AWARDED", {
pn = data.player_name,
pc = data.player_class,
id = data.item_id
} )
end
---@param data { player_name: PlayerName, player_class: PlayerClass, roll_type: RollType, roll: Roll }
local function on_roll( data )
broadcast( "ROLL", {
pn = data.player_name,
pc = data.player_class,
rt = data.roll_type,
r = data.roll
} )
end
local function enable_roll_popup()
m.pretty_print( "Broadcasting ENABLE_ROLL_POPUP" )
end
roll_controller.subscribe( "cancel_rolling", cancel_rolling )
roll_controller.subscribe( "start", on_start )
roll_controller.subscribe( "finish", on_finish )
roll_controller.subscribe( "there_was_a_tie", on_tie )
roll_controller.subscribe( "tie_start", on_tie_start )
roll_controller.subscribe( "tick", on_tick )
roll_controller.subscribe( "loot_awarded", on_loot_awarded )
roll_controller.subscribe( "roll", on_roll )
---@type ClientBroadcast
return {
enable_roll_popup = enable_roll_popup
}
end
m.ClientBroadcast = M
return M

518
src/Config.lua Normal file
View File

@ -0,0 +1,518 @@
RollFor = RollFor or {}
local m = RollFor
if m.Config then return end
local info = m.pretty_print
local print_header = m.print_header
local hl = m.colors.hl
local blue = m.colors.blue
local grey = m.colors.grey
local RollType = m.Types.RollType
local M = {}
---@alias Expansion
---| "Vanilla"
---| "BCC"
---@alias Config table
---@param db table
---@param event_bus EventBus
function M.new( db, event_bus )
local callbacks = {}
local toggles = {
[ "auto_loot" ] = { cmd = "auto-loot", display = "Auto-loot", help = "toggle auto-loot" },
[ "superwow_auto_loot_coins" ] = { cmd = "superwow-auto-loot-coins", display = "Auto-loot coins with SuperWoW", help = "toggle auto-loot coins with SuperWoW" },
[ "auto_loot_messages" ] = { cmd = "auto-loot-messages", display = "Auto-loot messages", help = "toggle auto-loot messages" },
[ "auto_loot_announce" ] = { cmd = "auto-loot-announce", display = "Announce auto-looted items", help = "toggle announcements of auto-loot items" },
[ "auto_class_announce" ] = { cmd = "auto-class-announce", display = "Announce class restriction on items", help = "toggle announcing of class restriction on items" },
[ "auto_tmog" ] = { cmd = "auto-tmog", display = "Disable transmog roll on trash loot", help ="toggle transmog roll on trash loot" },
[ "show_ml_warning" ] = { cmd = "ml", display = "Master loot warning", help = "toggle master loot warning" },
[ "auto_raid_roll" ] = { cmd = "auto-rr", display = "Auto raid-roll", help = "toggle auto raid-roll" },
[ "auto_group_loot" ] = { cmd = "auto-group-loot", display = "Auto group loot", help = "toggle auto group loot" },
[ "auto_master_loot" ] = { cmd = "auto-master-loot", display = "Auto master loot", help = "toggle auto master loot" },
[ "rolling_popup_lock" ] = { cmd = "rolling-popup-lock", display = "Rolling popup lock", help = "toggle rolling popup lock" },
[ "raid_roll_again" ] = { cmd = "raid-roll-again", display = string.format( "%s button", hl( "Raid roll again" ) ), help = string.format( "toggle %s button", hl( "Raid roll again" ) ) },
[ "loot_frame_cursor" ] = { cmd = "loot-frame-cursor", display = "Display loot frame at cursor position", help = "toggle displaying loot frame at cursor position"},
[ "classic_look" ] = { cmd = "classic-look", display = "Classic look", help = "toggle classic look", requires_reload = true },
[ "client_auto_hide_popup" ] = { cmd = "auto-hide", display = "Hide popup when rolling is complete", help = "toggle hiding of roll popup", client = true },
}
local function notify_subscribers( event, value )
if not callbacks[ event ] then return end
for _, callback in ipairs( callbacks[ event ] ) do
callback( value )
end
end
local function init()
if not db.ms_roll_threshold then db.ms_roll_threshold = 100 end
if not db.os_roll_threshold then db.os_roll_threshold = 99 end
if not db.tmog_roll_threshold then db.tmog_roll_threshold = 98 end
if not db.superwow_auto_loot_coins then db.superwow_auto_loot_coins = true end
if db.tmog_rolling_enabled == nil then db.tmog_rolling_enabled = true end
if db.auto_tmog_disable == nil then db.auto_tmog_disable = false end
if db.show_ml_warning == nil then db.show_ml_warning = false end
if db.default_rolling_time_seconds == nil then db.default_rolling_time_seconds = 8 end
if db.master_loot_frame_rows == nil then db.master_loot_frame_rows = 5 end
if db.auto_master_loot == nil then db.auto_master_loot = true end
if db.auto_loot == nil then db.auto_loot = true end
if db.auto_loot_announce == nil then db.auto_loot_announce = true end
if db.loot_frame_cursor == nil then db.loot_frame_cursor = false end
if db.client_show_roll_popup == nil then db.client_show_roll_popup = "Off" end
if db.client_auto_hide_popup == nil then db.client_auto_hide_popup = false end
if not db.award_filter then
db.award_filter = {
item_quality = { Uncommon = 1, Rare = 1, Epic = 1, Legendary = 1 },
winning_roll = {},
roll_type = { MainSpec = 1, OffSpec = 1, Transmog = 1, SoftRes = 1, RR = 1 }
}
end
m.classic = db.classic_look
end
local function print( toggle_key )
local toggle = toggles[ toggle_key ]
if not toggle then return end
local value = toggle.negate and not db[ toggle_key ] or db[ toggle_key ]
info( string.format( "%s is %s.", toggles[ toggle_key ].display, value and m.msg.enabled or m.msg.disabled ) )
notify_subscribers( toggle_key, value )
end
local function toggle( toggle_key )
return function()
if db[ toggle_key ] then
db[ toggle_key ] = false
else
db[ toggle_key ] = true
end
print( toggle_key )
if toggles[ toggle_key ].requires_reload then
event_bus.notify( "config_change_requires_ui_reload", { key = toggle_key } )
end
end
end
local function reset_rolling_popup()
info( "Rolling popup position has been reset." )
notify_subscribers( "reset_rolling_popup" )
end
local function reset_loot_frame()
info( "Loot frame position has been reset." )
notify_subscribers( "reset_loot_frame" )
end
local function print_roll_thresholds()
local ms_threshold = db.ms_roll_threshold
local os_threshold = db.os_roll_threshold
local tmog_threshold = db.tmog_roll_threshold
local tmog_info = string.format( ", %s %s", hl( "TMOG" ), tmog_threshold ) or ""
info( string.format( "Roll thresholds: %s %s, %s %s%s", hl( "MS" ), ms_threshold, hl( "OS" ), os_threshold, tmog_info ) )
end
local function print_transmog_rolling_setting( show_threshold )
if m.bcc then return end
local tmog_rolling_enabled = db.tmog_rolling_enabled
local threshold = show_threshold and tmog_rolling_enabled and string.format( " (%s)", hl( db.tmog_roll_threshold ) ) or ""
info( string.format( "Transmog rolling is %s%s.", tmog_rolling_enabled and m.msg.enabled or m.msg.disabled, threshold ) )
end
local function print_default_rolling_time()
info( string.format( "Default rolling time: %s seconds", hl( db.default_rolling_time_seconds ) ) )
end
local function print_master_loot_frame_rows()
info( string.format( "Master loot frame rows: %s", hl( db.master_loot_frame_rows ) ) )
end
local function print_settings()
print_header( "RollFor Configuration" )
print_default_rolling_time()
print_master_loot_frame_rows()
print_roll_thresholds()
print_transmog_rolling_setting()
for toggle_key, setting in pairs( toggles ) do
if not setting.hidden and not setting.client then
print( toggle_key )
end
end
m.print( string.format( "For more info, type: %s", hl( "/rf config help" ) ) )
end
local function print_client_roll()
info( string.format( "Show roll popup: %s", hl( db.client_show_roll_popup ) ) )
end
local function print_client_settings()
print_header( "RollFor Client Configuration" )
print_client_roll()
for toggle_key, setting in pairs( toggles ) do
if not setting.hidden and setting.client then
print( toggle_key )
end
end
m.print( string.format( "For more info, type: %s", hl( "/rf config client help" ) ) )
end
local function print_client_help()
local v = function( name ) return string.format( "%s%s%s", hl( "<" ), grey( name ), hl( ">" ) ) end
local function rfc( cmd ) return string.format( "%s%s", blue( "/rf config client" ), cmd and string.format( " %s", hl( cmd ) ) or "" ) end
print_header( "RollFor Client Configuration Help" )
m.print( string.format( "%s - show configuration", rfc() ) )
m.print( string.format( "%s %s - set when to show roll popup", rfc( "show-roll" ), v( "Off|Always|Eligible" ) ) )
for _, setting in pairs( toggles ) do
if not setting.hidden and setting.client then
m.print( string.format( "%s - %s", rfc( setting.cmd ), setting.help ) )
end
end
end
local function configure_default_rolling_time( args )
if args == "config default-rolling-time" then
print_default_rolling_time()
return
end
for value in string.gmatch( args, "config default%-rolling%-time (%d+)" ) do
local v = tonumber( value )
if v < 4 then
info( string.format( "Default rolling time must be at least %s seconds.", hl( "4" ) ) )
return
end
if v > 15 then
info( string.format( "Default rolling time must be at most %s seconds.", hl( "15" ) ) )
return
end
db.default_rolling_time_seconds = v
print_default_rolling_time()
return
end
info( string.format( "Usage: %s <seconds>", hl( "/rf config default-rolling-time" ) ) )
end
local function configure_master_loot_frame_rows( args )
if args == "config master-loot-frame-rows" then
print_master_loot_frame_rows()
return
end
for value in string.gmatch( args, "config master%-loot%-frame%-rows (%d+)" ) do
local v = tonumber( value )
if v < 5 then
info( string.format( "Master loot frame rows must be at least %s.", hl( "5" ) ) )
return
end
db.master_loot_frame_rows = v
print_master_loot_frame_rows()
notify_subscribers( "master_loot_frame_rows" )
return
end
info( string.format( "Usage: %s <rows>", hl( "/rf config master-loot-frame-rows" ) ) )
end
local function configure_ms_threshold( args )
for value in string.gmatch( args, "config ms (%d+)" ) do
db.ms_roll_threshold = tonumber( value )
print_roll_thresholds()
return
end
info( string.format( "Usage: %s <threshold>", hl( "/rf config ms" ) ) )
end
local function configure_os_threshold( args )
for value in string.gmatch( args, "config os (%d+)" ) do
db.os_roll_threshold = tonumber( value )
print_roll_thresholds()
return
end
info( string.format( "Usage: %s <threshold>", hl( "/rf config os" ) ) )
end
local function configure_tmog_threshold( args )
if args == "config tmog" then
db.tmog_rolling_enabled = not db.tmog_rolling_enabled
print_transmog_rolling_setting( true )
return
end
for value in string.gmatch( args, "config tmog (%d+)" ) do
db.tmog_roll_threshold = tonumber( value )
print_roll_thresholds()
return
end
info( string.format( "Usage: %s <threshold>", hl( "/rf config tmog" ) ) )
end
local function configure_client_roll( args )
for value in string.gmatch( args, "config client show%-roll (%a+)" ) do
if ({ off = true, always = true, eligible = true })[ string.lower( value ) ] then
db.client_show_roll_popup = string.upper( string.sub( value, 1, 1 ) ) .. string.lower( string.sub( value, 2 ) )
print_client_roll()
return
end
end
print_client_roll()
info( string.format( "Usage: %s <Off|Always|Eligible>", hl( "/rf config client show-roll" ) ) )
end
local function print_help()
local v = function( name ) return string.format( "%s%s%s", hl( "<" ), grey( name ), hl( ">" ) ) end
local function rfc( cmd ) return string.format( "%s%s", blue( "/rf config" ), cmd and string.format( " %s", hl( cmd ) ) or "" ) end
print_header( "RollFor Configuration Help" )
m.print( string.format( "%s - show configuration", rfc() ) )
m.print( string.format( "%s - toggle minimap icon", rfc( "minimap" ) ) )
m.print( string.format( "%s - lock/unlock minimap icon", rfc( "minimap lock" ) ) )
m.print( string.format( "%s - show default rolling time", rfc( "default-rolling-time" ) ) )
m.print( string.format( "%s - show master loot frame rows", rfc( "master-loot-frame-rows" ) ) )
m.print( string.format( "%s %s - set default rolling time", rfc( "default-rolling-time" ), v( "seconds" ) ) )
m.print( string.format( "%s - show MS rolling threshold ", rfc( "ms" ) ) )
m.print( string.format( "%s %s - set MS rolling threshold ", rfc( "ms" ), v( "threshold" ) ) )
m.print( string.format( "%s - show OS rolling threshold ", rfc( "os" ) ) )
m.print( string.format( "%s %s - set OS rolling threshold ", rfc( "os" ), v( "threshold" ) ) )
if m.vanilla then
m.print( string.format( "%s - toggle TMOG rolling", rfc( "tmog" ) ) )
m.print( string.format( "%s %s - set TMOG rolling threshold", rfc( "tmog" ), v( "threshold" ) ) )
end
for _, setting in pairs( toggles ) do
if not setting.hidden and not setting.client then
m.print( string.format( "%s - %s", rfc( setting.cmd ), setting.help ) )
end
end
m.print( string.format( "%s - reset rolling popup position", rfc( "reset-rolling-popup" ) ) )
m.print( string.format( "%s - reset loot frame position", rfc( "reset-loot-frame" ) ) )
m.print( string.format( "%s - show client configuration", rfc( "client" ) ) )
end
local function lock_minimap_button()
db.minimap_button_locked = true
info( string.format( "Minimap button is %s.", m.msg.locked ) )
notify_subscribers( "minimap_button_locked", true )
end
local function unlock_minimap_button()
db.minimap_button_locked = false
info( string.format( "Minimap button is %s.", m.msg.unlocked ) )
notify_subscribers( "minimap_button_locked", false )
end
local function hide_minimap_button()
db.minimap_button_hidden = true
notify_subscribers( "minimap_button_hidden", true )
end
local function show_minimap_button()
db.minimap_button_hidden = false
notify_subscribers( "minimap_button_hidden", false )
end
local function on_command( args )
if args == "config" then
print_settings()
return
end
if args == "config help" then
print_help()
return
end
if args == "config client" then
print_client_settings()
return
end
if args == "config client help" then
print_client_help()
return
end
if string.find( args, "^config client show%-roll" ) then
configure_client_roll( args )
return
end
for toggle_key, setting in pairs( toggles ) do
if args == string.format( "config client %s", setting.cmd ) and setting.client then
toggle( toggle_key )()
return
elseif args == string.format( "config %s", setting.cmd ) and not setting.client then
toggle( toggle_key )()
return
end
end
if args == "config reset-rolling-popup" then
reset_rolling_popup()
return
end
if args == "config reset-loot-frame" then
reset_loot_frame()
return
end
if args == "config minimap" then
if db.minimap_button_hidden then
show_minimap_button()
else
hide_minimap_button()
end
return
end
if args == "config minimap lock" then
if db.minimap_button_locked then
unlock_minimap_button()
else
lock_minimap_button()
end
return
end
if string.find( args, "^config ms" ) then
configure_ms_threshold( args )
return
end
if string.find( args, "^config os" ) then
configure_os_threshold( args )
return
end
if string.find( args, "^config tmog" ) then
configure_tmog_threshold( args )
return
end
if string.find( args, "^config default%-rolling%-time" ) then
configure_default_rolling_time( args )
return
end
if string.find( args, "^config master%-loot%-frame%-rows" ) then
configure_master_loot_frame_rows( args )
return
end
print_help()
end
local function subscribe( event, callback )
callbacks[ event ] = callbacks[ event ] or {}
table.insert( callbacks[ event ], callback )
end
local function roll_threshold( roll_type )
local threshold = (roll_type == RollType.MainSpec or roll_type == RollType.SoftRes) and db.ms_roll_threshold or
roll_type == RollType.OffSpec and db.os_roll_threshold or
db.tmog_roll_threshold
local threshold_str = string.format( "/roll%s", threshold == 100 and "" or string.format( " %s", threshold ) )
return {
value = threshold,
str = threshold_str
}
end
local function enable_client_roll_popup()
if db.client_show_roll_popup == "Off" then
db.client_show_roll_popup = "Eligible"
info( string.format( "Show roll popup has been set to %s by lootmaster.", hl( db.client_show_roll_popup ) ) )
end
end
init()
---@param setting_key string
---@param expansion Expansion?
---@param not_available_value any?
local function get( setting_key, expansion, not_available_value )
if expansion and (expansion == "Vanilla" and m.bcc or expansion == "BCC" and m.vanilla) then
return function()
return not_available_value
end
end
return function()
return db[ setting_key ]
end
end
local function printfn( setting_key ) return function() print( setting_key ) end end
local config = {
configure_ms_threshold = configure_ms_threshold,
configure_os_threshold = configure_os_threshold,
configure_tmog_threshold = configure_tmog_threshold,
hide_minimap_button = hide_minimap_button,
lock_minimap_button = lock_minimap_button,
minimap_button_hidden = get( "minimap_button_hidden" ),
minimap_button_locked = get( "minimap_button_locked" ),
ms_roll_threshold = get( "ms_roll_threshold" ),
on_command = on_command,
os_roll_threshold = get( "os_roll_threshold" ),
print = print,
print_help = print_help,
print_raid_roll_settings = printfn( "auto_raid_roll" ),
reset_rolling_popup = reset_rolling_popup,
reset_loot_frame = reset_loot_frame,
roll_threshold = roll_threshold,
show_minimap_button = show_minimap_button,
subscribe = subscribe,
tmog_roll_threshold = get( "tmog_roll_threshold" ),
tmog_rolling_enabled = get( "tmog_rolling_enabled", "Vanilla", false ),
unlock_minimap_button = unlock_minimap_button,
default_rolling_time_seconds = get( "default_rolling_time_seconds" ),
master_loot_frame_rows = get( "master_loot_frame_rows" ),
configure_master_loot_frame_rows = configure_master_loot_frame_rows,
client_show_roll_popup = get( "client_show_roll_popup" ),
client_auto_hide_popup = get( "client_auto_hide_popup" ),
enable_client_roll_popup = enable_client_roll_popup,
auto_tmog_disable = get( "auto_tmog_disable" ),
auto_class_announce = get( "auto_class_announce" ),
award_filter = get( "award_filter" ),
keep_award_data = get( "keep_award_data" ),
loot_frame_cursor = get( "loot_frame_cursor" )
}
for toggle_key, _ in pairs( toggles ) do
config[ toggle_key ] = get( toggle_key )
config[ "toggle_" .. toggle_key ] = toggle( toggle_key )
end
return config
end
m.Config = M
return M

124
src/ConfirmPopup.lua Normal file
View File

@ -0,0 +1,124 @@
RollFor = RollFor or {}
local m = RollFor
if m.ConfirmPopup then return end
local M = {}
local getn = m.getn
local button_defaults = {
width = 80,
height = 24,
scale = 0.76
}
---@class ConfirmPopup
---@field show fun( text: table, ufunc: function )
---@field hide fun()
---@field is_visible fun(): boolean
---@param popup_builder PopupBuilder
---@param config Config
function M.new( popup_builder, config )
local popup
local classic = config.classic_look()
local top_padding = classic and 18 or 14
local function create_popup()
local frame = popup_builder
:name( "RollForConfirmPopup" )
:point( { point = "CENTER", relative_point = "CENTER", x = 0, y = 100 } )
:sound()
:esc()
:gui_elements( m.GuiElements )
:backdrop_color( 0, 0, 0, 0.8 )
:border_color( 0.125, 0.624, 0.976, 0.3 )
:strata( "FULLSCREEN_DIALOG" )
:movable()
:build()
return frame
end
---@param text table
---@param on_yes function
---@param on_no function
local function make_content( text, on_yes, on_no )
local content = {}
for _, line in pairs( text ) do
table.insert( content, { type = "text", value = line } )
end
table.insert( content, { type = "button", label = "Yes", width = 80, on_click = on_yes } )
table.insert( content, { type = "button", label = "No", width = 80, on_click = on_no } )
return content
end
local function show( text, ufunc )
if not popup then popup = create_popup() end
popup:clear()
local function on_yes()
ufunc( true )
popup:Hide()
end
local function on_no()
ufunc( false )
popup:Hide()
end
for _, v in ipairs( make_content( text, on_yes, on_no ) ) do
popup.add_line( v.type, function( type, frame, lines )
if type == "text" then
frame:SetText( v.value )
elseif type == "button" then
frame:SetWidth( v.width or button_defaults.width )
frame:SetHeight( v.height or button_defaults.height )
frame:SetText( v.label or "" )
frame:SetScale( v.scale or button_defaults.scale )
frame:SetScript( "OnClick", v.on_click or function() end )
frame:SetFrameLevel( popup:GetFrameLevel() + 1 )
end
if type ~= "button" then
local count = getn( lines )
if count == 0 then
local y = -top_padding - (v.padding or 0)
frame:ClearAllPoints()
frame:SetPoint( "TOP", popup, "TOP", 0, y )
else
local line_anchor = lines[ count ].frame
frame:ClearAllPoints()
frame:SetPoint( "TOP", line_anchor, "BOTTOM", 0, v.padding and -v.padding or 0 )
end
end
end, v.padding )
end
popup:Show()
end
local function hide()
if popup then
popup:Hide()
end
end
local function is_visible()
return popup and popup:IsVisible() or false
end
---@type ConfirmPopup
return {
show = show,
hide = hide,
is_visible = is_visible
}
end
m.ConfirmPopup = M
return M

28
src/Db.lua Normal file
View File

@ -0,0 +1,28 @@
RollFor = RollFor or {}
local m = RollFor
if m.Db then return end
local M = {}
function M.new( db )
return function( module_name )
db[ module_name ] = db[ module_name ] or {}
local proxy = {}
local mt = {
__index = function( _, key )
return db[ module_name ][ key ]
end,
__newindex = function( _, key, value )
db[ module_name ][ key ] = value
end
}
setmetatable( proxy, mt )
return proxy
end
end
m.Db = M
return M

257
src/DebugBuffer.lua Normal file
View File

@ -0,0 +1,257 @@
RollFor = RollFor or {}
local m = RollFor
if m.DebugBuffer then return end
local M = {}
M.modules = {}
local getn = m.getn
-- Keeping a global index so we can later reconstruct the order of messages when printing only a subset of modules.
local message_index = 0
local pp = m.pretty_print
---@class DebugMessage
---@field index number
---@field text string
local colors = {
"ff8080", -- soft pink
"8aeb9f", -- mint green
"80b8ff", -- light blue
"ffb380", -- peach
"b880ff", -- lavender
"80ffb3", -- seafoam green
"ffa680", -- light orange
"80ffb3", -- pale turquoise
"b880ff", -- light purple
"ffb3b3", -- baby pink
"80e2ff", -- sky blue
"ffb380", -- cream
"b3ff80", -- lime cream
"ff80b3", -- rose pink
"80ffa6" -- pale mint
}
---@param text string
local function make_message( text )
message_index = message_index + 1
---@type DebugMessage
return { index = message_index, text = text }
end
---@class DebugBuffer
---@field add fun( message: string )
---@field show fun()
---@field enable fun( console: boolean )
---@field disable fun()
---@field toggle fun()
function M.new( module_name, max_size )
local messages = {} ---@type DebugMessage[]
local head = 0
local count = 0
local debug_enabled = false
local console_enabled = false
local function add( message )
head = head + 1
if head > max_size then
head = 1
end
messages[ head ] = make_message( message )
if count < max_size then
count = count + 1
end
if debug_enabled then
if console_enabled then
print( string.format( "[%s]: %s", module_name, message ) )
else
pp( message, m.colors.grey, module_name )
end
end
end
local function get()
local result = {}
local start = head - count + 1
if start < 1 then
start = start + max_size
end
for i = 1, count do
local idx = start + i - 1
if idx > max_size then
idx = idx - max_size
end
table.insert( result, messages[ idx ] )
end
return result
end
local function show()
for _, message in ipairs( get() ) do
pp( message.text, m.colors.grey, message.module_name )
end
end
local function print_debug_status()
if console_enabled then
print( string.format( "\n[%s]: Debug %s.", module_name, debug_enabled and "enabled" or "disabled" ) )
else
pp( string.format( "Debug %s.", debug_enabled and m.msg.enabled or m.msg.disabled, m.colors.grey, module_name ), m.colors.grey, module_name )
end
end
local function enable( console )
debug_enabled = true
console_enabled = console
print_debug_status()
end
local function disable()
debug_enabled = false
console_enabled = false
print_debug_status()
end
local function toggle()
debug_enabled = not debug_enabled
print_debug_status()
end
local result = {
add = add,
get = get,
show = show,
enable = enable,
disable = disable,
toggle = toggle,
is_enabled = function() return debug_enabled end
}
M.modules[ module_name ] = result
return result
end
M.disable_all = function()
for _, module in pairs( M.modules ) do
module.disable()
end
end
local function get_colors( module_names )
local result = {}
local color_count = getn( colors )
local color_index = math.random( color_count ) - 1
for _, module_name in ipairs( module_names ) do
if color_index == color_count then
color_index = 0
else
color_index = color_index + 1
end
result[ module_name ] = colors[ color_index ]
end
return result
end
---@param module_names string[]
local function show( module_names )
local c = get_colors( module_names )
local result = {}
for _, module_name in ipairs( module_names ) do
local mod = m[ module_name ]
if mod and mod.debug then
local dbg = mod.debug
local messages = dbg.get() ---@type DebugMessage[]
for _, message in ipairs( messages ) do
table.insert( result, { module_name = module_name, index = message.index, text = message.text } )
end
end
end
table.sort( result, function( a, b ) return a.index < b.index end )
if getn( result ) == 0 then
pp( "No debug messages.", m.colors.grey )
return
end
local msg = function( tag )
return string.format(
"Debug messages %s (%s %s):",
tag,
m.colors.blue( "RollFor" ),
m.colors.hl( "v" .. m.get_addon_version().str )
)
end
pp( msg( "start" ) )
for _, message in ipairs( result ) do
pp( message.text, m.colors.grey, m.colorize( c[ message.module_name ], message.module_name ) )
end
pp( msg( "end" ) )
end
function M.on_command( args )
---@param modules string
local function parse_module_names( modules )
local result = {}
for module_name in string.gmatch( modules, "%S+" ) do
table.insert( result, module_name )
end
return result
end
if args == "debug show" then
show( { "RollController", "LootController" } )
return
end
for command, modules in string.gmatch( args, "debug (.-) (.*)" ) do
local module_names = parse_module_names( modules )
if command == "show" then
show( module_names )
return
end
for _, module_name in ipairs( module_names ) do
local mod = m[ module_name ]
if mod and mod.debug then
local dbg = mod.debug
local f = dbg[ command ]
if f then f() end
end
end
return
end
end
m.DebugBuffer = M
return M

54
src/DroppedLoot.lua Normal file
View File

@ -0,0 +1,54 @@
RollFor = RollFor or {}
local m = RollFor
if m.DroppedLoot then return end
local M = {}
local getn = m.getn
---@class DroppedLoot
---@field get_dropped_item_id fun( item_name: string ): number
---@field get_dropped_item_name fun( item_id: number ): string
---@field add fun( item_id: number, item_name: string )
---@field clear fun()
---@param db table
---@return DroppedLoot
function M.new( db )
db.dropped_items = db.dropped_items or {}
local function get_dropped_item_id( item_name )
for _, item in pairs( db.dropped_items ) do
if item.name == item_name then return item.id end
end
return nil
end
local function get_dropped_item_name( item_id )
for _, item in pairs( db.dropped_items ) do
if item.id == item_id then return item.name end
end
return nil
end
local function add( item_id, item_name )
table.insert( db.dropped_items, { id = item_id, name = item_name } )
end
local function clear()
if getn( db.dropped_items ) == 0 then return end
m.clear_table( db.dropped_items )
end
return {
get_dropped_item_id = get_dropped_item_id,
get_dropped_item_name = get_dropped_item_name,
add = add,
clear = clear
}
end
m.DroppedLoot = M
return M

358
src/DroppedLootAnnounce.lua Normal file
View File

@ -0,0 +1,358 @@
RollFor = RollFor or {}
local m = RollFor
if m.DroppedLootAnnounce then return end
local M = {}
local getn = m.getn
local announce_limit = 6
local filter = m.filter
local BindType = m.ItemUtils.BindType
local ItemQuality = m.Types.ItemQuality
local function distinct( items )
local result = {}
local function exists( item )
for i = 1, getn( result ) do
if result[ i ].id == item.id then return true end
end
return false
end
for i = 1, getn( items ) do
local item = items[ i ]
if not exists( item ) then
table.insert( result, item )
end
end
return result
end
local function commify( t, f )
local result = ""
if getn( t ) == 0 then
return result
end
if getn( t ) == 1 then
return (f and f( t[ 1 ] ) or t[ 1 ])
end
for i = 1, getn( t ) - 1 do
if result ~= "" then
result = result .. ", "
end
result = result .. (f and f( t[ i ] ) or t[ i ])
end
result = result .. " and " .. (f and f( t[ getn( t ) ] ) or t[ getn( t ) ])
return result
end
local function stringify( announcements )
local result = {}
local function print_player( show_rolls )
return function( player )
local rolls = show_rolls and player.rolls > 1 and string.format( " [%s rolls]", player.rolls ) or ""
local sr_plus = player.sr_plus and string.format( " (+%s)", player.sr_plus ) or ""
return string.format( "%s%s%s", player.name, rolls, sr_plus )
end
end
for i = 1, getn( announcements ) do
local entry = announcements[ i ]
if entry.is_hardressed then
table.insert( result, {
text = string.format( "%s. %s (HR)", i, entry.item_link ),
entry = entry
} )
elseif entry.softres_count > 0 then
local count = entry.how_many_dropped
local prefix = count == 1 and "" or string.format( "%sx", count )
local f = print_player( entry.softres_count > 1 )
table.insert( result, {
text = string.format( "%s. %s%s (SR by %s)", i, prefix, entry.item_link, commify( entry.softressers, f ) ),
entry = entry
} )
else
local count = entry.how_many_dropped
local prefix = count == 1 and "" or string.format( "%sx", count )
table.insert( result, {
text = string.format( "%s. %s%s", i, prefix, entry.item_link ),
entry = entry
} )
end
end
return result
end
local function sort( announcements )
local hr = {}
local sr = {}
local free_roll = {}
for _, v in pairs( announcements ) do
if v.is_hardressed then
table.insert( hr, v )
elseif v.softres_count > 0 then
table.insert( sr, v )
else
table.insert( free_roll, v )
end
end
table.sort( free_roll, function( left, right )
if left.item_quality ~= right.item_quality then
return left.item_quality > right.item_quality
else
return left.item_name < right.item_name
end
end )
table.sort( sr, function( left, right )
if left.softres_count == 1 and left.softres_count == right.softres_count then
if left.item_quality == right.item_quality then
return left.softressers[ 1 ].name < right.softressers[ 1 ].name
else
return left.item_quality > right.item_quality
end
elseif left.softres_count ~= right.softres_count then
return left.softres_count < right.softres_count
else
if left.item_quality == right.item_quality then
return left.item_name < right.item_name
else
return left.item_quality > right.item_quality
end
end
end )
return m.merge( {}, hr, sr, free_roll )
end
function M.create_item_announcements( summary )
local result = {}
for i = 1, getn( summary ) do
local entry = summary[ i ]
local softres_count = getn( entry.softressers )
if entry.is_hardressed then
table.insert( result, {
item_link = entry.item.link,
item_name = entry.item.name,
item_quality = entry.item.quality,
is_hardressed = true,
softres_count = 0
} )
elseif softres_count == 0 then
table.insert( result, {
item_link = entry.item.link,
item_name = entry.item.name,
item_quality = entry.item.quality,
softres_count = 0,
how_many_dropped = entry.how_many_dropped
} )
elseif entry.how_many_dropped == softres_count then
for j = 1, softres_count do
table.insert( result, {
item_link = entry.item.link,
item_name = entry.item.name,
item_quality = entry.item.quality,
softres_count = 1,
how_many_dropped = 1,
softressers = { entry.softressers[ j ] }
} )
end
else
table.insert( result, {
item_link = entry.item.link,
item_name = entry.item.name,
item_quality = entry.item.quality,
softres_count = getn( entry.softressers ),
how_many_dropped = entry.how_many_dropped,
softressers = entry.softressers
} )
end
end
return stringify( sort( result ) )
end
---@param loot_list LootList
---@param softres GroupAwareSoftRes
---@param auto_loot AutoLoot
---@param config Config
function M.process_dropped_items( loot_list, softres, auto_loot, config )
local source_guid = loot_list.get_source_guid()
local threshold = m.api.GetLootThreshold()
local items = filter( loot_list.get_items(), function( item )
if auto_loot.is_auto_looted( item ) and not config.auto_loot_announce() or item.id == 29434 then return false end
local quality = item.quality or 0
if item.bind == BindType.BindOnPickup and quality >= ItemQuality.Uncommon then
return true
end
return quality >= threshold
end )
local summary = M.create_item_summary( items, softres )
return source_guid or "unknown", items, M.create_item_announcements( summary )
end
-- SoftResLootListDecorator?
function M.create_item_summary( items, softres )
local result = {}
local distinct_items = distinct( items )
local function count_items( item_id )
---@diagnostic disable-next-line: redefined-local
local result = 0
for i = 1, getn( items ) do
if items[ i ].id == item_id then result = result + 1 end
end
return result
end
for i = 1, getn( distinct_items ) do
local item = distinct_items[ i ]
local item_count = count_items( item.id )
local softressers = softres.get( item.id )
local softres_count = getn( softressers )
table.sort( softressers, function( l, r ) return l.name < r.name end )
local hardressed = softres.is_item_hardressed( item.id )
if hardressed then
table.insert( result, { item = item, how_many_dropped = 1, softressers = {}, is_hardressed = hardressed } )
item_count = item_count - 1
end
if item_count > 0 then
if item_count > softres_count and softres_count > 0 then
table.insert( result, { item = item, how_many_dropped = softres_count, softressers = softressers, is_hardressed = false } )
table.insert( result, { item = item, how_many_dropped = item_count - softres_count, softressers = {}, is_hardressed = false } )
else
table.insert( result, { item = item, how_many_dropped = item_count, softressers = softressers, is_hardressed = false } )
end
end
end
return result
end
local function should_announce( i, item_count, announcement )
if i < announce_limit then return true end
if i == announce_limit and item_count == announce_limit then return true end
if announcement.entry.softres_count and announcement.entry.softres_count > 0 then
return true
end
if i == item_count then return true end
return false
end
---@class DroppedLootAnnounce
---@field on_loot_opened fun()
---@field reset fun()
---@param loot_list LootList
---@param chat Chat
---@param dropped_loot DroppedLoot
---@param softres GroupAwareSoftRes
---@param winner_tracker WinnerTracker
---@param player_info PlayerInfo
---@param config Config
function M.new( loot_list, chat, dropped_loot, softres, winner_tracker, player_info, auto_loot, config )
local announcing = false
local announced_source_ids = {}
local function on_loot_opened()
if not player_info.is_master_looter() or announcing then
-- Wtf is this?
if m.real_api then
m.api = m.real_api
m.real_api = nil
end
return
end
local source_guid, items, announcements = M.process_dropped_items( loot_list, softres, auto_loot, config )
local was_announced = announced_source_ids[ source_guid ]
if was_announced then return end
announcing = true
local item_count = getn( items )
local target = m.api.UnitName( "target" )
local target_msg = target and not m.api.UnitIsFriend( "player", "target" ) and string.format( "%s dropped ", target ) or ""
if item_count > 0 then
chat.announce(
string.format( "%s%s item%s%s", target_msg, item_count, item_count > 1 and "s" or "", target_msg == "" and " dropped:" or ":" ) )
for i = 1, item_count do
local item = items[ i ]
dropped_loot.add( item.id, item.name )
end
local trimmed = false
for i, announcement in ipairs( announcements ) do
if not trimmed and should_announce( i, item_count, announcement ) then
chat.announce( announcement.text )
if announcement.entry.softres_count == 1 then
winner_tracker.track( announcement.entry.softressers[ 1 ].name, announcement.entry.item_link, m.Types.RollType.SoftRes,
nil, m.Types.RollingStrategy.SoftResRoll )
end
elseif not trimmed then
if i > (announce_limit - 1) and item_count > announce_limit then
local count = item_count - i + 1
chat.announce( string.format( "and %s more item%s...", count, count > 1 and "s" or "" ) )
trimmed = true
end
end
end
announced_source_ids[ source_guid ] = true
end
announcing = false
end
local function reset()
local former_size = m.count_elements( announced_source_ids )
announced_source_ids = {}
if former_size > 0 then
m.pretty_print( "Loot announcement has been reset." )
end
end
---@type DroppedLootAnnounce
return {
on_loot_opened = on_loot_opened,
reset = reset
}
end
m.DroppedLootAnnounce = M
return M

38
src/EventBus.lua Normal file
View File

@ -0,0 +1,38 @@
RollFor = RollFor or {}
local m = RollFor
if m.EventBus then return end
local M = {}
---@class EventBus
---@field subscribe fun( event_name: string, callback: function )
---@field notify fun( event_name: string, data: any? )
function M.new()
local subscribers = {}
---@param event_name string
---@param callback fun()
local function subscribe( event_name, callback )
subscribers[ event_name ] = subscribers[ event_name ] or {}
table.insert( subscribers[ event_name ], callback )
end
---@param event_name string
---@param data any
local function notify( event_name, data )
for _, callback in ipairs( subscribers[ event_name ] or {} ) do
callback( data )
end
end
---@type EventBus
return {
subscribe = subscribe,
notify = notify
}
end
m.EventBus = M
return M

59
src/EventFrame.lua Normal file
View File

@ -0,0 +1,59 @@
RollFor = RollFor or {}
local m = RollFor
if m.EventFrame then return end
local M = m.Module.new( "EventFrame" )
---@diagnostic disable-next-line: undefined-field
local lua50 = table.setn and true or false
function M.new( api )
local frame = api.CreateFrame( "Frame" )
local event_handlers = {}
local function subscribe( event_name, callback )
if not event_name then error( "event_name was nil." ) end
if not event_handlers[ event_name ] then
frame:RegisterEvent( event_name )
end
event_handlers[ event_name ] = event_handlers[ event_name ] or {}
table.insert( event_handlers[ event_name ], callback )
end
local function event_handler( event, arg1, arg2, arg3, arg4, arg5 )
for event_name, handlers in pairs( event_handlers ) do
if event_name == event then
M.debug.add( event_name )
for _, handle_event in ipairs( handlers ) do
handle_event( arg1, arg2, arg3, arg4, arg5 )
end
end
end
end
frame:SetScript( "OnEvent", function( _, _event, _arg1, _arg2, _arg3, _arg4, _arg5 )
---@diagnostic disable-next-line: undefined-global
local event = lua50 and event or _event
---@diagnostic disable-next-line: undefined-global
local arg1 = lua50 and arg1 or _arg1
---@diagnostic disable-next-line: undefined-global
local arg2 = lua50 and arg2 or _arg2
---@diagnostic disable-next-line: undefined-global
local arg3 = lua50 and arg3 or _arg3
---@diagnostic disable-next-line: undefined-global
local arg4 = lua50 and arg4 or _arg4
---@diagnostic disable-next-line: undefined-global
local arg5 = lua50 and arg5 or _arg5
---@diagnostic disable-next-line: undefined-global
event_handler( event, arg1, arg2, arg3, arg4, arg5 )
end )
return {
subscribe = subscribe
}
end
m.EventFrame = M
return M

121
src/EventHandler.lua Normal file
View File

@ -0,0 +1,121 @@
RollFor = RollFor or {}
local m = RollFor
if m.EventHandler then return end
local M = {}
---@diagnostic disable-next-line: undefined-field
local lua50 = table.setn and true or false
function M.handle_events( main )
local init = false
local function event_handler( _, _event, _arg1, _arg2, _arg3, _arg4, _arg5 )
---@diagnostic disable-next-line: undefined-global
local event = lua50 and event or _event
---@diagnostic disable-next-line: undefined-global
local arg1 = lua50 and arg1 or _arg1
---@diagnostic disable-next-line: undefined-global
local arg2 = lua50 and arg2 or _arg2
---@diagnostic disable-next-line: undefined-global
local arg3 = lua50 and arg3 or _arg3
---@diagnostic disable-next-line: undefined-global
local arg4 = lua50 and arg4 or _arg4
---@diagnostic disable-next-line: undefined-global
local arg5 = lua50 and arg5 or _arg5
if event == "PLAYER_LOGIN" then
main.on_player_login()
init = true
return
end
if not init then return end
if event == "GROUP_ROSTER_UPDATE" or event == "PARTY_MEMBERS_CHANGED" then
main.version_broadcast.on_group_changed()
main.on_group_changed()
main.new_group_event.on_group_changed()
elseif event == "CHAT_MSG_PARTY" then
main.roll_for_ad.on_chat_msg_party( arg1, arg2 )
-- main.on_chat_msg_system( arg1, arg2, arg3, arg4, arg5 )
elseif event == "CHAT_MSG_RAID" then
main.roll_for_ad.on_chat_msg_raid( arg1, arg2 )
elseif event == "CHAT_MSG_RAID_LEADER" then
main.roll_for_ad.on_chat_msg_raid( arg1, arg2 )
elseif event == "CHAT_MSG_WHISPER_INFORM" then
main.roll_for_ad.on_chat_msg_whisper_inform( arg1, arg2 )
elseif event == "CHAT_MSG_SYSTEM" then
main.on_chat_msg_system( arg1, arg2, arg3, arg4, arg5 )
elseif event == "CHAT_MSG_ADDON" then
main.on_chat_msg_addon( arg1, arg2, arg3, arg4 )
elseif event == "TRADE_SHOW" then
main.trade_tracker.on_trade_show()
elseif event == "TRADE_PLAYER_ITEM_CHANGED" then
main.trade_tracker.on_trade_player_item_changed( arg1, arg2, arg3, arg4, arg5 )
elseif event == "TRADE_TARGET_ITEM_CHANGED" then
main.trade_tracker.on_trade_target_item_changed( arg1, arg2, arg3, arg4, arg5 )
elseif event == "TRADE_CLOSED" then
main.trade_tracker.on_trade_closed()
elseif event == "TRADE_ACCEPT_UPDATE" then
main.trade_tracker.on_trade_accept_update( arg1, arg2, arg3, arg4, arg5 )
elseif event == "TRADE_REQUEST_CANCEL" then
main.trade_tracker.on_trade_request_cancel()
elseif event == "PLAYER_TARGET_CHANGED" then
main.master_loot_warning.on_player_target_changed()
main.auto_master_loot.on_player_target_changed( arg1 )
elseif event == "UI_ERROR_MESSAGE" then
local message = m.vanilla and arg1 or arg2
if message == "That player's inventory is full" then
main.master_loot.on_recipient_inventory_full()
main.roll_controller.player_has_full_bags()
elseif message == "You are too far away to loot that corpse." then
main.master_loot.on_player_is_too_far()
elseif message == "Player has too many of that item already" then
main.roll_controller.player_already_has_unique_item()
elseif message == "Player not found" then
main.roll_controller.player_not_found()
elseif message == "Can't assign item to that player" then
main.roll_controller.cant_assign_item_to_that_player()
else
main.master_loot.on_unknown_error_message( message )
end
end
end
local frame = m.api.CreateFrame( "FRAME", "RollForFrame" )
frame:RegisterEvent( "PLAYER_LOGIN" )
frame:RegisterEvent( "GROUP_JOINED" )
frame:RegisterEvent( "GROUP_LEFT" )
frame:RegisterEvent( "GROUP_FORMED" )
frame:RegisterEvent( "CHAT_MSG_SYSTEM" )
frame:RegisterEvent( "CHAT_MSG_ADDON" )
frame:RegisterEvent( "CHAT_MSG_PARTY" )
frame:RegisterEvent( "CHAT_MSG_RAID" )
frame:RegisterEvent( "CHAT_MSG_RAID_LEADER" )
frame:RegisterEvent( "CHAT_MSG_WHISPER_INFORM" )
frame:RegisterEvent( "OPEN_MASTER_LOOT_LIST" )
frame:RegisterEvent( "TRADE_SHOW" )
frame:RegisterEvent( "TRADE_PLAYER_ITEM_CHANGED" )
frame:RegisterEvent( "TRADE_TARGET_ITEM_CHANGED" )
frame:RegisterEvent( "TRADE_CLOSED" )
frame:RegisterEvent( "TRADE_ACCEPT_UPDATE" )
frame:RegisterEvent( "TRADE_REQUEST_CANCEL" )
frame:RegisterEvent( "UI_ERROR_MESSAGE" )
frame:RegisterEvent( "PLAYER_TARGET_CHANGED" )
frame:RegisterEvent( "ZONE_CHANGED" )
frame:RegisterEvent( "ZONE_CHANGED_NEW_AREA" )
if m.vanilla then
frame:RegisterEvent( "PARTY_MEMBERS_CHANGED" )
else
frame:RegisterEvent( "GROUP_ROSTER_UPDATE" )
end
frame:SetScript( "OnEvent", event_handler )
end
m.EventHandler = M
return M

626
src/FrameBuilder.lua Normal file
View File

@ -0,0 +1,626 @@
RollFor = RollFor or {}
local m = RollFor
if m.FrameBuilder then return end
local M = {}
local getn = m.getn
M.interface = {
}
---@alias FrameStyle
---| "Modern"
---| "Classic"
---| "None"
---@class Vector2
---@field x number
---@field y number
---@class FontString
---@field SetFont fun( self: FontString, font: string, size: number, flags: string )
---@field SetText fun( self: FontString, text: string )
---@field SetTextColor fun( self: FontString, r: number, g: number, b: number, a: number )
---@field SetJustifyH fun( self: FontString, justify_h: string )
---@field SetWidth fun( self: FontString, width: number )
---@field SetHeight fun( self: FontString, height: number )
---@field SetPoint fun( self: FontString, point: string, relative_frame: Frame|Texture, relative_point: string, x: number, y: number )
---@class Texture
---@field SetTexture fun( self: Texture, texture: string )
---@field SetWidth fun( self: Texture, width: number )
---@field SetHeight fun( self: Texture, height: number )
---@field SetPoint fun( self: Texture, point: string, relative_frame: Frame|Texture, relative_point: string, x: number, y: number )
---@field SetAllPoints fun( self: Texture, frame: Frame )
---@field SetTexCoord fun( self: Texture, x1: number, x2: number, y1: number, y2: number )
---@field SetBlendMode fun( self: Texture, blend_mode: string )
---@class Frame
---@field add_line fun( line_type: string, modify_fn: function, padding: number ): table
---@field clear fun()
---@field border_color fun( _, r: number, g: number, b: number, a: number )
---@field backdrop_color fun( _, r: number, g: number, b: number, a: number )
---@field lock fun()
---@field unlock fun()
---@field position fun( self: Frame, point: table )
---@field get_anchor_center fun(): Vector2
---@field get_anchor_point fun(): Point
---@field get_point fun(): Point
---@field anchor fun( frame: Frame, point: string, relative_point: string, x: number, y: number )
---@field Show fun( self )
---@field Hide fun( self )
---@field SetWidth fun( frame: Frame, width: number )
---@field SetHeight fun( frame: Frame, height: number )
---@field SetPoint fun( frame: Frame, point: string, relative_frame: Frame|string, relative_point: string, x: number, y: number )
---@field GetScale fun(): number
---@field GetWidth fun(): number
---@field GetHeight fun(): number
---@field StartSizing fun( self: Frame, resizePoint: string?, alwaysStartFromMouse: boolean? )
---@field StopMovingOrSizing fun()
---@field ClearAllPoints fun()
---@field SetAllPoints fun( frame: Frame, relativeTo?: Frame|string, doResize?: boolean )
---@field SetScript fun( frame: Frame, scriptTypeName: string, script: function|nil )
---@field IsVisible fun( self ): boolean
---@field GetName fun(): string?
---@field SetFrameStrata fun( self: Frame, strata: string )
---@field CreateTexture fun( self: Frame, name: string?, layer: string ): Texture
---@field SetNormalTexture fun( self: Frame, texture: string )
---@field SetPushedTexture fun( self: Frame, texture: string )
---@field CreateFontString fun( self: Frame, name: string?, layer: string, font: string ): FontString
---@field SetScript fun( self: Frame, event: string, callback: function )
---@field GetTop fun(): number
---@field GetBottom fun(): number
---@field GetLeft fun(): number
---@field GetRight fun(): number
---@field SetPushedTexture fun( self: Frame, texture: string )
---@field SetHighlightTexture fun( self: Frame, texture: string )
---@alias Anchor table
---@alias AnchorPoint
---| "TOPLEFT"
---| "TOPRIGHT"
---| "BOTTOMLEFT"
---| "BOTTOMRIGHT"
---| "CENTER"
---| "TOP"
---| "BOTTOM"
---| "LEFT"
---| "RIGHT"
---@class Point
---@field point AnchorPoint
---@field relative_frame (Frame|string)?
---@field relative_point AnchorPoint
---@field x number?
---@field y number?
---@alias FrameStrata
---| "BACKGROUND"
---| "LOW"
---| "MEDIUM"
---| "HIGH"
---| "DIALOG"
---| "FULLSCREEN"
---| "FULLSCREEN_DIALOG"
---| "TOOLTIP"
---@class FrameBuilder
---@field parent fun( self: FrameBuilder, parent: Frame ): FrameBuilder
---@field name fun( self: FrameBuilder, name: string ): FrameBuilder
---@field type fun( self: FrameBuilder, name: string ): FrameBuilder
---@field parent fun( self: FrameBuilder, parent: Frame ): FrameBuilder
---@field height fun( self: FrameBuilder, height: number ): FrameBuilder
---@field width fun( self: FrameBuilder, width: number ): FrameBuilder
---@field point fun( self: FrameBuilder, p: Point ): FrameBuilder
---@field sound fun( self: FrameBuilder ): FrameBuilder
---@field frame_level fun( self: FrameBuilder, frame_level: number ): FrameBuilder
---@field backdrop_color fun( self: FrameBuilder, r: number, g: number, b: number, a: number ): FrameBuilder
---@field bg_file fun( self: FrameBuilder, bg_file: string ): FrameBuilder
---@field edge_file fun( self: FrameBuilder, edge_file: string ): FrameBuilder
---@field esc fun( self: FrameBuilder ): FrameBuilder
---@field gui_elements fun( self: FrameBuilder, gui_elements: table ): FrameBuilder
---@field frame_style fun( self: FrameBuilder, frame_style: FrameStyle ): FrameBuilder
---@field on_drag_stop fun( self: FrameBuilder, callback: function ): FrameBuilder
---@field movable fun( self: FrameBuilder ): FrameBuilder
---@field resizable fun( self ): FrameBuilder
---@field on_resize fun( self: FrameBuilder, callback: function ): FrameBuilder
---@field enable_mouse fun( self: FrameBuilder ): FrameBuilder
---@field border_size fun( self: FrameBuilder, border_size: number ): FrameBuilder
---@field on_show fun( self: FrameBuilder, on_show: function ): FrameBuilder
---@field on_hide fun( self: FrameBuilder, on_hide: function ): FrameBuilder
---@field border_color fun( self: FrameBuilder, r: number, g: number, b: number, a: number ): FrameBuilder
---@field self_centered_anchor fun( self: FrameBuilder ): FrameBuilder
---@field scale fun( self: FrameBuilder, scale: number ): FrameBuilder
---@field strata fun( self: FrameBuilder, strata: FrameStrata ): FrameBuilder
---@field hidden fun( self: FrameBuilder ): FrameBuilder
---@field build fun( self: FrameBuilder ): Frame
---@class FrameBuilderFactory
---@field new fun(): FrameBuilder
---@field button fun(): FrameBuilder
---@field modern fun(): FrameBuilder
---@field classic fun(): FrameBuilder
---@return FrameBuilder
function M.new()
local options = {}
local frame_cache = {}
local lines = {}
local is_dragging
local function create_frame()
local function create_anchor()
local anchor = m.api.CreateFrame( "Frame", nil, m.api.UIParent )
anchor:SetWidth( 1 )
anchor:SetHeight( 1 )
anchor:SetPoint( "CENTER", 0, 0 )
anchor:EnableMouse( true )
anchor:SetMovable( true )
return anchor
end
local function create_main_frame( anchor )
local type = options.type or "Frame"
local frame = m.create_backdrop_frame( m.api, type, options.name, options.parent )
if options.hidden then
frame:Hide()
end
frame:SetWidth( options.width or 280 )
frame:SetHeight( options.height or 100 )
if anchor then
frame:SetPoint( "CENTER", anchor, "CENTER", 0, 0 )
end
if options.point then
local p = options.point
local f = anchor or frame
f:SetPoint( p.point, p.relative_frame or m.api.UIParent, p.relative_point, p.x, p.y )
else
frame:SetPoint( "CENTER", anchor or m.api.UIParent, "CENTER", 0, 0 )
end
if options.frame_level then
frame:SetFrameLevel( options.frame_level )
end
if options.strata then
frame:SetFrameStrata( options.strata )
else
frame:SetFrameStrata( "DIALOG" )
end
if options.frame_style == "Modern" then
frame:SetBackdrop( {
bgFile = options.bg_file or "Interface/Buttons/WHITE8x8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false,
tileSize = 0,
edgeSize = 0.8,
insets = { left = 0, right = 0, top = 0, bottom = 0 }
} )
elseif options.frame_style == "Classic" then
frame:SetBackdrop( {
bgFile = options.bg_file or "Interface/Buttons/WHITE8x8",
edgeFile = options.edge_file or "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true,
tileSize = 22,
edgeSize = options.border_size or 24,
insets = { left = 5, right = 5, top = 5, bottom = 5 }
} )
end
if options.backdrop_color then
local c = options.backdrop_color
frame:SetBackdropColor( c.r, c.g, c.b, c.a or 1 )
else
frame:SetBackdropColor( 0, 0, 0, 0.7 )
end
if options.border_color then
local c = options.border_color
frame:SetBackdropBorderColor( c.r, c.g, c.b, options.frame_style == "Classic" and 1 or c.a )
end
return frame
end
local function configure_main_frame( frame, anchor )
if options.sound then
local old_on_show = frame:GetScript( "OnShow" )
frame:SetScript( "OnShow", function()
if m.vanilla then
m.api.PlaySound( "igMainMenuOpen" )
else
m.api.PlaySound( m.api.SOUNDKIT.IG_MAINMENU_OPEN )
end
if old_on_show then old_on_show() end
if options.on_show then options.on_show() end
end )
frame:SetScript( "OnHide", function()
if is_dragging then
local f = anchor or frame
f:StopMovingOrSizing()
end
if m.vanilla then
m.api.PlaySound( "igMainMenuClose" )
else
m.api.PlaySound( m.api.SOUNDKIT.IG_MAINMENU_CLOSE )
end
if options.on_hide then options.on_hide() end
end )
end
if options.enable_mouse then
frame:EnableMouse( true )
end
if options.movable then
frame:SetMovable( true )
-- frame:EnableMouse( true )
frame:RegisterForDrag( "LeftButton" )
frame:SetScript( "OnDragStart", function()
if not frame:IsMovable() then return end
is_dragging = true
local f = anchor or frame
f:StartMoving()
end )
frame:SetScript( "OnDragStop", function()
is_dragging = false
local f = anchor or frame
f:StopMovingOrSizing()
if options.on_drag_stop then
options.on_drag_stop( frame )
end
if anchor then
frame:ClearAllPoints()
frame:SetPoint( "CENTER", anchor, "CENTER", 0, 0 )
end
end )
else
frame:SetMovable( false )
end
if options.resizable then
frame:SetResizable( true )
if options.on_resize and frame:IsResizable() then
frame:SetScript( "OnSizeChanged", function()
options.on_resize( frame )
end )
end
else
frame:SetResizable( false )
end
frame:EnableMouse( true )
if options.esc then
m.api.tinsert( m.api.UISpecialFrames, frame:GetName() )
end
if options.scale then
frame:SetScale( options.scale )
end
end
local function get_from_cache( line_type )
frame_cache[ line_type ] = frame_cache[ line_type ] or {}
for i = getn( frame_cache[ line_type ] ), 1, -1 do
if not frame_cache[ line_type ][ i ].is_used then
return frame_cache[ line_type ][ i ]
end
end
end
local function add_api_to( frame, anchor )
frame.add_line = function( line_type, modify_fn, padding )
local line_frame = get_from_cache( line_type )
if not line_frame then
local creator_fn = options.gui_elements and options.gui_elements[ line_type ] or nil
if not creator_fn then return end
line_frame = creator_fn( frame )
line_frame.is_used = true
table.insert( frame_cache[ line_type ], line_frame )
else
line_frame.is_used = true
line_frame:Show()
end
modify_fn( line_type, line_frame, lines )
local line = { line_type = line_type, padding = padding or 0, frame = line_frame }
table.insert( lines, line )
if frame.resize then frame:resize( lines ) end
return line
end
frame.clear = function()
for _, line in ipairs( lines ) do
line.frame:Hide()
line.frame.is_used = false
end
m.clear_table( lines )
if m.vanilla then lines.n = 0 end
end
frame.backdrop_color = function( _, r, g, b, a )
frame:SetBackdropColor( r, g, b, a )
end
frame.border_color = function( _, r, g, b, a )
frame:SetBackdropBorderColor( r, g, b, options.frame_style == "Classic" and 1 or a )
end
frame.lock = function()
frame:SetMovable( false )
end
frame.unlock = function()
frame:SetMovable( true )
end
frame.position = function( _, point )
local f = anchor or frame
f:ClearAllPoints()
f:SetPoint( point.point, point.anchor or m.api.UIParent, point.relative_point, point.x, point.y )
end
frame.get_anchor_center = function()
local f = anchor or frame
local x, y = f:GetCenter()
return { x = x, y = y }
end
frame.get_anchor_point = function()
local f = anchor or frame
local point, relative_frame, relative_point, x, y = f:GetPoint()
return point and { point = point, relative_frame = relative_frame, relative_point = relative_point, x = x, y = y }
end
frame.get_point = function()
local f = frame
local point, relative_frame, relative_point, x, y = f:GetPoint()
return point and { point = point, relative_frame = relative_frame, relative_point = relative_point, x = x, y = y }
end
frame.anchor = function( _, source_frame, point, relative_point, x, y )
if anchor then
source_frame:ClearAllPoints()
source_frame:SetPoint( point, anchor, relative_point, x, y )
else
source_frame:ClearAllPoints()
source_frame:SetPoint( point, m.api.UIParent, relative_point, x, y )
end
end
end
local self_centered_anchor = options.self_centered_anchor and create_anchor()
local frame = create_main_frame( self_centered_anchor )
configure_main_frame( frame, self_centered_anchor )
add_api_to( frame, self_centered_anchor )
return frame, self_centered_anchor
end
local function name( self, v )
options.name = v
return self
end
local function type( self, v )
options.type = v
return self
end
local function parent( self, v )
options.parent = v
return self
end
local function height( self, v )
options.height = v
return self
end
local function width( self, v )
options.width = v
return self
end
local function point( self, p )
options.point = { point = p.point, relative_frame = p.relative_frame or m.api.UIParent, relative_point = p.relative_point, x = p.x or 0, y = p.y or 0 }
return self
end
local function sound( self )
options.sound = true
return self
end
local function frame_level( self, v )
options.frame_level = v
return self
end
local function esc( self )
options.esc = true
return self
end
---@return Frame
---@return Anchor
local function build()
return create_frame()
end
local function backdrop_color( self, r, g, b, a )
options.backdrop_color = { r = r, g = g, b = b, a = a }
return self
end
local function bg_file( self, v )
options.bg_file = v
return self
end
local function edge_file( self, v )
options.edge_file = v
return self
end
local function gui_elements( self, t )
options.gui_elements = t
return self
end
---@param self FrameBuilder
---@param v FrameStyle
local function frame_style( self, v )
options.frame_style = v
return self
end
local function on_drag_stop( self, callback )
options.on_drag_stop = callback
return self
end
local function movable( self )
options.movable = true
return self
end
local function resizable( self )
options.resizable = true
return self
end
local function on_resize( self, callback )
options.on_resize = callback
return self
end
local function border_size( self, v )
options.border_size = v
return self
end
local function on_show( self, f )
options.on_show = f
return self
end
local function on_hide( self, f )
options.on_hide = f
return self
end
local function border_color( self, r, g, b, a )
options.border_color = { r = r, g = g, b = b, a = a }
return self
end
local function self_centered_anchor( self )
options.self_centered_anchor = true
return self
end
local function scale( self, v )
options.scale = v
return self
end
local function enable_mouse( self )
options.enable_mouse = true
return self
end
local function strata( self, v )
options.strata = v
return self
end
local function hidden( self )
options.hidden = true
return self
end
---@type FrameBuilder
return {
name = name,
type = type,
parent = parent,
height = height,
width = width,
point = point,
sound = sound,
frame_level = frame_level,
backdrop_color = backdrop_color,
bg_file = bg_file,
edge_file = edge_file,
esc = esc,
gui_elements = gui_elements,
frame_style = frame_style,
on_drag_stop = on_drag_stop,
movable = movable,
resizable = resizable,
on_resize = on_resize,
border_size = border_size,
on_show = on_show,
on_hide = on_hide,
border_color = border_color,
self_centered_anchor = self_centered_anchor,
scale = scale,
enable_mouse = enable_mouse,
strata = strata,
hidden = hidden,
build = build
}
end
function M.button()
return M.new():type( "Button" )
end
function M.modern()
return M.new()
:frame_style( "Modern" )
end
function M.classic()
return M.new()
:frame_style( "Classic" )
:border_size( 25 )
end
m.FrameBuilder = M
return M

121
src/GroupRoster.lua Normal file
View File

@ -0,0 +1,121 @@
RollFor = RollFor or {}
local m = RollFor
if m.GroupRoster then return end
local M = {}
---@type MakePlayerFn
local make_player = m.Types.make_player
---@class GroupRosterApi
---@field IsInParty fun(): number?
---@field IsInRaid fun(): number?
---@field IsInGroup fun(): number?
---@field UnitName fun( unit: string ): string?
---@field UnitClass fun( unit: string ): string?
---@field UnitIsConnected fun( unit: string ): number?
---@field GetRaidRosterInfo fun( index: number ): string?, string, number, number, PlayerClass, string, string
---@class GroupRoster
---@field get_all_players_in_my_group fun( f: (fun( player: Player ): boolean)? ): Player[]
---@field is_player_in_my_group fun( player_name: string ): boolean
---@field am_i_in_group fun(): boolean
---@field am_i_in_party fun(): boolean
---@field am_i_in_raid fun(): boolean
---@field find_player fun( player_name: string ): Player?
---@param api GroupRosterApi
---@param player_info PlayerInfo
function M.new( api, player_info )
local function sort( candidates )
table.sort( candidates, function( lhs, rhs )
if lhs.class < rhs.class then
return true
elseif lhs.class > rhs.class then
return false
end
return lhs.name < rhs.name
end )
end
local function get_all_players_in_my_group( f )
local result = {}
if not api.IsInGroup() then
local name = player_info.get_name()
local class = api.UnitClass( "player" )
table.insert( result, { name = name, class = class } )
return result
end
if api.IsInRaid() then
for i = 1, 40 do
local name, _, _, _, class, _, location = api.GetRaidRosterInfo( i )
local player = { name = name, class = class, online = location ~= "Offline" and true or false }
if name and (not f or f( player )) then table.insert( result, player ) end
end
sort( result )
return result
end
local party = { "player", "party1", "party2", "party3", "party4" }
for _, v in ipairs( party ) do
local name = api.UnitName( v )
local class = api.UnitClass( v )
local online = api.UnitIsConnected( v ) and true or false
local player = name and class and make_player( name, class, online )
if player and (not f or f( player )) then table.insert( result, player ) end
end
sort( result )
return result
end
local function is_player_in_my_group( player_name )
local players = get_all_players_in_my_group()
for _, player in pairs( players ) do
if string.lower( player.name ) == string.lower( player_name ) then return true end
end
return false
end
local function am_i_in_group()
return api.IsInGroup()
end
local function am_i_in_party()
return api.IsInGroup() and not api.IsInRaid()
end
local function am_i_in_raid()
return api.IsInGroup() and api.IsInRaid()
end
local function find_player( player_name )
local players = get_all_players_in_my_group()
for _, player in pairs( players ) do
if string.lower( player.name ) == string.lower( player_name ) then return player end
end
end
---@type GroupRoster
return {
get_all_players_in_my_group = get_all_players_in_my_group,
is_player_in_my_group = is_player_in_my_group,
am_i_in_group = am_i_in_group,
am_i_in_party = am_i_in_party,
am_i_in_raid = am_i_in_raid,
find_player = find_player
}
end
m.GroupRoster = M
return M

584
src/GuiElements.lua Normal file
View File

@ -0,0 +1,584 @@
RollFor = RollFor or {}
local m = RollFor
if m.GuiElements then return end
local hl = m.colors.hl
---@class GuiElements
---@field item_link fun( parent: Frame ): Frame
---@field item_link_with_icon fun( parent: Frame, text: string ): Frame
---@field text fun( parent: Frame, text: string ): Frame
---@field icon fun( parent: Frame, show: boolean, width: number, height: number ): Frame
---@field icon_text fun( parent: Frame, text: string ): Frame
---@field roll fun( parent: Frame ): Frame
---@field button fun( parent: Frame ): Frame
---@field info fun( parent: Frame ): Frame
---@field dropped_item fun( parent: Frame, text: string ): Frame
---@field tiny_button fun( parent: Frame, text: string?, tooltip: string?, color: table?, font-size: number?):Frame
---@field titlebar fun( parent: Frame, title: string, on_close: function )
local M = {}
function M.create_text_in_container( type, parent, container_width, alignment, text, inner_field, font_type )
local container = m.create_backdrop_frame( m.api, type, nil, parent )
container:SetWidth( container_width )
local label = container:CreateFontString( nil, "ARTWORK", font_type or "GameFontNormalSmall" )
label:SetTextColor( 1, 1, 1 )
if text then label:SetText( text ) end
if alignment then label:SetPoint( alignment, 0, 0 ) end
container:SetHeight( label:GetHeight() )
if inner_field then
container[ inner_field ] = label
else
container.inner = label
end
return container
end
function M.empty_line( parent )
local result = m.api.CreateFrame( "Frame", nil, parent )
result:SetWidth( 2 )
return result
end
function M.item_link_with_icon( parent, text )
local container = M.create_text_in_container( "Button", parent, 20, nil, nil, "text" )
local w = 14
local h = 14
local spacing = 10
local count = 0
local texture
local tooltip_link
container:SetPoint( "TOP", 0, 0 )
container.icon = M.icon( container, true, w, h )
container.icon:SetPoint( "LEFT", 0, 0 )
container.icon:SetTexCoord( 1 / w, (w - 1) / w, 1 / h, (h - 1) / h )
container.count = M.text( container )
container.text:SetTextColor( 1, 1, 1 )
if text then
container.text:SetText( text )
else
container.text:SetText( "PrincessKenny" )
end
container:SetHeight( container.text:GetHeight() )
local function resize()
if texture then
container.icon:Show()
local anchor = container.icon
local padding = spacing
local count_width = 0
if count > 1 then
container.count:Show()
container.count:ClearAllPoints()
container.count:SetPoint( "LEFT", container.icon, "RIGHT", spacing, 0 )
anchor = container.count
padding = 0
count_width = container.count:GetWidth()
end
container.text:ClearAllPoints()
container.text:SetPoint( "LEFT", anchor, "RIGHT", padding, 0 )
container:SetWidth( container.text:GetWidth() + w + count_width + spacing )
else
local anchor = container
local count_width = 0
if count > 1 then
container.count:Show()
container.count:ClearAllPoints()
container.count:SetPoint( "LEFT", container.icon, "RIGHT", spacing, 0 )
anchor = container.count
count_width = container.count:GetWidth()
end
container.icon:Hide()
container.text:ClearAllPoints()
container.text:SetPoint( "LEFT", anchor, 0, 0 )
container:SetWidth( count_width + container.text:GetWidth() )
end
end
container.SetItem = function( _, i, tt_link )
texture = i.texture
count = i.count or 0
tooltip_link = tt_link
container.text:SetText( i.link )
container.icon:SetTexture( texture )
container.count:SetText( count > 1 and hl( string.format( "%sx", count ) ) or nil )
resize()
end
local function on_enter( self )
if not tooltip_link then return end
if m.vanilla then self = this end
m.api.GameTooltip:SetOwner( self, "ANCHOR_CURSOR" )
m.api.GameTooltip:SetHyperlink( tooltip_link )
m.api.GameTooltip:Show()
end
local function on_leave()
m.api.GameTooltip:Hide()
end
container:SetScript( "OnEnter", on_enter )
container:SetScript( "OnLeave", on_leave )
container:SetScript( "OnClick", function()
if not tooltip_link then return end
if m.is_ctrl_key_down() then
m.api.DressUpItemLink( container.text:GetText() )
return
end
if m.is_shift_key_down() then
m.link_item_in_chat( container.text:GetText() )
end
end )
return container
end
function M.text( parent, text )
local label = parent:CreateFontString( nil, "ARTWORK", "GameFontNormalSmall" )
label:SetTextColor( 1, 1, 1 )
label:SetNonSpaceWrap( false )
if text then label:SetText( text ) end
return label
end
function M.icon( parent, show, width, height )
local icon = parent:CreateTexture( nil, "ARTWORK" )
if not show then icon:Hide() end
icon:SetWidth( width or 16 )
icon:SetHeight( height or 16 )
icon:SetTexture( "Interface\\AddOns\\RollFor\\assets\\icon-white2.tga" )
return icon
end
function M.icon_text( parent, text )
local container = M.create_text_in_container( "Button", parent, 20, nil, nil, "text" )
container:SetPoint( "CENTER", 0, 0 )
container.icon = M.icon( container, true )
container.icon:SetPoint( "LEFT", 0, 0 )
container.text:SetPoint( "LEFT", container.icon, "RIGHT", 3, 0 )
container.text:SetTextColor( 1, 1, 1 )
if text then container.text:SetText( text ) end
container.SetText = function( _, v )
container.text:SetText( v )
container:SetWidth( container.text:GetWidth() + 19 )
end
return container
end
function M.roll( parent )
local frame = m.create_backdrop_frame( m.api, "Button", nil, parent )
frame:SetWidth( 170 )
frame:SetHeight( 14 )
frame:SetFrameStrata( "DIALOG" )
frame:SetFrameLevel( parent:GetFrameLevel() + 1 )
frame:SetBackdrop( {
bgFile = "Interface/Buttons/WHITE8x8",
tile = true,
tileSize = 22,
} )
local function blue_hover( a )
frame:SetBackdropColor( 0.125, 0.624, 0.976, a )
end
local function hover()
if frame.is_selected then
return
end
blue_hover( 0.2 )
end
frame.select = function()
blue_hover( 0.3 )
frame.is_selected = true
end
local function no_hover()
if frame.is_selected then
frame.select()
else
blue_hover( 0 )
end
end
frame.deselect = function()
blue_hover( 0 )
frame.is_selected = false
end
frame:deselect()
frame:SetScript( "OnEnter", function()
hover()
end )
frame:SetScript( "OnLeave", function()
no_hover()
end )
frame:EnableMouse( true )
local roll_container = M.create_text_in_container( "Button", frame, 35, "RIGHT" )
roll_container:SetPoint( "LEFT", 0, 0 )
frame.roll = roll_container.inner
local icon = M.icon( frame )
icon:SetPoint( "LEFT", 22, 0 )
frame.icon = icon
roll_container:SetPoint( "LEFT", 0, 0 )
frame.roll = roll_container.inner
local player_name = M.text( frame )
player_name:SetPoint( "CENTER", frame, "CENTER", 0, 0 )
frame.player_name = player_name
local roll_type_container = M.create_text_in_container( "Button", frame, 37, "LEFT" )
roll_type_container:SetPoint( "RIGHT", 0, 0 )
frame.roll_type = roll_type_container.inner
return frame
end
function M.button( parent )
local template = m.vanilla and "StaticPopupButtonTemplate" or "UIPanelButtonTemplate"
local height = m.vanilla and 20 or 21
local button = m.api.CreateFrame( "Button", nil, parent, template )
button:SetWidth( 100 )
button:SetHeight( height )
button:SetText( "" )
button:GetFontString():SetPoint( "CENTER", 0, -1 )
return button
end
function M.award_button( parent )
local template = m.vanilla and "StaticPopupButtonTemplate" or "UIPanelButtonTemplate"
local height = m.vanilla and 20 or 21
local button = m.api.CreateFrame( "Button", nil, parent, template )
button:SetWidth( 100 )
button:SetHeight( height )
button:SetText( "" )
button:GetFontString():SetPoint( "CENTER", 0, -1 )
return button
end
---@param parent Frame
---@param text string?
---@param tooltip string?
---@param color string|table?
---@param font_size number?
function M.tiny_button( parent, text, tooltip, color, font_size )
local font_x, font_y
local button = m.api.CreateFrame( "Button", nil, parent )
if not text then text = 'X' end
if type( color ) == "string" and color ~= "" then
local str_color = color
color = {}
color.r, color.g, color.b, color.a = m.hex_to_rgba( str_color )
end
if m.classic then
if not color then color = { r = .9, g = .8, b = .25 } end
button:SetWidth( 18 )
button:SetHeight( 18 )
local highlight_texture = button:CreateTexture( nil, "HIGHLIGHT" )
highlight_texture:SetTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Highlight" )
highlight_texture:SetTexCoord( .1875, .78125, .21875, .78125 )
highlight_texture:SetBlendMode( "ADD" )
highlight_texture:SetAllPoints( button )
if text == 'X' then
button:SetNormalTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Up" )
button:SetPushedTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Down" )
else
button:SetNormalTexture( "Interface\\AddOns\\RollFor\\assets\\tiny-button-up.tga" )
button:SetPushedTexture( "Interface\\AddOns\\RollFor\\assets\\tiny-button-down.tga" )
end
button:GetNormalTexture():SetTexCoord( .1875, .78125, .21875, .78125 )
button:GetPushedTexture():SetTexCoord( .1875, .78125, .21875, .78125 )
if text ~= 'X' then
button:SetText( text )
button:SetPushedTextOffset( -1.5, -1.5 )
if string.upper( text ) == text then
font_x, font_y = 0, 0
font_size = font_size or 13
else
font_x, font_y = -1, 2
font_size = font_size or 15
end
end
else
if not color then color = { r = 1, g = .25, b = .25 } end
button:SetBackdrop( {
bgFile = "Interface/Tooltips/UI-Tooltip-Background",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false,
tileSize = 0,
edgeSize = 0.5,
insets = { left = 0, right = 0, top = 0, bottom = 0 }
} )
button:SetBackdropColor( 0, 0, 0, 1 )
button:SetBackdropBorderColor( .2, .2, .2, 1 )
button:SetHeight( 13 )
button:SetWidth( 13 )
button:SetText( text )
button:SetPushedTextOffset( 0, 0 )
if string.upper( text ) == text then
font_x = text == "?" and -.5 or 0
font_y = 0.5
font_size = font_size or 10
else
font_x, font_y = -.5, 1.5
font_size = font_size or 14
end
end
if not m.classic or text ~= "X" then
button:GetFontString():SetFont( "FONTS\\FRIZQT__.TTF", font_size )
button:GetFontString():SetTextColor( color.r, color.g, color.b, color.a or 1 )
button:GetFontString():SetPoint( "CENTER", font_x, font_y )
end
button:SetScript( "OnEnter", function()
local self = button
self:SetBackdropBorderColor( color.r, color.g, color.b, color.a or 1 )
if tooltip then
m.api.GameTooltip:SetOwner( button, "ANCHOR_RIGHT" )
m.api.GameTooltip:SetText( tooltip )
m.api.GameTooltip:SetScale( 0.8 )
m.api.GameTooltip:Show()
end
end )
button:SetScript( "OnLeave", function()
local self = button
if not self.active then
self:SetBackdropBorderColor( .2, .2, .2, 1 )
end
if tooltip and m.api.GameTooltip:IsVisible() then
m.api.GameTooltip:SetScale( 1 )
m.api.GameTooltip:Hide()
end
end )
return button
end
---@param parent Frame
---@param on_start function
---@param on_end function
function M.resize_grip( parent, on_start, on_end )
local button = m.api.CreateFrame( "Button", nil, parent )
button:SetWidth( 16 )
button:SetHeight( 16 )
button:SetNormalTexture( "Interface\\AddOns\\RollFor\\assets\\resize-grip.tga", "ARTWORK" )
button:GetNormalTexture():SetAllPoints( button )
button:SetScript( "OnEnter", function()
button:GetNormalTexture():SetBlendMode( "ADD" )
end )
button:SetScript( "OnLeave", function()
button:GetNormalTexture():SetBlendMode( "BLEND" )
end )
button:SetScript( "OnMouseDown", function()
parent:StartSizing( "BOTTOMRIGHT" )
if on_start then on_start( parent ) end
end )
button:SetScript( "OnMouseUp", function()
parent:StopMovingOrSizing()
if on_end then on_end( parent ) end
end )
return button
end
function M.checkbox( parent, text, on_change )
local frame = m.api.CreateFrame( "Frame", nil, parent )
frame:SetPoint( "LEFT", 5, 0 )
frame:SetHeight( 14 )
local cb = m.api.CreateFrame( "CheckButton", nil, frame, "UICheckButtonTemplate" )
cb:SetWidth( 14 )
cb:SetHeight( 14 )
cb:SetPoint( "LEFT", 2, 0 )
cb:SetNormalTexture( nil )
cb:SetPushedTexture( nil )
cb:SetHighlightTexture( nil )
cb:SetBackdrop( {
bgFile = "Interface/Buttons/WHITE8x8",
edgeFile = "Interface/Buttons/WHITE8x8",
edgeSize = 0.5,
insets = { left = 0, right = 0, top = 0, bottom = 0 }
} )
cb:SetBackdropColor( 0, 0, 0, 1 )
cb:SetBackdropBorderColor( .2, .2, .2, 1 )
cb:SetScript( "OnClick", function()
if on_change then on_change( cb:GetChecked() ) end
end )
frame.checkbox = cb
local label = M.create_text_in_container( "Button", frame, 1, "LEFT", text )
label.inner:SetJustifyH( "LEFT" )
label:SetWidth( label.inner:GetWidth() )
label:SetPoint( "LEFT", cb, "RIGHT", 5, 0 )
label:SetScript( "OnClick", function()
cb:SetChecked( not cb:GetChecked() )
if on_change then on_change( cb:GetChecked() ) end
end )
frame:SetWidth( cb:GetWidth() + label:GetWidth() + 5 )
return frame
end
---@param parent Frame
---@param title string
---@param on_close function
function M.titlebar( parent, title, on_close )
local frame = m.api.CreateFrame( "Frame", nil, parent )
frame:SetHeight( 32 )
if not m.classic then
frame:SetPoint( "TOPLEFT", 0, 5 )
frame:SetPoint( "RIGHT", 0, 0 )
else
frame:SetPoint( "TOPLEFT", 3, 2 )
frame:SetPoint( "RIGHT", -3, 2 )
frame:SetBackdrop( {
bgFile = "Interface\\AddOns\\RollFor\\assets\\titlebar-top.tga",
tile = true,
tileSize = 32,
edgeSize = 0,
insets = { left = 30, right = 30, top = 0, bottom = 0 }
} )
local topLeft = frame:CreateTexture( nil, "BORDER" )
topLeft:SetTexture( "Interface\\AddOns\\RollFor\\assets\\titlebar-topleft.tga" )
topLeft:SetPoint( "TOPLEFT", frame, "TOPLEFT", 0, 0 )
topLeft:SetWidth( 64 )
topLeft:SetHeight( 32 )
local topRight = frame:CreateTexture( nil, "BORDER" )
topRight:SetTexture( "Interface\\AddOns\\RollFor\\assets\\titlebar-topright.tga" )
topRight:SetPoint( "TOPRIGHT", frame, "TOPRIGHT", 0, 0 )
topRight:SetWidth( 64 )
topRight:SetHeight( 32 )
end
local label = frame:CreateFontString( nil, "ARTWORK", "GameFontNormalSmall" )
label:SetPoint( "TOPLEFT", 10, -12 )
label:SetPoint( "RIGHT", m.classic and -29 or 0, 0 )
label:SetJustifyH( "CENTER" )
label:SetTextColor( 1, 1, 1 )
label:SetText( title )
frame.title = label
local btn_close = M.tiny_button( parent, "X", "Close Window" )
btn_close:SetPoint( "TOPRIGHT", m.classic and -7 or -5, -5 )
btn_close:SetScript( "OnClick", function()
if on_close then
on_close()
else
if parent then parent:Hide() end
end
end )
return frame
end
function M.info( parent )
local frame = m.api.CreateFrame( "Frame", nil, parent )
frame:SetWidth( 11 )
frame:SetHeight( 11 )
frame:SetFrameStrata( "DIALOG" )
frame:SetFrameLevel( parent:GetFrameLevel() + 1 )
frame:EnableMouse( true )
local icon = frame:CreateTexture( nil, "BACKGROUND" )
icon:SetWidth( 11 )
icon:SetHeight( 11 )
icon:SetTexture( "Interface\\AddOns\\RollFor\\assets\\info.tga" )
icon:SetPoint( "CENTER", 0, 0 )
frame:SetScript( "OnEnter", function( self )
if m.vanilla then self = this end
self.tooltip_scale = m.api.GameTooltip:GetScale()
m.api.GameTooltip:SetOwner( self, "ANCHOR_CURSOR" )
m.api.GameTooltip:AddLine( frame.tooltip_info, 1, 1, 1 )
m.api.GameTooltip:SetScale( 0.75 )
m.api.GameTooltip:Show()
end )
frame:SetScript( "OnLeave", function( self )
if m.vanilla then self = this end
m.api.GameTooltip:Hide()
m.api.GameTooltip:SetScale( self.tooltip_scale or 1 )
end )
return frame
end
function M.create_icon_in_container( type, parent, w, h, icon_zoom )
local result = m.create_backdrop_frame( m.api, type or "Button", nil, parent )
result:SetWidth( w + 1 )
result:SetHeight( h )
result:SetBackdrop( {
bgFile = "Interface/Tooltips/UI-Tooltip-Background",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false,
tileSize = 0,
edgeSize = 1,
insets = { left = 0, right = 0, top = 0, bottom = 0 }
} )
result:SetBackdropBorderColor( 0, 0, 0, 1 )
result:SetBackdropColor( 0, 0, 0, 0 )
result.texture = M.icon( result, true, w, h )
result.texture:SetPoint( "CENTER", 0, 0 )
result.texture:SetTexCoord( icon_zoom / w, (w - icon_zoom) / w, icon_zoom / h, (h - icon_zoom) / h )
return result
end
m.GuiElements = M
return M

View File

@ -0,0 +1,87 @@
RollFor = RollFor or {}
local m = RollFor
if m.InstaRaidRollRollingLogic then return end
local M = {}
local getn = m.getn
local hl = m.colors.hl
local strategy = m.Types.RollingStrategy.InstaRaidRoll
local roll_type = m.Types.RollType.MainSpec
local clear_table = m.clear_table
---@type MakeWinnerFn
local make_winner = m.Types.make_winner
-- TODO: Lots of similarity with RaidRollRollingLogic. Perhaps refactor.
---@param chat Chat
---@param item Item|MasterLootDistributableItem
---@param item_count number
---@param winner_tracker WinnerTracker
---@param controller RollControllerFacade
---@param candidates ItemCandidate[]|Player[]
function M.new(
chat,
_,
item,
item_count,
winner_tracker,
controller,
candidates
)
local m_winners = {}
local function clear_winners()
clear_table( m_winners )
if m.vanilla then m_winners.n = 0 end
end
local function start_rolling()
clear_winners()
for _ = 1, item_count do
local roll = m.lua.math.random( 1, getn( candidates ) )
table.insert( m_winners, candidates[ roll ] )
end
local winners = m.map( m_winners,
---@param player ItemCandidate|Player
function( player )
if type( player ) == "table" then -- Fucking lua50 and its n.
local winner = make_winner( player.name, player.class, item, player.type == "ItemCandidate" or false, roll_type, nil )
winner_tracker.track( winner.name, item.link, roll_type, nil, m.Types.RollingStrategy.InstaRaidRoll )
return winner
end
end )
controller.winners_found( item, item_count, winners, strategy )
controller.finish()
end
local function show_sorted_rolls()
if getn( m_winners ) == 0 then
chat.info( "There is no winner yet.", nil, "RaidRoll" )
return
end
for _, winner in ipairs( m_winners ) do
chat.info( string.format( "%s won %s.", hl( winner.name ), item.link ), nil, "InstaRaidRoll" )
end
end
---@type RollingStrategy
return {
start_rolling = start_rolling, -- This probably doesn't belong here either.
on_roll = function() end,
is_rolling = function() return false end,
show_sorted_rolls = show_sorted_rolls,
get_type = function() return m.Types.RollingStrategy.InstaRaidRoll end,
stop_accepting_rolls = m.noop(),
cancel_rolling = m.noop()
}
end
m.InstaRaidRollRollingLogic = M
return M

71
src/Interface.lua Normal file
View File

@ -0,0 +1,71 @@
RollFor = RollFor or {}
local m = RollFor
if m.Interface then return end
local M = {}
---@diagnostic disable-next-line: undefined-global
local debugstack = debugstack
---@param implementation table
---@param i1 table
---@param i2 table?
---@param i3 table?
---@param i4 table?
---@param i5 table?
function M.validate( implementation, i1, i2, i3, i4, i5, i6, i7, i8, i9 )
assert( type( implementation ) == "table", "'implementation' must be a table." )
for _, interface in ipairs( { i1, i2, i3, i4, i5, i6, i7, i8, i9 } ) do
for method_name, expected_type in pairs( interface ) do
local v = implementation[ method_name ]
if type( v ) ~= expected_type then
if debugstack then
error( string.format( "'%s' must be a %s, got %s.", method_name, expected_type, type( v ) ) .. "\n" .. debugstack(), 2 )
else
error( string.format( "'%s' must be a %s, got %s.", method_name, expected_type, type( v ), debug.traceback() ), 2 )
end
end
end
end
end
---@param implementation table
---@param n1 string
---@param n2 string?
---@param n3 string?
---@param n4 string?
---@param n5 string?
---@param n6 string?
---@param n7 string?
---@param n8 string?
---@param n9 string?
function M.assert_members( implementation, n1, n2, n3, n4, n5, n6, n7, n8, n9 )
assert( type( implementation ) == "table", "'implementation' must be a table." )
for _, member_name in ipairs( { n1, n2, n3, n4, n5, n6, n7, n8, n9 } ) do
if implementation[ member_name ] == nil then
if debugstack then
error( string.format( "Member '%s' is not present.", member_name ) .. "\n" .. debugstack(), 2 )
else
error( string.format( "Member '%s' is not present: %s.", member_name, debug.traceback() ), 2 )
end
end
end
end
function M.noop() end
function M.mock( interface )
local result = {}
for k, v in pairs( interface ) do
if v == "function" then result[ k ] = M.noop end
end
return result
end
m.Interface = M
return M

306
src/ItemUtils.lua Normal file
View File

@ -0,0 +1,306 @@
RollFor = RollFor or {}
local m = RollFor
if m.ItemUtils then return end
local red, white = m.colors.red, m.colors.white
local M = {}
---@class LT
---@field Item "Item"
---@field SoftRessedItem "SoftRessedItem"
---@field HardRessedItem "HardRessedItem"
---@field Coin "Coin"
---@field DroppedItem "DroppedItem"
---@field SoftRessedDroppedItem "SoftRessedDroppedItem"
---@field HardRessedDroppedItem "HardRessedDroppedItem"
---@type LT
local LootType = {
Item = "Item",
SoftRessedItem = "SoftRessedItem",
HardRessedItem = "HardRessedItem",
Coin = "Coin",
DroppedItem = "DroppedItem",
SoftRessedDroppedItem = "SoftRessedDroppedItem",
HardRessedDroppedItem = "HardRessedDroppedItem"
}
M.LootType = LootType
---@alias LootType
---| "Item"
---| "SoftRessedItem"
---| "HardRessedItem"
---| DroppedLootType
---@alias DroppedLootType
---| CoinType
---| DroppedItemType
---@alias CoinType
---| "Coin"
---@alias DroppedItemType
---| "DroppedItem"
---| "SoftRessedDroppedItem"
---| "HardRessedDroppedItem"
---@class BT
---@field BindOnPickup "BindOnPickup"
---@field BindOnEquip "BindOnEquip"
---@field Soulbound "Soulbound"
---@field Quest "Quest"
---@field None "None"
---@type BT
local BindType = {
BindOnPickup = "BindOnPickup",
BindOnEquip = "BindOnEquip",
Soulbound = "Soulbound",
Quest = "Quest",
None = "None"
}
M.BindType = BindType
---@alias BindType
---| "BindOnPickup"
---| "BindOnEquip"
---| "Soulbound"
---| "Quest"
---| "None"
---@alias ItemQuality
---| 0 -- Poor
---| 1 -- Common
---| 2 -- Uncommon
---| 3 -- Rare
---| 4 -- Epic
---| 5 -- Legendary
---@alias ItemLink string
---@alias TooltipItemLink string
---@alias ItemTexture string
---@class Item
---@field id number
---@field name string
---@field link ItemLink
---@field quality ItemQuality?
---@field texture string?
---@field classes table<number, PlayerClass>?
---@field is_boss_loot boolean?
---@field type "Item"
---@class DroppedItem : Item
---@field tooltip_link TooltipItemLink
---@field quantity number
---@field bind BindType?
---@field type "DroppedItem"
---@class HardRessedDroppedItem : DroppedItem
---@field type "HardRessedDroppedItem"
---@class SoftRessedDroppedItem : DroppedItem
---@field sr_players RollingPlayer[]
---@field type "SoftRessedDroppedItem"
---@class Coin
---@field texture string
---@field amount_text string
---@field type "Coin"
---@alias MasterLootDistributableItem DroppedItem|HardRessedDroppedItem|SoftRessedDroppedItem
---@alias MakeItemFn fun(
--- id: number,
--- name: string,
--- link: ItemLink,
--- quality: ItemQuality,
--- texture: string ): Item
---@alias MakeDroppedItemFn fun(
--- id: number,
--- name: string,
--- link: ItemLink,
--- tooltip_link: TooltipItemLink,
--- quality: ItemQuality,
--- quantity: number,
--- texture: string,
--- bind: BindType,
--- classes: table<number, PlayerClass>|nil,
--- is_boss_loot: boolean ): DroppedItem
---@alias MakeSoftRessedDroppedItemFn fun(
--- item: DroppedItem,
--- sr_players: RollingPlayer[] ): SoftRessedDroppedItem
---@alias MakeHardRessedDroppedItemFn fun(
--- item: DroppedItem ): HardRessedDroppedItem
---@class ItemUtils
---@field get_item_id fun( item_link: ItemLink ): number?
---@field get_item_name fun( item_link: ItemLink ): string
---@field parse_link fun( item_link: string ): ItemLink? -- Sometimes we need to parse the link from the "[Item Name]x4." string.
---@field parse_all_links fun( item_links: string ): ItemLink[]
---@field get_tooltip_link fun( item_link: ItemLink ): TooltipItemLink
---@field bind_abbrev fun( bind: BindType ): string?
---@field make_item MakeItemFn
---@field make_dropped_item MakeDroppedItemFn
---@field make_softres_dropped_item MakeSoftRessedDroppedItemFn
---@field make_hardres_dropped_item MakeHardRessedDroppedItemFn
---@field make_coin fun( texture: string, amount_text: string ): Coin
---@param item_link ItemLink
---@return number?
function M.get_item_id( item_link )
for item_id in string.gmatch( item_link, "|c%x%x%x%x%x%x%x%x|Hitem:(%d+):.+|r" ) do
return tonumber( item_id )
end
end
---@param item_link ItemLink
---@return string
function M.get_item_name( item_link )
local result = string.gsub( item_link, "|c%x%x%x%x%x%x%x%x|Hitem:%d+.*|h%[(.*)%]|h|r", "%1" )
return result
end
---@param item_link string
---@return string?
function M.parse_link( item_link )
if not item_link then return end
for link in string.gmatch( item_link, "|c%x%x%x%x%x%x%x%x|Hitem:%d+.-|h%[.-%]|h|r" ) do
return link
end
end
---@param item_links string
---@return ItemLink[]
function M.parse_all_links( item_links )
local result = {}
if not item_links then return result end
for item_link in string.gmatch( item_links, "|c%x%x%x%x%x%x%x%x|Hitem:[^%]]+%]|h|r" ) do
table.insert( result, item_link )
end
return result
end
---@param item_link ItemLink
---@return TooltipItemLink
function M.get_tooltip_link( item_link )
return string.match( item_link, "|H(item:[^|]+)|h" )
end
---@param bind BindType
---@return string?
function M.bind_abbrev( bind )
if bind == BindType.BindOnPickup or bind == BindType.Soulbound or bind == BindType.Quest then
return red( "BoP" )
elseif bind == BindType.BindOnEquip then
return white( "BoE" )
end
end
---@param id number
---@param name string
---@param link ItemLink
---@param quality ItemQuality?
---@param texture string?
---@return Item
function M.make_item( id, name, link, quality, texture )
return {
id = id,
name = name,
link = link,
quality = quality,
texture = texture,
type = LootType.Item
}
end
---@param id number
---@param name string
---@param link ItemLink
---@param tooltip_link TooltipItemLink
---@param quality ItemQuality?
---@param quantity number?
---@param texture string?
---@param bind BindType?
---@param classes table<number, PlayerClass>?
---@param is_boss_loot boolean?
---@return DroppedItem
function M.make_dropped_item( id, name, link, tooltip_link, quality, quantity, texture, bind, classes, is_boss_loot )
return {
id = id,
name = name,
link = link,
tooltip_link = tooltip_link,
quality = quality,
quantity = quantity,
texture = texture,
bind = bind or BindType.None,
classes = classes,
is_boss_loot = is_boss_loot,
type = LootType.DroppedItem
}
end
---@param item DroppedItem
---@param sr_players RollingPlayer[]
---@return SoftRessedDroppedItem
function M.make_softres_dropped_item( item, sr_players )
---@param a RollingPlayer
---@param b RollingPlayer
local function sort( a, b ) return a.name < b.name end
local players = sr_players or {}
table.sort( players, sort )
return {
id = item.id,
name = item.name,
link = item.link,
tooltip_link = item.tooltip_link,
quality = item.quality,
quantity = item.quantity,
texture = item.texture,
bind = item.bind,
sr_players = players,
type = LootType.SoftRessedDroppedItem
}
end
---@param item DroppedItem
---@return HardRessedDroppedItem
function M.make_hardres_dropped_item( item )
return {
id = item.id,
name = item.name,
link = item.link,
tooltip_link = item.tooltip_link,
quality = item.quality,
quantity = item.quantity,
texture = item.texture,
bind = item.bind,
type = LootType.HardRessedDroppedItem
}
end
---@param texture string
---@param amount_text string
---@return Coin
function M.make_coin( texture, amount_text )
return {
texture = texture,
amount_text = amount_text,
type = LootType.Coin
}
end
m.ItemUtils = M
return M

90
src/LootAutoProcess.lua Normal file
View File

@ -0,0 +1,90 @@
RollFor = RollFor or {}
local m = RollFor
if m.LootAutoProcess then return end
local M = {}
local getn = m.getn
---@type LT
local LT = m.ItemUtils.LootType
local clear_table = m.clear_table
---@class LootAutoProcess
---@field on_loot_opened fun()
---@field on_loot_slot_cleared fun( slot: number )
---@field on_loot_closed fun()
---@param config Config
---@param roll_tracker RollTracker
---@param loot_list LootList
---@param roll_controller RollController
---@param player_info PlayerInfo
---@return LootAutoProcess
function M.new( config, roll_tracker, loot_list, roll_controller, player_info )
local loot_cache = {}
-- local selected_loot_list_item
local function process_next_item()
local threshold = m.api.GetLootThreshold()
local data = roll_tracker.get()
local items = loot_list.get_items()
local item_count = getn( items )
if item_count == 0 then return end
local is_coin = items[ 1 ].type == LT.Coin
local first_item = not is_coin and items[ 1 ]
if first_item and first_item.quality >= threshold and not data.status then
local count = loot_list.count( first_item.id )
roll_controller.preview( first_item, count )
end
end
local function on_loot_slot_cleared( slot )
loot_cache[ slot ] = nil
end
local function on_loot_opened()
for _, item in ipairs( loot_list.get_items() ) do
local slot = loot_list.get_slot( item.id )
if slot then
loot_cache[ slot ] = item
end
end
if not config.auto_process_loot() or not player_info.is_master_looter() then return end
if config.autostart_loot_process() then
process_next_item()
end
end
local function on_loot_closed()
clear_table( loot_cache )
if m.vanilla then loot_cache.n = 0 end
end
-- local function on_loot_list_item_selected( selected_item )
-- selected_loot_list_item = selected_item
-- end
--
-- local function on_loot_list_item_deselected()
-- selected_loot_list_item = nil
-- end
roll_controller.subscribe( "process_next_item", process_next_item )
-- roll_controller.subscribe( "loot_list_item_selected", on_loot_list_item_selected )
-- roll_controller.subscribe( "loot_list_item_deselected", on_loot_list_item_deselected )
return {
on_loot_opened = on_loot_opened,
on_loot_slot_cleared = on_loot_slot_cleared,
on_loot_closed = on_loot_closed
}
end
m.LootAutoProcess = M
return M

74
src/LootAwardCallback.lua Normal file
View File

@ -0,0 +1,74 @@
RollFor = RollFor or {}
local m = RollFor
if m.LootAwardCallback then return end
local getn = m.getn
local M = m.Module.new( "LootAwardCallback" )
---@class LootAwardCallback
---@field on_loot_awarded fun( item_id: number, item_link: string, player_name: string, player_class: string?, is_trade: boolean? )
---@param awarded_loot AwardedLoot
---@param roll_controller RollController
---@param winner_tracker WinnerTracker
---@param group_roster GroupRoster
---@param softres GroupAwareSoftRes
function M.new( awarded_loot, roll_controller, winner_tracker, group_roster, softres )
---@param item_id number
---@param item_link string
---@param player_name string
---@param player_class PlayerClass?
local function on_loot_awarded( item_id, item_link, player_name, player_class, is_trade )
M.debug.add( string.format( "on_loot_awarded( %s, %s, %s, %s )", item_id, item_link, player_name, player_class or "nil" ) )
local roll_tracker = roll_controller.get_roll_tracker( item_id )
local _, current_iteration = roll_tracker.get()
local roll_data = m.find( player_name, current_iteration.rolls, 'player_name' )
local sr_players = softres.get( item_id )
local sr_player = m.find( player_name, sr_players, 'name' )
local rolling_strategy
local class
if roll_data then
rolling_strategy = current_iteration.rolling_strategy
else
local winners = winner_tracker.find_winners( item_link )
local winner = m.find( player_name, winners, 'winner_name' )
rolling_strategy = winner and winner.rolling_strategy
end
if not player_class then
local player = group_roster.find_player( player_name )
class = player and player.class or nil
end
awarded_loot.award(
player_name,
item_id,
roll_data,
rolling_strategy,
item_link,
player_class or class,
sr_player and sr_player.sr_plus
)
if is_trade then return end
if player_class then
roll_controller.loot_awarded( item_id, item_link, player_name, player_class )
else
roll_controller.loot_awarded( item_id, item_link, player_name, class )
end
winner_tracker.untrack( player_name, item_link )
end
---@type LootAwardCallback
return {
on_loot_awarded = on_loot_awarded,
}
end
m.LootAwardCallback = M
return M

224
src/LootAwardPopup.lua Normal file
View File

@ -0,0 +1,224 @@
RollFor = RollFor or {}
local m = RollFor
if m.LootAwardPopup then return end
local M = m.Module.new( "LootAwardPopup" )
local getn = m.getn
local RS = m.Types.RollingStrategy
local RT = m.Types.RollType
local LAE = m.Types.LootAwardError
local red = m.colors.red
local blue = m.colors.blue
local c = m.colorize_player_by_class
local r = m.roll_type_color
local possesive_case = m.possesive_case
local article = m.article
local button_defaults = {
width = 80,
height = 24,
scale = 0.76
}
---@class LootAwardPopup
---@field show fun( data: MasterLootConfirmationData )
---@field hide fun()
---@param popup_builder PopupBuilder
---@param config Config
---@param rolling_popup RollingPopup
function M.new( popup_builder, config, rolling_popup )
local popup
local top_padding = config.classic_look() and 18 or 14
local on_hide ---@type fun()?
local function create_popup()
local builder = popup_builder
:name( "RollForLootAssignmentFrame" )
:width( 280 )
:height( 100 )
:sound()
:esc()
:gui_elements( m.GuiElements )
:on_hide( function()
if on_hide then
on_hide()
end
end )
:self_centered_anchor()
:strata( "DIALOG" )
local anchor = rolling_popup.get_anchor_point()
if anchor then
builder = builder:point( anchor )
end
local frame = builder:build()
return frame
end
local function border_color( item_id )
local _, _, quality = m.api.GetItemInfo( string.format( "item:%s:0:0:0", item_id ) )
local color = m.get_popup_border_color( quality or 0 )
local col = config.classic_look() and m.brighten( color, 0.5 ) or color
popup:border_color( col.r, col.g, col.b, col.a )
end
---@param content table
---@param winners Winner[]
---@param receiver ItemCandidate
---@diagnostic disable-next-line: unused-local
local function add_raid_roll_winners( content, winners, receiver ) -- TODO: To fix the popup display.
for i, winner in ipairs( winners ) do
local padding = i > 1 and 2 or 8
local player = c( winner.name, winner.class )
table.insert( content, { type = "text", value = string.format( "%s wins the %s.", player, blue( "raid-roll" ) ), padding = padding } )
end
end
---@param winner Winner
---@param padding number?
local function sr_content( winner, padding )
M.debug.add( "sr_content" )
local player = c( winner.name, winner.class )
local soft_ressed = r( RT.MainSpec, "soft-ressed" )
return { type = "text", value = string.format( "%s %s this item.", player, soft_ressed ), padding = padding or top_padding }
end
---@param content table
---@param winners Winner[]
---@param strategy_type RollingStrategyType
local function add_roll_winners( content, winners, strategy_type )
local last_award_button_visible = false
for i, winner in ipairs( winners ) do
local player = c( winner.name, winner.class )
local roll_type = winner.roll_type and r( winner.roll_type )
local roll = winner.winning_roll and blue( winner.winning_roll )
local padding = last_award_button_visible and 8 or i == 1 and top_padding or (top_padding - 6)
if roll then
table.insert( content,
{ type = "text", value = string.format( "%s wins the %s roll with %s %s.", player, roll_type, article( winner.winning_roll ), roll ), padding = padding } )
elseif strategy_type == RS.SoftResRoll then
table.insert( content, sr_content( winner, padding ) )
else
table.insert( content, { type = "text", value = string.format( "%s %s win the roll.", player, red( "did not" ) ), padding = padding } )
end
end
end
---@param content table
---@param data MasterLootConfirmationData
local function add_winners( content, data )
if data.strategy_type == RS.InstaRaidRoll or data.strategy_type == RS.RaidRoll then
add_raid_roll_winners( content, data.winners, data.receiver )
else
add_roll_winners( content, data.winners, data.strategy_type )
end
end
---@param data MasterLootConfirmationData
local function make_content( data )
local content = { { type = "item_link_with_icon", link = data.item.link, texture = data.item.texture } }
local winner_count = getn( data.winners )
if winner_count > 0 then
add_winners( content, data )
end
local name = c( data.receiver.name, data.receiver.class )
-- TODO: check if receiver is a winner and add a warning if not.
table.insert( content, { type = "text", value = string.format( "Award this item to %s?", name ), padding = 6 } )
if data.error then
local message = data.error == LAE.FullBags and string.format( "%s%s %s", name, red( possesive_case( data.receiver.name ) ), red( "bags are full." ) ) or
data.error == LAE.AlreadyOwnsUniqueItem and string.format( "%s %s", name, red( "already owns this unique item." ) ) or
data.error == LAE.PlayerNotFound and string.format( "%s %s", name, red( "cannot be found." ) ) or
data.error == LAE.CantAssignItemToThatPlayer and string.format( "%s %s.", red( "Can't assign this item to" ), name ) or nil
if message then
table.insert( content, { type = "text", value = message, padding = 7 } )
end
end
table.insert( content, { type = "button", label = "Yes", width = 80, on_click = data.confirm_fn } )
table.insert( content, {
type = "button",
label = "No",
width = 80,
on_click = function()
on_hide = nil
data.abort_fn()
end
} )
return content
end
---@param data MasterLootConfirmationData
local function show( data )
if not popup then popup = create_popup() end
popup:clear()
on_hide = data.abort_fn
for _, v in ipairs( make_content( data ) ) do
popup.add_line( v.type, function( type, frame, lines )
if type == "item_link_with_icon" then
frame:SetItem( v, v.link and m.ItemUtils.get_tooltip_link( v.link ) )
elseif type == "text" then
frame:SetText( v.value )
elseif type == "button" then
frame:SetWidth( v.width or button_defaults.width )
frame:SetHeight( v.height or button_defaults.height )
frame:SetText( v.label or "" )
frame:SetScale( v.scale or button_defaults.scale )
frame:SetScript( "OnClick", v.on_click or function() end )
frame:SetFrameLevel( popup:GetFrameLevel() + 1 )
end
if type ~= "button" then
local count = getn( lines )
if count == 0 then
local y = -top_padding - (v.padding or 0)
frame:ClearAllPoints()
frame:SetPoint( "TOP", popup, "TOP", 0, y )
else
local line_anchor = lines[ count ].frame
frame:ClearAllPoints()
frame:SetPoint( "TOP", line_anchor, "BOTTOM", 0, v.padding and -v.padding or 0 )
end
end
end, v.padding )
end
border_color( data.item.id )
popup:ClearAllPoints()
popup:SetPoint( "CENTER", rolling_popup.get_frame(), "CENTER", 0, 0 )
popup:Show()
end
local function hide()
if popup then
on_hide = nil
popup:Hide()
end
end
---@type LootAwardPopup
return {
show = show,
hide = hide
}
end
m.LootAwardPopup = M
return M

344
src/LootController.lua Normal file
View File

@ -0,0 +1,344 @@
RollFor = RollFor or {}
local m = RollFor
if m.LootController then return end
local M = m.Module.new( "LootController" )
local getn = m.getn
local red, orange, hl = m.colors.red, m.colors.orange, m.colors.hl
local item_utils = m.ItemUtils
---@alias SelectedItem { item_id: number, comment: string? }
---@param player_info PlayerInfo
---@param loot_facade LootFacade
---@param loot_list LootList
---@param loot_frame LootFrame
---@param roll_controller RollController
---@param softres GroupAwareSoftRes
---@param rolling_logic RollingLogic
---@param chat Chat
function M.new( player_info, loot_facade, loot_list, loot_frame, roll_controller, softres, rolling_logic, chat )
-- This will store which items were selected, because we'll lost that info when the loot is closed.
-- Upon loot opening, we'll check it here and reselect if appropriate.
local item_selection_cache = {}
local selected_item = nil ---@type SelectedItem?
local function show()
M.debug.add( "show" )
loot_frame.show()
end
---@param item SelectedItem
local function make_cache_key( item )
return string.format( "%s|%s", item.item_id, item.comment or "" )
end
---@param entries LootListEntry[]
---@param item_id number
---@param comment string?
local function count_selected_items( entries, item_id, comment )
local result = 0
for _, entry in ipairs( entries ) do
if entry.item.id == item_id and entry.comment == comment then
result = result + 1
end
end
return result
end
---@param item_id number
---@param entries LootListEntry[]
local function find_top_priority_comment( item_id, entries ) -- HR > SR > none
for _, entry in ipairs( entries ) do
if entry.item.id == item_id and entry.hard_ressed then
return entry.comment
end
if entry.item.id == item_id and entry.soft_ressed then
return entry.comment
end
end
end
---@param entries LootListEntry[]
---@param item DroppedItem
---@param hard_ressed boolean?
---@param soft_ressed boolean?
local function select_item( entries, item, hard_ressed, soft_ressed )
M.debug.add( string.format( "select_item( %s, %s )", item.id, hard_ressed and "hr" or soft_ressed and "sr" or "free roll" ) )
local c = find_top_priority_comment( item.id, entries )
local count = count_selected_items( entries, item.id, c )
selected_item = { item_id = item.id, comment = c }
local key = make_cache_key( selected_item )
item_selection_cache[ key ] = selected_item
roll_controller.preview( item, count )
end
---@param items (DroppedItem|Coin)[]
local function make_sr_player_map( items )
local result = {}
for _, item in ipairs( items ) do
if item.type ~= "Coin" then
result[ item.id ] = softres.get( item.id )
end
end
return result
end
---@return RollingPlayer?
local function pop_first_item_from_a_table( t )
if getn( t ) == 0 then return nil end
local result = t[ 1 ]
table.remove( t, 1 )
return result
end
---@param sr_players RollingPlayer[]
local function make_comment_tooltip( sr_players )
local result = { orange( "Soft-ressed by" ) }
if getn( sr_players ) == 1 then
local player = sr_players[ 1 ]
table.insert( result, m.colorize_player_by_class( player.name, player.class ) )
return result
end
for _, player in ipairs( sr_players ) do
local rolls = player.rolls and player.rolls > 1 and hl( string.format( " [%s rolls]", player.rolls ) ) or ""
table.insert( result, string.format( "%s%s", m.colorize_player_by_class( player.name, player.class ), rolls ) )
end
return result
end
---@alias LootListEntry {
--- item: DroppedItem|Coin,
--- comment: string?,
--- comment_tooltip: string?,
--- hard_ressed: boolean?,
--- soft_ressed: boolean? }
---@param item_id number
---@param entries LootListEntry[]
local function is_already_hr( item_id, entries )
for _, entry in ipairs( entries ) do
if entry.item.id == item_id and entry.hard_ressed then return true end
end
return false
end
---@param items (DroppedItem|Coin)[]
---@return LootListEntry[]
local function get_entries( items )
local result = {}
local sr_player_map = make_sr_player_map( items )
for _, item in ipairs( items ) do
if item.type == "Coin" then
table.insert( result, { item = item } )
elseif softres.is_item_hardressed( item.id ) and not is_already_hr( item.id, result ) then
table.insert( result, { item = item, comment = red( "HR" ), hard_ressed = true } )
else
local sr_players = softres.get( item.id )
local sr_player_count = getn( sr_players )
local item_count = loot_list.count( item.id )
if is_already_hr( item.id, result ) then item_count = item_count - 1 end
if item_count > 0 then
if sr_player_count > 0 then
if sr_player_count > item_count then
table.insert( result, { item = item, comment = orange( "SR" ), comment_tooltip = make_comment_tooltip( sr_players ), soft_ressed = true } )
else
local sr_player = pop_first_item_from_a_table( sr_player_map[ item.id ] )
if sr_player then
table.insert( result, { item = item, comment = orange( "SR" ), comment_tooltip = make_comment_tooltip( { sr_player } ), soft_ressed = true } )
else
table.insert( result, { item = item } )
end
end
else
table.insert( result, { item = item } )
end
end
end
end
return result
end
---@param item_id number
---@param comment string?
local function should_be_selected( item_id, comment )
if selected_item and selected_item.item_id == item_id and selected_item.comment == comment then
return true
end
return false
end
---@param entries LootListEntry[]
local function find_selected_item( entries )
for _, entry in ipairs( entries ) do
if entry.item.type ~= "Coin" then
local key = make_cache_key( { item_id = entry.item.id, comment = entry.comment } )
if item_selection_cache[ key ] then
return item_selection_cache[ key ]
end
end
end
end
local function update()
M.debug.add( "update" )
local items = loot_list.get_items() ---@type (DroppedItem|Coin)[]
local entries = get_entries( items )
selected_item = find_selected_item( entries )
---@type LootFrameItem[]
local result = {}
for index, entry in ipairs( entries ) do
local item = entry.item
local is_coin = item.type == "Coin"
local selected = should_be_selected( item.id, entry.comment )
local selected_entry = entry -- Fucking lua50 and its broken closures.
---@type LootFrameItem
table.insert( result, {
index = index,
texture = item.texture,
name = is_coin and m.one_line_coin_name( item.amount_text ) or item.name,
quality = item.quality or 0,
quantity = item.quantity,
link = item.link,
click_fn = function()
if m.is_ctrl_key_down() then
m.api.DressUpItemLink( item.link )
return
end
if m.is_shift_key_down() then
m.link_item_in_chat( item.link )
return
end
if m.bcc and (is_coin or item.quality < 2) then
local slot = loot_list.get_slot( item.id )
if slot then loot_facade.loot_slot( slot ) end
return
end
if rolling_logic.is_rolling() then
chat.info( "Cannot select item while rolling is in progress.", m.colors.red )
return
end
local master_loot = m.is_master_loot()
if is_coin or selected or not master_loot then return end
if master_loot and not player_info.is_master_looter() then
chat.info( "You are not the master looter.", m.colors.red )
return
end
select_item( entries, selected_entry.item --[[@as DroppedItem]], selected_entry.hard_ressed, selected_entry.soft_ressed ); update()
end,
is_selected = selected or false,
is_enabled = selected or not selected_item or false,
slot = loot_list.get_slot( is_coin and "Coin" or item.id ),
tooltip_link = item.tooltip_link,
comment = entry.comment,
comment_tooltip = entry.comment_tooltip,
bind = item_utils.bind_abbrev( item.bind )
} )
end
loot_frame.update( result )
end
local function hide()
M.debug.add( "hide" )
loot_frame.hide()
end
---@class LootFrameDeselectData
---@field item_id number?
---@param data LootFrameDeselectData
local function deselect( data )
if data.item_id then
local key = string.format( "^%s", data.item_id )
for k, _ in pairs( item_selection_cache ) do
if string.find( k, key ) then item_selection_cache[ k ] = nil end
end
update()
return
end
if not selected_item then return end
M.debug.add( "deselect" )
local key = make_cache_key( selected_item )
item_selection_cache[ key ] = nil
selected_item = nil
update()
end
local function on_loot_opened()
M.debug.add( "loot_opened" )
show()
update()
if selected_item then
roll_controller.update( selected_item.item_id )
end
end
local function on_loot_slot_cleared( slot )
M.debug.add( string.format( "loot_slot_cleared( %s )", slot ) )
update()
end
local function on_loot_closed()
M.debug.add( "loot_closed" )
hide()
end
---@param item_id number
local function clear_selection_cache( item_id )
for k, v in pairs( item_selection_cache ) do
if v.item_id == item_id then item_selection_cache[ k ] = nil end
end
end
loot_facade.subscribe( "LootOpened", on_loot_opened )
loot_facade.subscribe( "LootClosed", on_loot_closed )
loot_facade.subscribe( "LootSlotCleared", on_loot_slot_cleared )
roll_controller.subscribe( "LootFrameDeselect", deselect )
roll_controller.subscribe( "LootFrameClearSelectionCache", clear_selection_cache )
roll_controller.subscribe( "LootFrameUpdate", update )
end
m.LootController = M
return M

132
src/LootFacade.lua Normal file
View File

@ -0,0 +1,132 @@
RollFor = RollFor or {}
local m = RollFor
if m.LootFacade then return end
local M = {}
local interface = m.Interface
M.interface = {
subscribe = "function",
get_item_count = "function",
get_source_guid = "function",
get_link = "function",
get_info = "function",
is_item = "function",
is_coin = "function",
loot_slot = "function"
}
---@class LootSlotInfo
---@field texture string
---@field name string
---@field quantity number
---@field quality number
---@class LootFacade
---@field subscribe fun( event_name: LootEventName, callback: fun( arg: any? ) )
---@field get_item_count fun(): number
---@field get_source_guid fun(): string
---@field get_link fun( slot: number ): ItemLink
---@field get_info fun( slot: number ): LootSlotInfo
---@field is_item fun( slot: number ): boolean
---@field is_coin fun( slot: number ): boolean
---@field loot_slot fun( slot: number )
---@alias LootEventName
---| "LootOpened"
---| "LootClosed"
---| "LootSlotCleared"
---| "ChatMsgLoot"
function M.new( event_frame, api )
interface.validate( api, m.WowApi.LootInterface )
---@param event_name LootEventName
---@param callback fun()
local function subscribe( event_name, callback )
local blizz_event =
event_name == "LootOpened" and "LOOT_OPENED" or
event_name == "LootClosed" and "LOOT_CLOSED" or
event_name == "LootSlotCleared" and "LOOT_SLOT_CLEARED" or
event_name == "ChatMsgLoot" and "CHAT_MSG_LOOT"
if blizz_event then
event_frame.subscribe( blizz_event, callback )
end
end
---@return number
local function get_item_count()
return api.GetNumLootItems()
end
---@return string?
local function get_source_guid()
return m.UnitGUID( api, "target" )
end
---@param slot number
---@return ItemLink?
local function get_link( slot )
return api.GetLootSlotLink( slot )
end
---@param slot number
---@return LootSlotInfo?
local function get_info( slot )
if m.vanilla then
local texture, name, quantity, quality = api.GetLootSlotInfo( slot )
return texture and {
texture = texture,
name = name,
quantity = quantity,
quality = quality
} or nil
else
local texture, name, quantity, _, quality = api.GetLootSlotInfo( slot )
return texture and {
texture = texture,
name = name,
quantity = quantity,
quality = quality
} or nil
end
end
---@param slot number
---@return boolean
local function is_item( slot )
local slot_type = api.GetLootSlotType( slot )
return slot_type == api.LOOT_SLOT_ITEM
end
---@param slot number
---@return boolean
local function is_coin( slot )
local slot_type = api.GetLootSlotType( slot )
return slot_type == api.LOOT_SLOT_MONEY
end
---@param slot number
local function loot_slot( slot )
api.LootSlot( slot )
end
---@type LootFacade
return {
subscribe = subscribe,
get_item_count = get_item_count,
get_source_guid = get_source_guid,
get_link = get_link,
get_info = get_info,
is_item = is_item,
is_coin = is_coin,
loot_slot = loot_slot
}
end
m.LootFacade = M
return M

View File

@ -0,0 +1,76 @@
RollFor = RollFor or {}
local m = RollFor
if m.LootFacadeListener then return end
local IU = m.ItemUtils
local M = {}
---@param loot_facade LootFacade
---@param auto_loot AutoLoot
---@param dropped_loot_announce DroppedLootAnnounce
---@param master_loot MasterLoot
---@param auto_group_loot AutoGroupLoot
---@param roll_controller RollController
---@param player_info PlayerInfo
function M.new(
loot_facade,
auto_loot,
dropped_loot_announce,
master_loot,
auto_group_loot,
roll_controller,
player_info
)
loot_facade.subscribe( "LootOpened", function()
auto_loot.on_loot_opened()
dropped_loot_announce.on_loot_opened()
master_loot.on_loot_opened()
auto_group_loot.on_loot_opened()
roll_controller.loot_opened()
end )
loot_facade.subscribe( "LootClosed", function()
roll_controller.loot_closed()
end )
loot_facade.subscribe( "LootSlotCleared", function( slot )
master_loot.on_loot_slot_cleared( slot )
auto_group_loot.on_loot_slot_cleared()
end )
-- This covers the scenario where the master looter assigns the loot and then moves immediately,
-- causing the loot frame to close. In normal circumstances, when the last item gets assigned,
-- the LOOT_SLOT_CLEARED fires and then LOOT_CLOSED event follows. In this case, however,
-- LOOT_CLOSED fires first, because of the player movement and the LOOT_SLOT_CLEARED doesn't
-- (because we're not looting anymore).
local function on_chat_msg_loot( message )
for player_name, link_with_optional_quantity in string.gmatch( message, "(.-) receives loot: (.*)" ) do
local item_link = IU.parse_link( link_with_optional_quantity )
local item_id = item_link and IU.get_item_id( item_link )
if item_id and item_link then
master_loot.on_loot_received( player_name, item_id, item_link )
end
return
end
for link_with_optional_quantity in string.gmatch( message, "You receive loot: (.*)" ) do
local item_link = IU.parse_link( link_with_optional_quantity )
local item_id = item_link and IU.get_item_id( item_link )
if item_id and item_link then
master_loot.on_loot_received( player_info.get_name(), item_id, item_link )
end
return
end
end
loot_facade.subscribe( "ChatMsgLoot", on_chat_msg_loot )
end
m.LootFacadeListener = M
return M

219
src/LootFrame.lua Normal file
View File

@ -0,0 +1,219 @@
RollFor = RollFor or {}
local m = RollFor
if m.LootFrame then return end
local M = m.Module.new( "LootFrame" )
---@class LootFrame
---@field show fun()
---@field update fun( items: LootFrameItem[] )
---@field hide fun()
---@field get_frame fun(): Frame
M.center_point = { point = "CENTER", relative_point = "CENTER", x = -260, y = 220 }
---@class LootFrameSkin
---@field header fun( on_drag_stop: function, on_show: function, on_hide: function ): Frame
---@field body fun( parent: Frame ): Frame
---@field dropped_item fun(): Frame
---@field footer fun( parent: Frame ): Frame?
---@field get_item_height fun(): number
---@param loot_frame_skin LootFrameSkin
---@param db table
---@param config Config
function M.new( loot_frame_skin, db, config )
---@type Frame
local header_frame
---@type Frame
local body_frame
---@type Frame?
local footer_frame
local boss_name_width = 0
local max_frame_width
local max_item_count
local function on_drag_stop( frame )
local point, _, relative_point, x, y = frame:GetPoint()
if m.is_frame_out_of_bounds( frame ) then
db.point = M.center_point
frame:position( M.center_point )
return
end
db.point = { point = point, relative_point = relative_point, x = x, y = y }
end
local function create_header_frame()
local function on_show()
body_frame:Show()
if footer_frame then footer_frame:Show() end
end
local function on_hide()
body_frame:Hide()
if footer_frame then footer_frame:Hide() end
end
local frame = loot_frame_skin.header( on_drag_stop, on_show, on_hide )
frame:ClearAllPoints()
if db.point then
local p = db.point
---@diagnostic disable-next-line: undefined-global
frame:SetPoint( p.point, UIParent, p.relative_point, p.x, p.y )
else
frame:position( M.center_point )
end
return frame
end
local function update_boss_name_frame()
header_frame.clear()
header_frame.add_line( "text", function( type, frame )
if type == "text" then
frame:ClearAllPoints()
frame:SetHeight( 16 )
frame:SetPoint( "CENTER", 1, 0 )
frame:SetTextColor( 0.125, 0.624, 0.976 )
local name = m.api.UnitName( "target" )
if not name then
frame:SetText( "Loot" )
else
frame:SetText( string.format( "%s%s Loot", name, m.possesive_case( name ) ) )
end
boss_name_width = frame:GetStringWidth() + 30
end
end, 0 )
end
local function show()
M.debug.add( "show" )
update_boss_name_frame()
max_frame_width = nil
max_item_count = nil
header_frame:Show()
end
local function hide()
if header_frame then
M.debug.add( "hide" )
header_frame:Hide()
end
end
---@class LootFrameItem
---@field index number
---@field texture ItemTexture
---@field name string
---@field quality ItemQuality
---@field quantity number
---@field link ItemLink
---@field click_fn fun()
---@field is_selected boolean
---@field is_enabled boolean
---@field slot number?
---@field tooltip_link TooltipItemLink?
---@field comment string?
---@field comment_tooltip string[]?
---@field bind string?
---@param items LootFrameItem[]
local function update( items )
M.debug.add( "update" )
body_frame.clear()
local content = {}
for _, item in ipairs( items ) do
table.insert( content, {
type = "dropped_item",
item = item
} )
end
local max_width = 0
local anchor
local item_count = 0
local frames = {}
for _, v in ipairs( content ) do
body_frame.add_line( v.type, function( type, frame )
if type == "dropped_item" then
local item = v.item ---@type LootFrameItem
frame:SetItem( item )
frame:ClearAllPoints()
if max_frame_width then
frame:SetWidth( max_frame_width - 2 )
end
if not anchor then
frame:SetPoint( "TOPLEFT", body_frame, "TOPLEFT", 0, 0 )
frame:SetPoint( "TOPRIGHT", body_frame, "TOPRIGHT", 0, 0 )
else
frame:SetPoint( "TOPLEFT", anchor, "BOTTOMLEFT", 0, 0 )
frame:SetPoint( "TOPRIGHT", anchor, "BOTTOMRIGHT", 0, 0 )
end
anchor = frame
local w = frame:GetWidth() + 2
if w > max_width then max_width = w end
item_count = item_count + 1
table.insert( frames, frame )
end
end, 0 )
end
max_frame_width = m.lua.math.max( boss_name_width, max_width )
max_item_count = max_item_count or item_count
header_frame:SetWidth( max_frame_width )
body_frame:SetWidth( max_frame_width )
body_frame:SetHeight( item_count * loot_frame_skin.get_item_height() + 1 )
if footer_frame then
footer_frame:ClearAllPoints()
footer_frame:SetWidth( max_frame_width )
footer_frame:SetPoint( "TOP", body_frame, "BOTTOM", 0, 2 )
end
for _, frame in ipairs( frames ) do
frame:SetWidth( max_frame_width - 2 )
end
if config.loot_frame_cursor() and item_count == max_item_count then
local uiScale, x, y = m.api.UIParent:GetEffectiveScale(), m.api.GetCursorPosition()
header_frame:SetPoint( "TOPLEFT", m.api.UIParent, "BOTTOMLEFT", (x / uiScale) -10, (y / uiScale) + 30 )
end
end
config.subscribe( "reset_loot_frame", function()
db.point = nil
if header_frame then header_frame:position( M.center_point ) end
end )
header_frame = create_header_frame()
body_frame = loot_frame_skin.body( header_frame )
footer_frame = loot_frame_skin.footer( header_frame )
---@type LootFrame
return {
show = show,
update = update,
hide = hide,
get_frame = function() return header_frame end
}
end
m.LootFrame = M
return M

180
src/LootList.lua Normal file
View File

@ -0,0 +1,180 @@
RollFor = RollFor or {}
local m = RollFor
if m.LootList then return end
local M = m.Module.new( "LootList" )
local getn = m.getn
local interface = m.Interface
local clear = m.clear_table
---@class LootList
---@field get_items fun(): DroppedItem[]
---@field get_source_guid fun(): string
---@field get_slot fun( item_id: number|"Coin" ): number?
---@field is_looting fun(): boolean
---@field count fun( item_id: number ): number
---@field size fun(): number
---@param loot_facade LootFacade
---@param item_utils ItemUtils
---@param tooltip_reader TooltipReader
---@param boss_list BossList
---@return LootList
function M.new( loot_facade, item_utils, tooltip_reader, boss_list, dummy_items_fn )
interface.validate( loot_facade, m.LootFacade.interface )
interface.validate( item_utils, m.ItemUtils.interface )
---@alias Slot number
---@type table<Slot, Coin|DroppedItem>
local items = {}
local lf = loot_facade
local looting = false
local source_guid
local function clear_items()
clear( items )
source_guid = nil
end
local function add_item( slot, item, item_count )
local dummy_items = dummy_items_fn and dummy_items_fn() or {}
local dummy_item_count = getn( dummy_items )
local new_item = item_count > dummy_item_count and item or dummy_items[ item_count ]
items[ slot ] = new_item
end
local function on_loot_opened()
M.debug.add( "loot_opened" )
clear_items()
looting = true
source_guid = lf.get_source_guid()
local item_count = 1
for slot = 1, lf.get_item_count() do
if lf.is_coin( slot ) then
local info = lf.get_info( slot )
if info then
items[ slot ] = item_utils.make_coin( info.texture, info.name )
end
else
local link = lf.get_link( slot )
local info = lf.get_info( slot )
local item_id = link and item_utils.get_item_id( link )
local item_name = link and item_utils.get_item_name( link )
local tooltip_link = link and item_utils.get_tooltip_link( link )
local bind_type = tooltip_reader.get_slot_bind_type( slot )
local classes = tooltip_reader.get_slot_classes( slot )
local is_boss_loot = false
if m.api.UnitName then -- workaround to make tests work
local target_name = m.target_name()
if target_name and m.target_dead() then
local zone_name = m.api.GetRealZoneText()
local bosses = boss_list[ zone_name ] or {}
is_boss_loot = m.table_contains_value( bosses, target_name )
end
end
if item_id and item_name then
add_item( slot,
item_utils.make_dropped_item(
item_id,
item_name,
link,
tooltip_link,
info and info.quality,
info and info.quantity,
info and info.texture,
bind_type,
classes,
is_boss_loot
), item_count )
item_count = item_count + 1
end
end
end
end
local function on_loot_closed()
M.debug.add( "loot_closed" )
clear_items()
looting = false
end
local function on_loot_slot_cleared( slot )
M.debug.add( "loot_slot_cleared" )
items[ slot ] = nil
end
local function get_items()
local result = {}
for _, item in pairs( items ) do
table.insert( result, item )
end
return result
end
loot_facade.subscribe( "LootOpened", on_loot_opened )
loot_facade.subscribe( "LootClosed", on_loot_closed )
loot_facade.subscribe( "LootSlotCleared", on_loot_slot_cleared )
---@param item_id number|"Coin"
---@return number?
local function get_slot( item_id )
for slot, item in pairs( items ) do
if item_id == "Coin" and item.type == "Coin" then
return slot
end
if item.id == item_id then
return slot
end
end
end
local function is_looting()
return looting
end
local function count( item_id )
local result = 0
for _, item in pairs( items ) do
if item.id == item_id then
result = result + 1
end
end
return result
end
local function size()
local result = 0
for _ in pairs( items ) do
result = result + 1
end
return 0
end
---@type LootList
return {
get_items = get_items,
get_source_guid = function() return source_guid end,
get_slot = get_slot,
is_looting = is_looting,
count = count,
size = size
}
end
m.LootList = M
return M

139
src/MasterLoot.lua Normal file
View File

@ -0,0 +1,139 @@
RollFor = RollFor or {}
local m = RollFor
if m.MasterLoot then return end
local M = m.Module.new( "MasterLoot" )
local pretty_print = m.pretty_print
local hl = m.colors.hl
local clear_table = m.clear_table
local err = m.err
---@class MasterLoot
---@field on_loot_opened fun()
---@field on_recipient_inventory_full fun()
---@field on_player_is_too_far fun()
---@field on_unknown_error_message fun( message: string )
---@field on_loot_slot_cleared fun( slot: number )
---@field on_loot_received fun( player_name: string, item_id: number, item_link: string )
---@param master_loot_candidates MasterLootCandidates
---@param loot_award_callback LootAwardCallback
---@param loot_list LootList
---@param roll_controller RollController
function M.new( master_loot_candidates, loot_award_callback, loot_list, roll_controller )
---@type { player: ItemCandidate|Winner, item: Item }?
local m_confirmed = nil
local m_slot_cache = {}
local function reset_confirmation()
M.debug.add( "reset_confirmation" )
m_confirmed = nil
end
-- We are storing the item in the slot cache (m_slot_cache) and ML confirmation (m_confirmed).
-- This is to correlate the loot award event which we have to do using LOOT_SLOT_CLEARED,
-- because CHAT_MSG_LOOT doesn't seem to be synced with LOOT_ events.
-- Normally one would expect CHAT_MSG_LOOT to happen before LOOT_SLOT_CLEARED, or at least
-- before LOOT_CLOSED, but this is what happened once:
-- LOOT_OPENED -> LOOT_SLOT_CLEARED -> LOOT_CLOSED -> CHAT_MSG_LOOT.
-- It's safer and simpler to just rely on LOOT_ events.
local function on_loot_slot_cleared( slot )
M.debug.add( string.format( "on_loot_slot_cleared( %s )", slot or nil ) )
if not m_slot_cache[ slot ] or not m_confirmed then return end
local cached_item = m_slot_cache[ slot ]
if cached_item.id == m_confirmed.item.id then
loot_award_callback.on_loot_awarded( m_confirmed.item.id, m_confirmed.item.link, m_confirmed.player.name, m_confirmed.player.class )
reset_confirmation()
end
m_slot_cache[ slot ] = nil
end
---@param data AwardConfirmedData
local function on_confirm( data )
local player = data.player
local item = data.item
M.debug.add( string.format( "on_confirm( %s [%s], %s )", player and player.name or "nil", player and player.type or "nil", item and item.id or "nil" ) )
local slot = loot_list.get_slot( item.id )
if not slot then return end
if player.type ~= "ItemCandidate" and not (player.type == "Winner" and player.is_on_master_loot_candidate_list) then
err( "Player is not eligible for this item." )
return
end
m_confirmed = { item = item, player = player }
m_slot_cache[ slot ] = item
local index = master_loot_candidates.get_index( slot, player.name )
if not index then
err( "Player is not in the loot candidates list." )
return
end
m.api.GiveMasterLoot( slot, index )
end
local function on_loot_opened()
M.debug.add( "on_loot_opened" )
clear_table( m_slot_cache )
reset_confirmation()
end
local function on_loot_received( player_name, item_id, item_link )
M.debug.add( string.format( "on_loot_received( %s, %s, %s )", player_name or "nil", item_id or "nil", item_link or "nil" ) )
local is_looting = loot_list.is_looting()
if m_confirmed and is_looting then return end
if not m_confirmed then return end
-- This isn't tested, because it's hard to reproduce. Not sure if it can happen. Let's keep it here to be safe.
if m_confirmed.item.id ~= item_id then return end
loot_award_callback.on_loot_awarded( item_id, item_link, player_name )
reset_confirmation()
end
local function on_recipient_inventory_full()
if m_confirmed then
pretty_print( string.format( "%s%s bags are full.", hl( m_confirmed.player.name ), m.possesive_case( m_confirmed.player.name ) ), "red" )
reset_confirmation()
end
end
local function on_player_is_too_far()
if m_confirmed then
pretty_print( string.format( "%s is too far to receive the item.", hl( m_confirmed.player.name ) ), "red" )
reset_confirmation()
end
end
local function on_unknown_error_message( message )
if m_confirmed then
if message ~= "You are too far away!" and message ~= "You must be in a raid group to enter this instance" then
pretty_print( message, "red" )
end
reset_confirmation()
end
end
roll_controller.subscribe( "award_confirmed", on_confirm )
---@type MasterLoot
return {
on_loot_opened = on_loot_opened,
on_recipient_inventory_full = on_recipient_inventory_full,
on_player_is_too_far = on_player_is_too_far,
on_unknown_error_message = on_unknown_error_message,
on_loot_slot_cleared = on_loot_slot_cleared,
on_loot_received = on_loot_received
}
end
m.MasterLoot = M
return M

View File

@ -0,0 +1,260 @@
RollFor = RollFor or {}
local m = RollFor
if m.MasterLootCandidateSelectionFrame then return end
local M = {}
local icon_width = 16
local button_width = 85 + icon_width
local button_height = 16
local vertical_margin = 5
local horizontal_margin = 5
local horizontal_padding = 3
local vertical_padding = 5
local mod, getn = m.mod, m.getn
local function highlight( frame )
frame:SetBackdropColor( frame.color.r, frame.color.g, frame.color.b, 0.3 )
end
local function dim( frame )
frame:SetBackdropColor( 0.5, 0.5, 0.5, 0.1 )
end
local function press( frame )
frame:SetBackdropColor( frame.color.r, frame.color.g, frame.color.b, 0.7 )
end
---@param frame_builder FrameBuilderFactory
---@param config Config
local function create_main_frame( frame_builder, config )
local builder = config.classic_look() and
frame_builder.classic() or
frame_builder.modern()
:backdrop_color( 0, 0, 0, 0.8 )
:border_color( 0.851, 0.553, 0.341, 0.3 )
builder = builder
:name( "RollForPlayerSelectionFrame" )
:width( 100 )
:height( 100 )
:point( { point = "CENTER", relative_frame = m.api.UIParent, relative_point = "CENTER" } )
:enable_mouse()
:strata( "DIALOG" )
:hidden()
if config.classic_look() then
vertical_margin = 9
horizontal_margin = 10
end
local frame = builder:build()
frame:SetScript( "OnLeave",
function( self )
if m.vanilla then self = this end
local mouse_x, mouse_y = m.api.GetCursorPosition()
local x, y = self:GetCenter()
local width = self:GetWidth()
local height = self:GetHeight()
local half_width = width / 2
local half_height = height / 2
local left = x - half_width
local right = x + half_width
local top = y + half_height
local bottom = y - half_height
local is_over = mouse_x >= left and mouse_x <= right and mouse_y >= bottom and mouse_y <= top
if not is_over then self:Hide() end
end )
return frame
end
local function position_button( button, parent, index, rows )
local width = horizontal_margin + horizontal_padding + m.api.math.floor( (index - 1) / rows ) * (button_width + horizontal_padding)
local height = (-vertical_margin) - vertical_padding - (mod( index - 1, rows ) * (button_height + vertical_padding))
button:ClearAllPoints()
button:SetPoint( "TOPLEFT", parent, "TOPLEFT", width, height )
end
local function create_button( parent, index, rows )
local frame = m.create_backdrop_frame( m.api, "Button", nil, parent )
frame:SetWidth( button_width )
frame:SetHeight( button_height )
position_button( frame, parent, index, rows )
frame:SetBackdrop( { bgFile = "Interface\\Buttons\\WHITE8x8" } )
frame:SetNormalTexture( "" )
frame.parent = parent
local text = frame:CreateFontString( nil, "OVERLAY", "GameFontNormalSmall" )
text:SetPoint( "CENTER", frame, "CENTER" )
text:SetText( "" )
frame.text = text
local icon = frame:CreateTexture( nil, "ARTWORK" )
icon:SetPoint( "LEFT", text, "RIGHT", 2, 0 )
icon:SetWidth( 13 )
icon:SetHeight( 12 )
icon:SetTexture( string.format( "Interface\\AddOns\\RollFor\\assets\\star-%s.tga", "gold" ) )
icon:Hide()
frame.icon = icon
frame:SetScript( "OnEnter", function( self )
if m.vanilla then self = this end
highlight( self )
end )
frame:SetScript( "OnLeave", function( self )
if m.vanilla then self = this end
dim( self )
end )
frame:SetScript( "OnMouseDown", function( self, button )
if m.vanilla then
self = this
button = arg1
end
if button == "LeftButton" then press( self ) end
end )
frame:SetScript( "OnMouseUp", function( self, button )
if m.vanilla then
self = this
button = arg1
end
if button == "LeftButton" then
if m.api.MouseIsOver( self ) then
highlight( self )
else
dim( self )
end
end
end )
frame.unmark_winner = function()
frame.text:SetPoint( "CENTER", frame, "CENTER" )
frame.icon:Hide()
end
frame.mark_winner = function()
frame.text:SetPoint( "CENTER", frame, "CENTER", 2 - icon_width / 2, 0 )
frame.icon:Show()
end
return frame
end
---@class MasterLootCandidateSelectionFrame
---@field show fun( candidates: MasterLootCandidate[] )
---@field hide fun()
---@field get_frame fun(): Frame
---@param frame_builder FrameBuilderFactory
---@param config Config
function M.new( frame_builder, config )
local m_frame
local m_buttons = {}
local function resize_frame( total, rows )
local columns = m.api.math.ceil( total / rows )
local total_rows = total < 5 and total or rows
m_frame:SetWidth( (button_width + horizontal_padding) * columns + horizontal_padding + horizontal_margin * 2 )
m_frame:SetHeight( (button_height + vertical_padding) * total_rows + vertical_padding + vertical_margin * 2 )
end
---@param candidates MasterLootCandidate[]
local function create_candidate_frames( candidates )
local total = getn( candidates )
local rows = config.master_loot_frame_rows()
resize_frame( total, rows )
local function loop( i )
if i > total then
if m_buttons[ i ] then m_buttons[ i ]:Hide() end
return
end
local candidate = candidates[ i ]
if not m_buttons[ i ] then
m_buttons[ i ] = create_button( m_frame, i, rows )
end
local button = m_buttons[ i ]
button.text:SetText( candidate.name )
local color = m.api.RAID_CLASS_COLORS[ string.upper( candidate.class ) ]
button.color = color
button.player = candidate
if color then
button.text:SetTextColor( color.r, color.g, color.b )
dim( button )
else
button.text:SetTextColor( 1, 1, 1 )
end
button:SetScript( "OnClick", candidate.confirm_fn )
if candidate.is_winner then
button.mark_winner()
else
button.unmark_winner()
end
button:Show()
end
for i = 1, 40 do
loop( i )
end
end
---@param candidates MasterLootCandidate[]
local function show( candidates )
if not m_frame then m_frame = create_main_frame( frame_builder, config ) end
create_candidate_frames( candidates )
m_frame:Show()
end
local function hide()
if m_frame then m_frame:Hide() end
end
config.subscribe( "master_loot_frame_rows", function()
if not m_frame then return end
local total = 0
local rows = config.master_loot_frame_rows()
for i = 1, 40 do
if m_buttons[ i ] then
total = total + 1
position_button( m_buttons[ i ], m_frame, i, rows )
end
end
resize_frame( total, rows )
end )
---@type MasterLootCandidateSelectionFrame
return {
show = show,
hide = hide,
get_frame = function() return m_frame end
}
end
m.MasterLootCandidateSelectionFrame = M
return M

View File

@ -0,0 +1,118 @@
RollFor = RollFor or {}
local m = RollFor
if m.MasterLootCandidates then return end
local M = {}
---@type MakeItemCandidateFn
local make_item_candidate = m.Types.make_item_candidate
---@type MakeWinnerFn
local make_winner = m.Types.make_winner
local function get_dummy_candidates()
return {
{ name = "Ohhaimark", class = "Warrior", value = 1 },
{ name = "Obszczymucha", class = "Druid", value = 2 },
{ name = "Jogobobek", class = "Hunter", value = 3 },
{ name = "Xiaorotflmao", class = "Shaman", value = 4 },
{ name = "Kacprawcze", class = "Priest", value = 5 },
{ name = "Psikutas", class = "Paladin", value = 6 },
{ name = "Motoko", class = "Rogue", value = 7 },
{ name = "Blanchot", class = "Warrior", value = 8 },
{ name = "Adamsandler", class = "Druid", value = 9 },
{ name = "Johnstamos", class = "Hunter", value = 10 },
{ name = "Xiaolmao", class = "Shaman", value = 11 },
{ name = "Ronaldtramp", class = "Priest", value = 12 },
{ name = "Psikuta", class = "Paladin", value = 13 },
{ name = "Kusanagi", class = "Rogue", value = 14 },
{ name = "Chuj", class = "Priest", value = 15 },
}
end
---@class MasterLootCandidatesApi
---@field GetMasterLootCandidate fun( slot: number, index: number ): string
---@class MasterLootCandidates
---@field get fun( slot: number ): ItemCandidate[]
---@field find fun( slot: number, player_name: string ): ItemCandidate?
---@field get_index fun( slot: number, player_name: string ): number?
---@field transform_to_winner fun( player: RollingPlayer, item: Item|MasterLootDistributableItem, roll_type: RollType, winning_roll: number?, rerolling: boolean? ): Winner
---@param api MasterLootCandidatesApi
---@param group_roster GroupRoster
---@param loot_list LootList
function M.new( api, group_roster, loot_list )
local function get( slot )
if not group_roster then return get_dummy_candidates() end
local result = {}
local players = group_roster.get_all_players_in_my_group()
for i = 1, 40 do
-- There's probably a better way of separating the APIs. For now I'm leaving it like this.
if m.vanilla then
---@diagnostic disable-next-line: missing-parameter
local name = api.GetMasterLootCandidate( i )
for _, p in ipairs( players ) do
if name == p.name then
table.insert( result, make_item_candidate( name, p.class, p.online ) )
end
end
else
local name = api.GetMasterLootCandidate( slot, i )
for _, p in ipairs( players ) do
if name == p.name then
table.insert( result, make_item_candidate( name, p.class, p.online ) )
end
end
end
end
return result
end
local function find( slot, player_name )
local candidates = get( slot )
return m.find_value_in_table( candidates, player_name, function( v ) return v.name end )
end
---@param player RollingPlayer
---@param item Item|MasterLootDistributableItem
---@param roll_type RollType
---@param winning_roll number?
---@param rerolling boolean?
---@return Winner
local function transform_to_winner( player, item, roll_type, winning_roll, rerolling )
local slot = loot_list.get_slot( item.id )
local candidate = slot and find( slot, player.name )
return make_winner( player.name, player.class, item, candidate and true or false, roll_type, winning_roll and winning_roll, rerolling )
end
local function get_index( slot, player_name )
for i = 1, 40 do
if m.vanilla then
---@diagnostic disable-next-line: missing-parameter
local name = api.GetMasterLootCandidate( i )
if name == player_name then return i end
else
local name = api.GetMasterLootCandidate( slot, i )
if name == player_name then return i end
end
end
end
---@type MasterLootCandidates
return {
get = get,
find = find,
get_index = get_index,
transform_to_winner = transform_to_winner
}
end
m.MasterLootCandidates = M
return M

95
src/MasterLootWarning.lua Normal file
View File

@ -0,0 +1,95 @@
RollFor = RollFor or {}
local m = RollFor
if m.MasterLootWarning then return end
local M = {}
local red = m.colors.red
local blue = m.colors.blue
local grey = m.colors.grey
local table_contains_value = m.table_contains_value
---@diagnostic disable-next-line: undefined-global
local UIParent = UIParent
local function create_frame( api )
local frame = api().CreateFrame( "FRAME", "RollForMasterLootWarning", UIParent )
frame:Hide()
local label = frame:CreateFontString( nil, "OVERLAY" )
label:SetFont( "FONTS\\FRIZQT__.TTF", 24, "OUTLINE" )
label:SetPoint( "CENTER", 0, 0 )
label:SetText( string.format( "No %s!", red( "Master Loot" ) ) )
local label2 = frame:CreateFontString( nil, "OVERLAY" )
label2:SetFont( "FONTS\\FRIZQT__.TTF", 16, "OUTLINE" )
label2:SetPoint( "CENTER", 0, -20 )
label2:SetText( string.format( "Enable %s or type %s to disable this message.", grey( "Master Loot" ), blue( "/rf ml" ) ) )
frame:SetWidth( label:GetWidth() )
frame:SetHeight( label:GetHeight() )
frame:SetPoint( "CENTER", UIParent, "CENTER", 0, 140 )
return frame
end
---@param api table
---@param config Config
---@param boss_list BossList
---@param player_info PlayerInfo
function M.new( api, config, boss_list, player_info )
local frame
local is_visible = false
local function show()
if not frame or is_visible or not config.show_ml_warning() then return end
api().UIFrameFadeRemoveFrame( frame )
frame:SetAlpha( 1 )
frame:Show()
is_visible = true
frame.fading_out = nil
end
local function hide()
if not frame or frame.fading_out or not frame:IsVisible() then return end
frame.fading_out = true
is_visible = false
api().UIFrameFadeOut( frame, 2, 1, 0 )
frame.fadeInfo.finishedFunc = function()
frame.fading_out = nil
frame:Hide()
end
end
local function toggle()
if not player_info.is_master_looter() and not player_info.is_leader() or config.auto_master_loot() then
if is_visible then hide() end
return
end
local master_loot = m.is_master_loot()
local zone_name = api().GetRealZoneText()
local target_name = api().UnitName( "target" )
local zone = boss_list[ zone_name ] or {}
local dead = api().UnitIsDead( "target" )
if not zone or master_loot or not table_contains_value( zone, target_name ) or dead then
if is_visible then hide() end
return
end
if not frame then frame = create_frame( api ) end
show()
end
return {
on_player_target_changed = toggle,
on_party_loot_method_changed = toggle,
hide = hide
}
end
m.MasterLootWarning = M
return M

316
src/MinimapButton.lua Normal file
View File

@ -0,0 +1,316 @@
RollFor = RollFor or {}
local m = RollFor
if m.MinimapButton then return end
local M = {}
local hl = m.colors.hl
local blue = m.colors.blue
local grey = m.colors.grey
local white = m.colors.white
local green = m.colors.green
local red = m.colors.red
local pretty_print = m.pretty_print
local class_color = m.colorize_player_by_class
local ColorType = {
White = "White",
Green = "Green",
Orange = "Orange",
Red = "Red"
}
function M.new( api, db, manage_softres_fn, winners_popup_fn, softres_check, config )
local icon_color
local function persist_angle( angle )
db.angle = angle
end
local function get_angle()
return db.angle
end
local function is_locked()
return config.minimap_button_locked()
end
local function is_hidden()
return config.minimap_button_hidden()
end
local function print_players_who_did_not_softres( tooltip )
local result, players = softres_check.check_softres( true )
if result == softres_check.ResultType.SomeoneIsNotSoftRessing then
tooltip:AddLine( white( "Missing softres:" ) )
for _, player in pairs( players ) do
tooltip:AddLine( class_color( player.name, player.class ) )
end
end
end
local function create()
local frame = api().CreateFrame( "Button", "RollForMinimapButton", api().Minimap )
local was_dragging = false
function frame.OnClick( self )
if m.vanilla then self = this end
if m.is_shift_key_down() then
winners_popup_fn()
else
manage_softres_fn()
end
self:OnEnter()
api().GameTooltip:Hide()
end
function frame.OnMouseDown( self )
if m.vanilla then self = this end
self.icon:SetTexCoord( 0, 1, 0, 1 )
was_dragging = false
end
function frame.OnMouseUp( self )
if m.vanilla then self = this end
self.icon:SetTexCoord( 0.05, 0.95, 0.05, 0.95 )
if m.vanilla and not was_dragging then self:OnClick() end
end
function frame.OnEnter( self )
if m.vanilla then self = this end
if not self.dragging then
api().GameTooltip:SetOwner( self, "ANCHOR_LEFT" )
api().GameTooltip:SetText( blue( "RollFor" ) )
api().GameTooltip:AddLine( " " )
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/htr" ), white( "show how to roll" ) ) )
api().GameTooltip:AddLine( string.format( "%s %s - %s", hl( "/rf" ), grey( "<item>" ), white( "roll for" ) ) )
api().GameTooltip:AddLine( string.format( "%s %s - %s", hl( "/rr" ), grey( "<item>" ), white( "raid-roll" ) ) )
api().GameTooltip:AddLine( string.format( "%s %s - %s", hl( "/irr" ), grey( "<item>" ), white( "insta raid-roll" ) ) )
api().GameTooltip:AddLine( string.format( "%s %s - %s", hl( "/arf" ), grey( "<item>" ), white( "roll for (ignore SR)" ) ) )
api().GameTooltip:AddLine( string.format( "%s %s %s - %s", hl( "/rf" ), grey( "<item>" ), grey( "<seconds>" ), white( "roll with custom time" ) ) )
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/sr" ), white( "manage softres" ) ) )
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/sro" ), white( "fix player softres name" ) ) )
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/src" ), white( "check softres status" ) ) )
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/srs" ), white( "show softres items" ) ) )
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/rfr" ), white( "reset loot announce" ) ) )
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/cr" ), white( "cancel rolling in progress" ) ) )
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/fr" ), white( "finish rolling early" ) ) )
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/rf config" ), white( "show configuration" ) ) )
api().GameTooltip:AddLine( string.format( "%s - %s", hl( "/rf config help" ), white( "show configuration help" ) ) )
api().GameTooltip:AddLine( " " )
api().GameTooltip:AddLine( "Click to manage softres." )
if icon_color == ColorType.Green then
api().GameTooltip:AddLine( " " )
api().GameTooltip:AddLine( string.format( "%s %s", white( "Softres status:" ), green( "OK" ) ) )
elseif icon_color == ColorType.Orange then
api().GameTooltip:AddLine( " " )
print_players_who_did_not_softres( api().GameTooltip )
elseif icon_color == ColorType.Red then
api().GameTooltip:AddLine( " " )
api().GameTooltip:AddLine( white( "Softres status:" ) )
api().GameTooltip:AddLine( red( "Found outdated softres data!" ) )
end
api().GameTooltip:Show()
end
end
function frame.OnLeave()
api().GameTooltip:Hide()
end
function frame.OnDragStart( self )
if m.vanilla then self = this end
self.dragging = true
self:LockHighlight()
self.icon:SetTexCoord( 0, 1, 0, 1 )
self:SetScript( "OnUpdate", self.OnUpdate )
api().GameTooltip:Hide()
was_dragging = true
end
function frame.OnDragStop( self )
if m.vanilla then self = this end
self.dragging = nil
self:SetScript( "OnUpdate", nil )
self.icon:SetTexCoord( 0.05, 0.95, 0.05, 0.95 )
self:UnlockHighlight()
end
function frame.OnUpdate( self )
if m.vanilla then self = this end
local mx, my = api().Minimap:GetCenter()
local px, py = api().GetCursorPosition()
local scale = api().Minimap:GetEffectiveScale()
px, py = px / scale, py / scale
persist_angle( m.mod( math.deg( math.atan2( py - my, px - mx ) ), 360 ) )
self:UpdatePosition()
end
-- Copy pasted from Bongos.
--magic fubar code for updating the minimap button"s position
--I suck at trig, so I"m not going to bother figuring it out
---@diagnostic disable-next-line: redefined-local
function frame.UpdatePosition( self )
if m.vanilla then self = this end
local angle = math.rad( get_angle() or m.lua.random( 0, 360 ) )
local cos = math.cos( angle )
local sin = math.sin( angle )
local minimapShape = api().GetMinimapShape and api().GetMinimapShape() or "ROUND"
local round = false
if minimapShape == "ROUND" then
round = true
elseif minimapShape == "SQUARE" then
round = false
elseif minimapShape == "CORNER-TOPRIGHT" then
round = not (cos < 0 or sin < 0)
elseif minimapShape == "CORNER-TOPLEFT" then
round = not (cos > 0 or sin < 0)
elseif minimapShape == "CORNER-BOTTOMRIGHT" then
round = not (cos < 0 or sin > 0)
elseif minimapShape == "CORNER-BOTTOMLEFT" then
round = not (cos > 0 or sin > 0)
elseif minimapShape == "SIDE-LEFT" then
round = cos <= 0
elseif minimapShape == "SIDE-RIGHT" then
round = cos >= 0
elseif minimapShape == "SIDE-TOP" then
round = sin <= 0
elseif minimapShape == "SIDE-BOTTOM" then
round = sin >= 0
elseif minimapShape == "TRICORNER-TOPRIGHT" then
round = not (cos < 0 and sin > 0)
elseif minimapShape == "TRICORNER-TOPLEFT" then
round = not (cos > 0 and sin > 0)
elseif minimapShape == "TRICORNER-BOTTOMRIGHT" then
round = not (cos < 0 and sin < 0)
elseif minimapShape == "TRICORNER-BOTTOMLEFT" then
round = not (cos > 0 and sin < 0)
end
local x, y
if round then
x = cos * 80
y = sin * 80
else
x = math.max( -82, math.min( 110 * cos, 84 ) )
y = math.max( -86, math.min( 110 * sin, 82 ) )
end
self:ClearAllPoints()
self:SetPoint( "CENTER", x, y )
end
frame:SetFrameStrata( "MEDIUM" )
frame:SetWidth( 31 )
frame:SetHeight( 31 )
frame:SetFrameLevel( 8 )
frame:RegisterForClicks( "anyUp" )
frame:RegisterForDrag( "LeftButton" )
frame:SetHighlightTexture( "Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight" )
local overlay = frame:CreateTexture( nil, "OVERLAY" )
overlay:SetWidth( 53 )
overlay:SetHeight( 53 )
overlay:SetTexture( "Interface\\Minimap\\MiniMap-TrackingBorder" )
overlay:SetPoint( "TOPLEFT", 0, 0 )
local icon = frame:CreateTexture( nil, "BACKGROUND" )
icon:SetWidth( 20 )
icon:SetHeight( 20 )
icon:SetTexCoord( 0.05, 0.95, 0.05, 0.95 )
icon:SetPoint( "TOPLEFT", 7, -5 )
frame.icon = icon
frame:SetScript( "OnEnter", frame.OnEnter )
frame:SetScript( "OnLeave", frame.OnLeave )
frame:SetScript( "OnClick", frame.OnClick )
frame:SetScript( "OnMouseDown", frame.OnMouseDown )
frame:SetScript( "OnMouseUp", frame.OnMouseUp )
frame:UpdatePosition()
frame:SetScript( "OnEvent", function() frame:UpdatePosition() end )
frame:RegisterEvent( "PLAYER_ENTERING_WORLD" )
return frame
end
local frame = create()
local function show()
if is_hidden() then
frame:Hide()
pretty_print( string.format( "Minimap button is hidden. Type %s to show.", hl( "/rf config minimap" ) ) )
else
frame:Show()
end
end
local function lock()
if is_locked() then
frame:SetScript( "OnDragStart", nil )
frame:SetScript( "OnDragStop", nil )
else
frame:SetScript( "OnDragStart", frame.OnDragStart )
frame:SetScript( "OnDragStop", frame.OnDragStop )
end
end
local function set_icon( color )
frame.icon:SetTexture( string.format( "Interface\\AddOns\\RollFor\\assets\\icon-%s.tga", string.lower( color ) ) )
icon_color = color
end
show()
lock()
set_icon( ColorType.Red )
local function toggle()
if is_hidden() then
config.show_minimap_button()
else
config.hide_minimap_button()
end
show()
end
local function toggle_lock()
if is_locked() then
config.unlock_minimap_button()
else
config.lock_minimap_button()
end
lock()
end
config.subscribe( "minimap_button_hidden", show )
return {
toggle = toggle,
toggle_lock = toggle_lock,
set_icon = set_icon,
ColorType = ColorType
}
end
m.MinimapButton = M
return M

330
src/ModernLootFrameSkin.lua Normal file
View File

@ -0,0 +1,330 @@
RollFor = RollFor or {}
local m = RollFor
if m.ModernLootFrameSkin then return end
local M = {}
local gui = m.GuiElements
local item_height = 22
local footer_height = 0
---@param frame_builder FrameBuilderFactory
function M.new( frame_builder )
---@param parent Frame
local function dropped_item( parent )
local container = m.create_loot_button( m.api, parent )
local w = 22
local h = 22
local spacing = 6
local bind_spacing = 3
local mouse_down = false
local icon_zoom = 2
local item
container:SetHeight( h )
container.name = gui.create_text_in_container( "Frame", container, 20, "LEFT", nil, "text" )
container.name.text:SetJustifyH( "LEFT" )
container.name.text:SetTextColor( 1, 1, 1 )
container.index = gui.create_text_in_container( "Frame", container, 20, "CENTER", nil, "text" )
container.index:SetPoint( "LEFT", 1, 0 )
container.index:SetWidth( 16 )
container.index:SetHeight( h )
container.icon = gui.create_icon_in_container( "Button", container, w, h, icon_zoom )
container.icon:SetPoint( "LEFT", container.index, "RIGHT", 2, 0 )
container.icon:EnableMouse( false )
container.quantity = gui.create_text_in_container( "Frame", container.icon, 20, "CENTER", nil, "text", "NumberFontNormalSmall" )
container.quantity:SetPoint( "BOTTOMRIGHT", 1, -2 )
container.quantity:SetHeight( 16 )
container.bind = gui.create_text_in_container( "Frame", container, 15, "LEFT", nil, "text" )
container.bind:SetPoint( "LEFT", container.icon, "RIGHT", 5, 0 )
container.comment = gui.create_text_in_container( "Button", container, 20, "CENTER", nil, "text" )
container.comment:SetPoint( "RIGHT", -4, 0 )
container.comment:SetHeight( 16 )
local function resize()
container.icon:Show()
local index_width = container.index:GetWidth() + 1
local icon_width = container.icon:GetWidth() + spacing
local bind_width = item.bind and (container.bind:GetWidth() + bind_spacing) or 0
local text_width = container.name.text:GetStringWidth() + spacing + 1
local comment_width = container.comment:IsVisible() and container.comment:GetWidth() + spacing or 0
local total_width = index_width + icon_width + bind_width + text_width + comment_width
container:SetWidth( total_width )
container:SetPoint( "LEFT", 0, 0 )
container:SetPoint( "RIGHT", 0, 0 )
end
local function get_color( multiplier )
local mult = multiplier or 1
local color = m.api.ITEM_QUALITY_COLORS[ item.quality or 0 ]
return color.r * mult, color.g * mult, color.b * mult
end
local function hovered_color()
if not item then return end
if item.is_selected then return end
local r, g, b = get_color()
container:SetBackdropColor( r, g, b, 0.3 )
end
local function clicked_color()
local r, g, b = get_color()
container:SetBackdropColor( r, g, b, 0.4 )
end
local function selected_color()
if not item then return end
local r, g, b = get_color()
container:SetBackdropColor( r, g, b, 0.3 )
end
local function not_hovered_color()
if not item or item.is_selected then return end
container:SetBackdropColor( 0, 0, 0, 0.1 )
end
local function update()
if not item then return end
if not item.is_enabled then
container:SetAlpha( 0.6 )
return
end
if item.is_selected then
selected_color()
else
not_hovered_color()
end
container:SetAlpha( 1 )
end
---@param v LootFrameItem
container.SetItem = function( _, v )
item = v
container.index.text:SetText( v.index )
container.icon.texture:SetTexture( v.texture )
container.name.text:SetText( m.colorize_item_by_quality( v.name, v.quality ) )
if v.bind then
container.bind.text:SetText( v.bind )
container.bind:SetWidth( container.bind.text:GetStringWidth() )
container.bind:Show()
container.name:SetPoint( "LEFT", container.bind, "RIGHT", bind_spacing, 0 )
else
container.bind:Hide()
container.name:SetPoint( "LEFT", container.icon, "RIGHT", spacing, 0 )
end
if v.comment then
container.comment.text:SetText( v.comment )
container.comment:Show()
container.name:SetPoint( "RIGHT", container.comment, "LEFT", 0, 0 )
else
container.comment:Hide()
container.name:SetPoint( "RIGHT", container, "RIGHT", 0, 0 )
end
if v.quantity and v.quantity > 1 then
container.quantity:Show()
container.quantity.text:SetText( v.quantity )
container.quantity:SetWidth( container.quantity.text:GetStringWidth() )
else
container.quantity:Hide()
end
local function modifier_fn()
if m.is_ctrl_key_down() then
m.api.DressUpItemLink( v.link )
return
end
if m.is_shift_key_down() then
m.link_item_in_chat( v.link )
return
end
end
container:SetScript( "OnClick", v.is_enabled and not v.is_selected and v.click_fn or modifier_fn )
container.comment:SetScript( "OnClick", v.is_enabled and not v.is_selected and v.click_fn or modifier_fn )
if m.vanilla then
-- Fucking hell this took forever to figure out. Fuck you Blizzard.
-- For looting to work in vanilla, the frame must be of a "LootButton" type and
-- then it comes with the SetSlot function that we need to use to set the slot.
-- This will probably be a pain in the ass when porting.
container:SetSlot( v.slot or 0 )
end
update()
resize()
end
local function on_enter( self )
if m.vanilla then self = this end
if not item then return end
if item.tooltip_link then
m.api.GameTooltip:SetOwner( self, "ANCHOR_RIGHT" )
m.api.GameTooltip:SetHyperlink( item.tooltip_link )
m.api.GameTooltip:Show()
end
if not item.is_enabled then return end
hovered_color()
end
container:SetBackdrop( {
bgFile = "Interface/Buttons/WHITE8x8",
tile = false,
tileSize = 0,
} )
not_hovered_color()
local function on_leave()
m.api.GameTooltip:Hide()
mouse_down = false
not_hovered_color()
end
container.comment:SetScript( "OnEnter", function( self )
if not item then return end
if item.comment_tooltip then
if m.vanilla then self = this end
self.tooltip_scale = m.api.GameTooltip:GetScale()
m.api.GameTooltip:SetOwner( self, "ANCHOR_RIGHT" )
local result = ""
for _, line in ipairs( item.comment_tooltip ) do
if result ~= "" then result = result .. "\n" end
result = result .. line
end
m.api.GameTooltip:AddLine( result, 1, 1, 1 )
m.api.GameTooltip:SetScale( 0.9 )
m.api.GameTooltip:Show()
end
if not item.is_enabled then return end
hovered_color()
end )
container.comment:SetScript( "OnLeave", function( self )
if m.vanilla then self = this end
m.api.GameTooltip:Hide()
m.api.GameTooltip:SetScale( self.tooltip_scale or 1 )
mouse_down = false
not_hovered_color()
end )
container:SetScript( "OnEnter", on_enter )
container:SetScript( "OnLeave", on_leave )
local function on_mouse_down()
if not item then return end
if not item.is_enabled or item.is_selected then return end
mouse_down = true
clicked_color()
end
local function on_mouse_up()
if not item then return end
if not item.is_enabled or item.is_selected then return end
if not mouse_down then return end
hovered_color()
end
container:SetScript( "OnMouseUp", on_mouse_up )
container:SetScript( "OnMouseDown", on_mouse_down )
container:SetScript( "OnShow", function()
mouse_down = false
end )
return container
end
---@param on_drag_stop function
---@param on_show function
---@param on_hide function
local function header( on_drag_stop, on_show, on_hide )
return frame_builder.new()
:name( "RollForLootFrameHeader" )
:parent( m.api.UIParent )
:width( 380 )
:height( 24 )
:sound()
:gui_elements( gui )
:frame_style( "Modern" )
:backdrop_color( 0, 0.501, 1, 0.3 )
:border_color( 0, 0, 0, 0.9 )
:movable()
:gui_elements( m.GuiElements )
:bg_file( "Interface/Buttons/WHITE8x8" )
:on_show( on_show )
:on_hide( on_hide )
:on_drag_stop( on_drag_stop )
:hidden()
:build()
end
---@param parent Frame
local function body( parent )
local frame = frame_builder.new()
:name( "RollForLootFrame" )
:parent( parent )
:width( 280 )
:height( 100 )
:gui_elements( { dropped_item = dropped_item } )
:frame_style( "Modern" )
:backdrop_color( 0, 0, 0, 0.5 )
:border_color( 0, 0, 0, 0.9 )
:movable()
:bg_file( "Interface/Buttons/WHITE8x8" )
:build()
frame:ClearAllPoints()
frame:SetPoint( "TOP", parent, "BOTTOM", 0, 0 )
return frame
end
local function footer()
end
local function get_item_height()
return item_height
end
local function get_footer_height()
return footer_height
end
---@type LootFrameSkin
return {
header = header,
body = body,
dropped_item = dropped_item,
footer = footer,
get_item_height = get_item_height,
get_footer_height = get_footer_height
}
end
m.ModernLootFrameSkin = M
return M

22
src/Module.lua Normal file
View File

@ -0,0 +1,22 @@
RollFor = RollFor or {}
local m = RollFor
if m.Module then return end
---@class RollForModule
---@field debug DebugBuffer
local M = {}
---@return RollForModule
function M.new( module_name, debug_size )
---@type DebugBuffer
local debug_buffer = m.DebugBuffer.new( module_name, debug_size or 20 )
return {
debug = debug_buffer
}
end
m.Module = M
return M

262
src/NameAutoMatcher.lua Normal file
View File

@ -0,0 +1,262 @@
RollFor = RollFor or {}
local rf = RollFor
if rf.NameAutoMatcher then return end
local M = {}
local getn = rf.getn
local count = rf.count_elements
local map = rf.map
local function to_map( t )
local result = {}
for _, v in pairs( t ) do
result[ v ] = 1
end
return result
end
-- Returns the values that are in the left table, but not in the right table.
local function is_in_left_but_not_in_right( left, right )
local softres_player_map = to_map( right )
local result = {}
for _, player_name in pairs( left ) do
if not softres_player_map[ player_name ] then
table.insert( result, player_name )
end
end
return result
end
local function string_similarity( s1, s2 )
local n = string.len( s1 )
local m = string.len( s2 )
local ssnc = 0
if n > m then
s1, s2 = s2, s1
n, m = m, n
end
for i = n, 1, -1 do
if i <= string.len( s1 ) then
for j = 1, n - i + 1, 1 do
local pattern = string.sub( s1, j, j + i - 1 )
if string.len( pattern ) == 0 then break end
local foundAt = string.find( s2, pattern )
if foundAt ~= nil then
ssnc = ssnc + (2 * i) ^ 2
s1 = string.sub( s1, 0, j - 1 ) .. string.sub( s1, j + i )
s2 = string.sub( s2, 0, foundAt - 1 ) .. string.sub( s2, foundAt + i )
break
end
end
end
end
return (ssnc / ((n + m) ^ 2)) ^ (1 / 2)
end
local function get_levenshtein( s1, s2 )
local len1 = string.len( s1 )
local len2 = string.len( s2 )
local matrix = {}
local cost = 1
local min = math.min;
-- quick cut-offs to save time
if (len1 == 0) then
return len2
elseif (len2 == 0) then
return len1
elseif (s1 == s2) then
return 0
end
-- initialise the base matrix values
for i = 0, len1, 1 do
matrix[ i ] = {}
matrix[ i ][ 0 ] = i
end
for j = 0, len2, 1 do
matrix[ 0 ][ j ] = j
end
-- actual Levenshtein algorithm
for i = 1, len1, 1 do
for j = 1, len2, 1 do
if (string.byte( s1, i ) == string.byte( s2, j )) then
cost = 0
end
matrix[ i ][ j ] = min( matrix[ i - 1 ][ j ] + 1, matrix[ i ][ j - 1 ] + 1, matrix[ i - 1 ][ j - 1 ] + cost )
end
end
-- return the last value - this is the Levenshtein distance
return matrix[ len1 ][ len2 ]
end
local function get_similarity_predictions( present_players_who_did_not_softres, absent_players_who_did_softres, sort )
local result = {}
for _, player in pairs( present_players_who_did_not_softres ) do
local predictions = {}
for _, candidate in pairs( absent_players_who_did_softres ) do
local prediction = {
[ "candidate" ] = candidate,
[ "similarity" ] = string_similarity( player, candidate ),
[ "levenshtein" ] = get_levenshtein( player, candidate )
}
table.insert( predictions, prediction )
end
table.sort( predictions, sort )
result[ player ] = predictions
end
return result
end
local function improved_descending( l, r )
return l[ "levenshtein" ] < r[ "levenshtein" ] or
l[ "levenshtein" ] == r[ "levenshtein" ] and l[ "similarity" ] > r[ "similarity" ]
end
---@diagnostic disable-next-line: unused-function
local function ends_with( str, ending )
return ending == "" or string.sub( str, -string.len( ending ) ) == ending
end
---@diagnostic disable-next-line: unused-function, unused-local
local function format_percent( value )
local result = string.format( "%.2f", value * 100 )
if ends_with( result, "0" ) then
result = string.sub( result, 0, string.len( result ) - 1 )
end
if ends_with( result, "0" ) then
result = string.sub( result, 0, string.len( result ) - 1 )
end
if ends_with( result, "." ) then
result = string.sub( result, 0, string.len( result ) - 1 )
end
return string.format( "%s%%", result )
end
local function assign_predictions( predictions, top_threshold, bottom_threshold )
local function format_4( value ) return string.format( "%.4f", value ) end
local results = {}
local results_below_threshold = {}
for player, prediction in pairs( predictions ) do
local top_candidate = prediction[ 1 ]
local similarity = top_candidate[ "similarity" ]
local levenshtein = top_candidate[ "levenshtein" ]
local match = {
[ "matched_name" ] = top_candidate[ "candidate" ],
[ "similarity" ] = format_4( similarity ),
[ "levenshtein" ] = levenshtein
}
if similarity >= (top_threshold or 0.57) then
results[ player ] = match
elseif similarity >= (bottom_threshold or 0.4) then
results_below_threshold[ player ] = match
end
end
return results, results_below_threshold
end
function M.new( group_roster, softres, top_threshold, bottom_threshold )
local matched_names = {}
local matched_names_below_threshold = {}
local function auto_match()
matched_names = {}
matched_names_below_threshold = {}
---@param p Player | Roller
local function get_name( p ) return p.name end
local player_names = map( group_roster.get_all_players_in_my_group(), get_name )
local roller_names = map( softres.get_all_rollers(), get_name )
local present_players_who_did_not_softres = is_in_left_but_not_in_right( player_names, roller_names )
if getn( present_players_who_did_not_softres ) == 0 then return end
local absent_players_who_did_softres = is_in_left_but_not_in_right( roller_names, player_names )
if getn( absent_players_who_did_softres ) == 0 then return end
local predictions = get_similarity_predictions( present_players_who_did_not_softres, absent_players_who_did_softres, improved_descending )
local matched, matched_below_threshold = assign_predictions( predictions, top_threshold, bottom_threshold )
for player, match_result in pairs( matched ) do
local matched_name = match_result[ "matched_name" ]
local similarity = match_result[ "similarity" ]
matched_names[ matched_name ] = { [ "matched_name" ] = player, [ "similarity" ] = similarity }
end
for player, match_result in pairs( matched_below_threshold ) do
local matched_name = match_result[ "matched_name" ]
local similarity = match_result[ "similarity" ]
matched_names_below_threshold[ matched_name ] = { [ "matched_name" ] = player, [ "similarity" ] = similarity }
end
end
local function get_softres_name( matched_name )
for softres_name, match in pairs( matched_names ) do
if match.matched_name == matched_name then return softres_name end
end
return nil
end
local function get_matched_name( softres_name )
return matched_names[ softres_name ] and matched_names[ softres_name ].matched_name or nil
end
local function get_matches()
if count( matched_names ) == 0 and count( matched_names_below_threshold ) == 0 then return {}, {} end
local matches = {}
local not_matches = {}
for softres_name, match in pairs( matched_names ) do
table.insert( matches, { softres_name = softres_name, matched_name = match.matched_name, similarity = match.similarity } )
end
for softres_name, match in pairs( matched_names_below_threshold ) do
table.insert( not_matches, { softres_name = softres_name, matched_name = match.matched_name, similarity = match.similarity } )
end
return matches, not_matches
end
local function is_matched( softres_name )
return get_matched_name( softres_name ) or false
end
return {
auto_match = auto_match,
get_softres_name = get_softres_name,
get_matched_name = get_matched_name,
is_matched = is_matched,
get_matches = get_matches
}
end
rf.NameAutoMatcher = M
return M

160
src/NameManualMatcher.lua Normal file
View File

@ -0,0 +1,160 @@
RollFor = RollFor or {}
local m = RollFor
if m.NameManualMatcher then return end
local M = {}
local getn = m.getn
local clone = m.clone
local negate = m.negate
local filter = m.filter
local keys = m.keys
local merge = m.merge
local colors = m.colors
local p = m.pretty_print
local map = m.map
function M.new( db, api, absent_unfiltered_softres, name_matcher, softres_status_changed )
db.manual_matches = db.manual_matches or {}
local manual_match_options = nil
local function show_manual_matches( matches, absent_players )
if getn( matches ) == 0 and getn( absent_players ) == 0 then
p( "There are no players that can be manually matched." )
return
end
local index = 1
if getn( matches ) > 0 then
p( string.format( "To unmatch, clear your target and type: %s", colors.hl( "/sro <number>" ) ) )
for i = 1, getn( matches ) do
p( string.format( "[%s]: %s (manually matched with %s)", colors.green( index ), colors.hl( matches[ i ] ), colors.hl( db.manual_matches[ matches[ i ] ] ) ) )
index = index + 1
end
end
if getn( absent_players ) > 0 then
p( string.format( "To match, target a player and type: %s", colors.hl( "/sro <number>" ) ) )
for i = 1, getn( absent_players ) do
p( string.format( "[%s]: %s", colors.green( index ), colors.red( absent_players[ i ] ) ) )
index = index + 1
end
end
end
local parse_number = function( args )
for i in string.gmatch( args, "(%d+)" ) do
return tonumber( i )
end
return nil
end
local function is_matched( player )
return db.manual_matches[ player.name ] or name_matcher.is_matched( player.name )
end
local function create_matches_and_show()
local absent_players = map( filter( absent_unfiltered_softres.get_all_rollers(), negate( is_matched ) ), function( v ) return v.name end )
local manually_matched = keys( db.manual_matches )
manual_match_options = merge( {}, manually_matched, absent_players )
show_manual_matches( manually_matched, absent_players )
end
local function manual_match( args )
if not manual_match_options or not args or args == "" then
create_matches_and_show()
return
end
local count = getn( manual_match_options )
local target = api().UnitName( "target" )
local index = parse_number( args )
if not index or index < 0 or index > count then
p( "Invalid player number." )
create_matches_and_show()
return
end
local softres_name = manual_match_options[ index ]
local already_matched_name = db.manual_matches[ softres_name ]
if target and already_matched_name then
p( string.format( "%s is already matched to %s.", colors.hl( softres_name ), colors.hl( already_matched_name ) ) )
create_matches_and_show()
elseif target and not already_matched_name then
manual_match_options = nil
db.manual_matches[ softres_name ] = target
p( string.format( "%s is now soft-ressing as %s.", colors.hl( target ), colors.hl( softres_name ) ) )
softres_status_changed()
elseif not target and already_matched_name then
manual_match_options = nil
db.manual_matches[ softres_name ] = nil
p( string.format( "Unmatched %s.", colors.hl( softres_name ) ) )
softres_status_changed()
else
p( string.format( "To match a player, target them first." ) )
create_matches_and_show()
end
end
local function get_matched_name( softres_name )
return db.manual_matches[ softres_name ] or name_matcher.get_matched_name( softres_name )
end
local function get_softres_name( matched_name )
for softres_name, name in pairs( db.manual_matches ) do
if name == matched_name then return softres_name end
end
return name_matcher.get_softres_name( matched_name )
end
local function clear( report )
if not db.manual_matches or m.count_elements( db.manual_matches ) == 0 then return end
m.clear_table( db.manual_matches )
if report then p( "Cleared manual matches." ) end
end
local function remove_duplicates( source, duplicates )
local result = {}
for _, v in pairs( source ) do
if not m.find( v.softres_name, duplicates, "softres_name" ) then
table.insert( result, v )
end
end
return result
end
local function get_matches()
local matches = {}
for softres_name, match in pairs( db.manual_matches ) do
table.insert( matches, { softres_name = softres_name, matched_name = match } )
end
local auto_matches, auto_not_matches = name_matcher.get_matches()
return remove_duplicates( auto_matches, matches ), remove_duplicates( remove_duplicates( auto_not_matches, matches ), auto_matches ), matches
end
local decorator = clone( name_matcher )
decorator.manual_match = manual_match
decorator.is_matched = is_matched
decorator.get_matched_name = get_matched_name
decorator.get_softres_name = get_softres_name
decorator.clear = clear
decorator.get_matches = get_matches
return decorator
end
m.NameManualMatcher = M
return M

32
src/NameMatchReport.lua Normal file
View File

@ -0,0 +1,32 @@
RollFor = RollFor or {}
local m = RollFor
if m.SoftResCheck then return end
local M = {}
local hl = m.colors.hl
local grey = m.colors.grey
local red = m.colors.red
local p = m.pretty_print
function M.report( name_matcher )
local auto_matched, auto_not_matched, manually_matched = name_matcher.get_matches()
if manually_matched then
for _, match in pairs( manually_matched ) do
p( string.format( "%s is manually matched with %s.", hl( match.softres_name ), hl( match.matched_name ) ), grey )
end
end
for _, match in pairs( auto_matched ) do
p( string.format( "%s is auto-matched with %s.", hl( match.softres_name ), hl( match.matched_name ) ), grey )
end
for _, match in pairs( auto_not_matched ) do
p( string.format( "%s could not be auto-matched. Top candidate: %s.", hl( match.softres_name ), hl( match.matched_name ) ), red )
end
end
m.NameMatchReport = M
return M

48
src/NewGroupEvent.lua Normal file
View File

@ -0,0 +1,48 @@
RollFor = RollFor or {}
local m = RollFor
if m.NewGroupEvent then return end
local M = {}
---@class NewGroupEvent
---@field on_group_changed fun()
---@field subscribe fun( callback: fun() )
---@param group_roster GroupRoster
function M.new( group_roster )
local m_subscribers = {}
local group = group_roster.am_i_in_group()
local function notify_subscribers()
for _, subscriber in ipairs( m_subscribers ) do
subscriber()
end
end
local function on_group_changed()
local in_group_now = group_roster.am_i_in_group()
if not group and in_group_now then
group = true
notify_subscribers()
return
end
if group and not in_group_now then
group = false
end
end
local function subscribe( callback )
table.insert( m_subscribers, callback )
end
return {
on_group_changed = on_group_changed,
subscribe = subscribe
}
end
m.NewGroupEvent = M
return M

View File

@ -0,0 +1,288 @@
RollFor = RollFor or {}
local m = RollFor
if m.NonSoftResRollingLogic then return end
local M = m.Module.new( "NonSoftResRollingLogic" )
local getn = m.getn
local count_elements = m.count_elements
local merge = m.merge
local take = m.take
local rlu = m.RollingLogicUtils
local RollType = m.Types.RollType
local hl = m.colors.hl
---@type MakeRollFn
local make_roll = m.Types.make_roll
---@param players RollingPlayer[]
local function have_all_players_rolled( players )
for _, v in ipairs( players ) do
if v.rolls > 0 then return false end
end
return true
end
---@param chat Chat
---@param ace_timer AceTimer
---@param players RollingPlayer[]
---@param item Item
---@param item_count number
---@param info string?
---@param seconds number
---@param on_rolling_finished RollingFinishedCallback
---@param config Config
---@param controller RollControllerFacade
function M.new(
chat,
ace_timer,
players,
item,
item_count,
info,
seconds,
on_rolling_finished,
config,
controller
)
---@type RollingPlayer[], Roll[]
local mainspec_rollers, mainspec_rolls = players, {}
---@type RollingPlayer[], Roll[]
local offspec_rollers, offspec_rolls = rlu.copy_rollers( mainspec_rollers ), {}
---@type RollingPlayer[], Roll[]
local tmog_rollers, tmog_rolls = rlu.copy_rollers( mainspec_rollers ), {}
local rolling = false
local seconds_left = seconds
local timer
local ms_threshold = config.ms_roll_threshold()
local os_threshold = config.os_roll_threshold()
local tmog_threshold = config.tmog_roll_threshold()
local tmog_rolling_enabled = config.tmog_rolling_enabled()
tmog_rolling_enabled = tmog_rolling_enabled and (item.is_boss_loot or not config.auto_tmog_disable() )
local function sort_rolls()
local f = function( a, b )
if a.roll == b.roll then
return a.player.name < b.player.name
else
return a.roll > b.roll
end
end
table.sort( mainspec_rolls, f )
table.sort( offspec_rolls, f )
table.sort( tmog_rolls, f )
end
local function have_all_rolls_been_exhausted()
local mainspec_roll_count = getn( mainspec_rolls )
local offspec_roll_count = getn( offspec_rolls )
local tmog_roll_count = getn( tmog_rolls )
local total_roll_count = mainspec_roll_count + offspec_roll_count + tmog_roll_count
if item_count == getn( tmog_rollers ) and have_all_players_rolled( tmog_rollers ) or
item_count == getn( offspec_rollers ) and have_all_players_rolled( offspec_rollers ) or
item_count == getn( mainspec_rollers ) and total_roll_count == getn( mainspec_rollers ) then
return true
end
return have_all_players_rolled( mainspec_rollers )
end
---@param player_name string
---@param rollers RollingPlayer[]
local function find_player( player_name, rollers )
for _, player in ipairs( rollers ) do
if player.name == player_name then return player end
end
end
local function stop_listening()
rolling = false
if timer then
ace_timer:CancelTimer( timer )
timer = nil
end
end
local function find_winner()
stop_listening()
local mainspec_roll_count = count_elements( mainspec_rolls )
local offspec_roll_count = count_elements( offspec_rolls )
local tmog_roll_count = count_elements( tmog_rolls )
if mainspec_roll_count == 0 and offspec_roll_count == 0 and tmog_roll_count == 0 then
on_rolling_finished( item, item_count, {} )
return
end
sort_rolls()
---@type Roll[]
local all_rolls = merge( {}, mainspec_rolls, offspec_rolls, tmog_rolls )
local roll_count = getn( all_rolls )
local function count_top_roll_winners()
if roll_count == 0 then return 0 end
local function split_by_roll_and_type()
local result = {}
local last_roll
local last_type
for _, roll in ipairs( all_rolls ) do
if not last_roll or last_roll ~= roll.roll or last_type ~= roll.roll_type then
table.insert( result, { roll } )
last_roll = roll.roll
last_type = roll.roll_type
else
table.insert( result[ getn( result ) ], roll )
end
end
return result
end
local result = 0
for _, rolls in ipairs( split_by_roll_and_type() ) do
result = result + getn( rolls )
if result >= item_count then return result end
end
return result
end
local top_roll_winner_count = count_top_roll_winners()
local winner_rolls = take( all_rolls, top_roll_winner_count > item_count and top_roll_winner_count or item_count )
on_rolling_finished( item, item_count, winner_rolls )
end
---@param roller Player
---@param roll number
---@param min number
---@param max number
local function on_roll( roller, roll, min, max )
if not rolling or min ~= 1 or (max ~= tmog_threshold and max ~= os_threshold and max ~= ms_threshold) then return end
if max == tmog_threshold and not tmog_rolling_enabled then return end
local ms_roll = max == ms_threshold
local os_roll = max == os_threshold
local roll_type = ms_roll and RollType.MainSpec or os_roll and RollType.OffSpec or RollType.Transmog
local rollers = ms_roll and mainspec_rollers or os_roll and offspec_rollers or tmog_rollers
local player = find_player( roller.name, rollers ) ---@type RollingPlayer
if player.rolls == 0 then
chat.info( m.msg.rolls_exhausted( player.name, player.class, roll ) )
controller.roll_was_ignored( roller.name, player.class, roll_type, roll, "Rolled too many times." )
return
end
player.rolls = player.rolls - 1
local t = ms_roll and mainspec_rolls or os_roll and offspec_rolls or tmog_rolls
table.insert( t, make_roll( player, roll_type, roll ) )
controller.roll_was_accepted( player.name, player.class, roll_type, roll )
if have_all_rolls_been_exhausted() then find_winner() end
end
local function stop_accepting_rolls()
find_winner()
end
local function on_timer()
seconds_left = seconds_left - 1
if seconds_left <= 0 then
stop_accepting_rolls()
return
end
controller.tick( seconds_left )
end
local function accept_rolls()
rolling = true
timer = ace_timer.ScheduleRepeatingTimer( M, on_timer, 1.7 )
end
local function start_rolling()
local count_str = item_count > 1 and string.format( "%sx", item_count ) or ""
local tmog_info = tmog_rolling_enabled and string.format( " or /roll %s (TMOG)", config.tmog_roll_threshold() ) or ""
local default_ms = config.ms_roll_threshold() ~= 100 and string.format( "%s ", config.ms_roll_threshold() ) or ""
local roll_info = string.format( " /roll %s(MS) or /roll %s (OS)%s", default_ms, config.os_roll_threshold(), tmog_info )
local info_str = info and info ~= "" and string.format( " %s", info ) or roll_info
local x_rolls_win = item_count > 1 and string.format( ". %d top rolls win.", item_count ) or ""
if item.classes and config.auto_class_announce() then
local class_str = table.concat( item.classes, "s, " )
class_str = string.gsub( class_str, ", (%S+)$", " and %1" )
info_str = string.format( " %ss", class_str )
end
chat.announce( string.format( "Roll for %s%s:%s%s", count_str, item.link, info_str, x_rolls_win ), true )
accept_rolls()
end
local function show_sorted_rolls( limit )
local function show( prefix, sorted_rolls )
if getn( sorted_rolls ) == 0 then return end
chat.info( string.format( "%s rolls:", prefix ) )
local i = 0
for _, v in ipairs( sorted_rolls ) do
if limit and limit > 0 and i > limit then return end
chat.info( string.format( "[%s]: %s", hl( v.roll ), v.player.name ) )
i = i + 1
end
end
local total_mainspec_rolls = count_elements( mainspec_rolls )
local total_offspec_rolls = count_elements( offspec_rolls )
if total_mainspec_rolls + total_offspec_rolls == 0 then
chat.info( "No rolls found." )
return
end
sort_rolls()
show( "Mainspec", mainspec_rolls )
show( "Offspec", offspec_rolls )
show( "Transmog", tmog_rolls )
end
local function print_rolling_complete( canceled )
chat.info( string.format( "Rolling for %s %s.", item.link, canceled and "was canceled" or "finished" ) )
end
local function cancel_rolling()
stop_listening()
print_rolling_complete( true )
chat.announce( string.format( "Rolling for %s was canceled.", item.link ) )
end
local function is_rolling()
return rolling
end
---@type RollingStrategy
return {
start_rolling = start_rolling,
on_roll = on_roll,
show_sorted_rolls = show_sorted_rolls,
stop_accepting_rolls = stop_accepting_rolls,
cancel_rolling = cancel_rolling,
is_rolling = is_rolling,
get_type = function() return m.Types.RollingStrategy.NormalRoll end
}
end
m.NonSoftResRollingLogic = M
return M

465
src/OgLootFrameSkin.lua Normal file
View File

@ -0,0 +1,465 @@
RollFor = RollFor or {}
local m = RollFor
if m.OgLootFrameSkin then return end
local M = {}
local gui = m.GuiElements
local texture_size = 512
local right_side_width = 32
local item_height = 41
local header_height = 73
local footer_height = 11
local min_width = 183
local texture_dimensions = {
total = { width = texture_size, height = texture_size },
topleft = { width = texture_size - right_side_width, height = 73 },
topright = { width = right_side_width, height = 73 },
middleleft = { width = texture_size - right_side_width, height = item_height },
middleright = { width = right_side_width, height = item_height },
bottomleft = { width = texture_size - right_side_width, height = 11 },
bottomright = { width = right_side_width, height = 11 }
}
local td = texture_dimensions
---@param frame_builder FrameBuilderFactory
function M.new( frame_builder )
---@param og_set_width function
---@param update function?
local function set_width( og_set_width, update )
return function( self, width )
local w = width < min_width and min_width or min_width
og_set_width( self, w )
if update then update( w ) end
end
end
---@param parent Frame
local function create_close_button( parent )
local button = frame_builder.button():parent( parent ):width( 32 ):height( 32 ):build()
button:SetNormalTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Up" )
button:SetPushedTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Down" )
button:Show()
local highlight_texture = button:CreateTexture( nil, "HIGHLIGHT" )
highlight_texture:SetTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Highlight" )
highlight_texture:SetBlendMode( "ADD" )
highlight_texture:SetAllPoints( button )
---@diagnostic disable-next-line: undefined-field
button:SetScript( "OnClick", function()
parent:Hide()
m.api.CloseLoot()
end )
return button
end
---@param parent Frame
local function texture( parent )
local result = parent:CreateTexture( nil, "BACKGROUND" )
result:SetTexture( "Interface\\AddOns\\RollFor\\assets\\og-loot-frame.tga" )
return result
end
---@param parent Frame
local function create_portrait( parent )
local overlay = parent:CreateTexture( nil, "OVERLAY" )
overlay:SetTexture( "Interface\\TargetingFrame\\TargetDead" )
overlay:SetWidth( 55 )
overlay:SetHeight( 56 )
overlay:SetPoint( "TOPLEFT", parent, "TOPLEFT", 9, -5 )
end
---@param parent Frame
local function create_title( parent )
local font_string = parent:CreateFontString( nil, "ARTWORK", "GameFontNormal" )
font_string:SetText( "Items" )
font_string:SetJustifyH( "CENTER" )
font_string:SetWidth( 90 )
font_string:SetHeight( 30 )
font_string:SetPoint( "TOP", parent, "TOP", 20, -6 )
end
---@param parent Frame
local function dropped_item( parent )
local container = m.create_backdrop_frame( m.api, "Frame", nil, parent )
local w = 38
local h = 38
local spacing = 6
local mouse_down = false
local icon_zoom = 0
local item
container:SetHeight( item_height )
container.name = gui.create_text_in_container( "Button", container, 20, "LEFT", nil, "text", "GameFontNormal" )
container.name.text:SetJustifyH( "LEFT" )
container.name.text:SetTextColor( 1, 1, 1 )
container.name.text:SetWidth( 86 )
container.name:SetHeight( h )
container.name:SetWidth( 102 )
local name_texture = container.name:CreateTexture( nil, "BACKGROUND" )
name_texture:SetTexture( "Interface\\QuestFrame\\UI-QuestItemNameFrame" )
name_texture:SetWidth( 133 )
name_texture:SetHeight( 62 )
name_texture:SetPoint( "LEFT", -15, 0 )
container.icon = gui.create_icon_in_container( m.vanilla and "LootButton" or "Button", container, w, h, icon_zoom )
container.icon:SetPoint( "LEFT", container, "LEFT", 20, 0 )
local pushed_texture = container.icon:CreateTexture( nil, "OVERLAY" )
pushed_texture:SetAllPoints( container.icon )
pushed_texture:SetTexture( "Interface\\Buttons\\UI-Quickslot-Depress" )
pushed_texture:Hide()
container.icon.pushed_texture = pushed_texture
local highlight_texture = container.icon:CreateTexture( nil, "OVERLAY" )
highlight_texture:SetAllPoints( container.icon )
highlight_texture:SetTexture( "Interface\\Buttons\\ButtonHilight-Square" )
highlight_texture:SetBlendMode( "ADD" )
highlight_texture:Hide()
container.icon.highlight_texture = highlight_texture
container.comment = m.create_backdrop_frame( m.api, "Frame", nil, container )
container.comment:SetPoint( "CENTER", container.icon, "CENTER", 0, 0 )
container.comment:SetWidth( 16 )
container.comment:SetHeight( 15 )
container.comment:SetFrameLevel( container.icon:GetFrameLevel() + 1 )
container.comment.inner = gui.create_text_in_container( "Frame", container.comment, 12, "CENTER", nil, "text", "GameFontNormalSmall" )
container.comment.inner:ClearAllPoints()
container.comment.inner:SetAllPoints( container.comment )
container.comment.inner.text:ClearAllPoints()
-- Small differences in rendering between OG and modern clients.
if m.vanilla then
container.comment.inner.text:SetPoint( "CENTER", container.comment.inner, "CENTER", 0, 1 )
else
container.comment.inner.text:SetPoint( "CENTER", container.comment.inner, "CENTER", 1, 0 )
end
container.comment.inner:SetScale( 0.7 )
container.comment.inner:SetFrameLevel( container.comment:GetFrameLevel() + 1 )
container.comment:SetBackdrop( {
bgFile = "Interface/Buttons/WHITE8x8",
edgeFile = "Interface/Buttons/WHITE8x8",
tile = false,
tileSize = 0,
edgeSize = 0.8
} )
container.comment:SetBackdropColor( 0, 0, 0, 0.7 )
container.comment:SetBackdropBorderColor( 1, 0.561, 0.184, 1 )
container.quantity = gui.create_text_in_container( "Frame", container.icon, 20, "CENTER", nil, "text", "NumberFontNormal" )
container.quantity:SetPoint( "BOTTOMRIGHT", -4, 1 )
container.quantity:SetHeight( 16 )
local middleleft = texture( container )
local middleright = texture( container )
local function resize()
container.icon:Show()
local icon_width = container.icon:GetWidth() + spacing
local text_width = container.name.text:GetStringWidth() + spacing + 1
local total_width = icon_width + text_width
container:SetWidth( total_width )
container:SetPoint( "LEFT", 0, 0 )
container:SetPoint( "RIGHT", 0, 0 )
end
local function not_hovered_color()
container:SetBackdropColor( 0, 0, 0, 0 )
end
local function update()
if not item then return end
if not item.is_enabled then
container.icon:SetAlpha( 0.35 )
container.name:SetAlpha( 0.35 )
return
end
container.icon:SetAlpha( 1 )
container.name:SetAlpha( 1 )
end
---@param v LootFrameItem
container.SetItem = function( _, v )
item = v
container.icon.texture:SetTexture( v.texture )
container.name.text:SetText( m.colorize_item_by_quality( v.name, v.quality ) )
container.name:SetPoint( "LEFT", container.icon, "RIGHT", spacing + 1, 0 )
if v.quantity and v.quantity > 1 then
container.quantity:Show()
container.quantity.text:SetText( v.quantity )
container.quantity:SetWidth( container.quantity.text:GetStringWidth() )
else
container.quantity:Hide()
end
if v.comment then
container.comment.inner.text:SetText( v.comment )
container.comment:Show()
else
container.comment:Hide()
end
local function modifier_fn()
if m.is_ctrl_key_down() then
m.api.DressUpItemLink( v.link )
return
end
if m.is_shift_key_down() then
m.link_item_in_chat( v.link )
return
end
end
container.icon:SetScript( "OnClick", v.is_enabled and not v.is_selected and v.click_fn or modifier_fn )
if m.vanilla then
-- Fucking hell this took forever to figure out. Fuck you Blizzard.
-- For looting to work in vanilla, the frame must be of a "LootButton" type and
-- then it comes with the SetSlot function that we need to use to set the slot.
-- This will probably be a pain in the ass when porting.
container.icon:SetSlot( v.slot or 0 )
end
update()
resize()
end
local function on_enter( self )
if m.vanilla then self = this end
if not item or item.is_enabled then container.icon.highlight_texture:Show() end
if not item then return end
if item.tooltip_link then
m.api.GameTooltip:SetOwner( self, "ANCHOR_RIGHT" )
m.api.GameTooltip:SetHyperlink( item.tooltip_link )
m.api.GameTooltip:Show()
end
if not item.is_enabled then return end
end
container:SetBackdrop( {
bgFile = "Interface/Buttons/WHITE8x8",
tile = false,
tileSize = 0,
} )
not_hovered_color()
local function on_leave()
container.icon.highlight_texture:Hide()
m.api.GameTooltip:Hide()
mouse_down = false
end
container.name:SetScript( "OnEnter", function( self )
if not item then return end
if item.comment_tooltip then
if m.vanilla then self = this end
self.tooltip_scale = m.api.GameTooltip:GetScale()
m.api.GameTooltip:SetOwner( self, "ANCHOR_RIGHT" )
local result = ""
for _, line in ipairs( item.comment_tooltip ) do
if result ~= "" then result = result .. "\n" end
result = result .. line
end
m.api.GameTooltip:AddLine( result, 1, 1, 1 )
m.api.GameTooltip:SetScale( 0.9 )
m.api.GameTooltip:Show()
end
end )
container.name:SetScript( "OnLeave", function( self )
if m.vanilla then self = this end
m.api.GameTooltip:Hide()
m.api.GameTooltip:SetScale( self.tooltip_scale or 1 )
mouse_down = false
not_hovered_color()
end )
container.icon:SetScript( "OnEnter", on_enter )
container.icon:SetScript( "OnLeave", on_leave )
local function on_mouse_down()
if not item or item.is_enabled then container.icon.pushed_texture:Show() end
if not item then return end
if not item.is_enabled or item.is_selected then return end
mouse_down = true
end
local function on_mouse_up()
container.icon.pushed_texture:Hide()
if not item then return end
if not item.is_enabled or item.is_selected then return end
if not mouse_down then return end
end
container.icon:SetScript( "OnMouseUp", on_mouse_up )
container.icon:SetScript( "OnMouseDown", on_mouse_down )
container:SetScript( "OnShow", function()
mouse_down = false
end )
local function update_textures( width )
local left_side_width = width - right_side_width
local height_offset = 0.498 + ((td.middleleft.height + 1) / 512)
middleleft:SetTexCoord( 0, (left_side_width + 2) / td.total.width, 0.511, height_offset )
middleleft:SetWidth( left_side_width + 2 )
middleleft:SetHeight( td.middleleft.height )
middleleft:SetPoint( "TOPLEFT", container, "TOPLEFT", 0, 0 )
middleright:SetTexCoord( (td.total.width - td.middleright.width) / td.total.width, 1, 0.511, height_offset )
middleright:SetWidth( td.middleright.width )
middleright:SetHeight( td.middleright.height )
middleright:SetPoint( "TOPRIGHT", container, "TOPRIGHT", 0, 0 )
end
local og_set_width = container.SetWidth
container.SetWidth = set_width( og_set_width, update_textures )
return container
end
---@param on_drag_stop function
---@param on_show function
---@param on_hide function
local function header( on_drag_stop, on_show, on_hide )
local frame = frame_builder.new() ---@type Frame
:name( "RollForLootFrameHeader" )
:parent( m.api.UIParent )
:width( min_width )
:height( header_height )
:sound()
:gui_elements( {} )
:movable()
:on_show( on_show )
:on_hide( on_hide )
:on_drag_stop( on_drag_stop )
:hidden()
:build()
local topleft = texture( frame )
local topright = texture( frame )
create_portrait( frame )
create_title( frame )
local close_button = create_close_button( frame )
close_button:ClearAllPoints()
close_button:SetPoint( "TOPRIGHT", frame, "TOPRIGHT", 5, -6 )
local function update( width )
local topoffset = td.topleft.height / td.total.height
local left_side_width = width - right_side_width
topleft:SetTexCoord( 0, left_side_width / td.total.width, 0, topoffset )
topleft:SetWidth( left_side_width )
topleft:SetHeight( td.topleft.height )
topleft:SetPoint( "TOPLEFT", frame, "TOPLEFT", 0, 0 )
topright:SetTexCoord( (td.total.width - td.topright.width) / td.total.width, 1, 0, topoffset )
topright:SetWidth( td.topright.width )
topright:SetHeight( td.topright.height )
topright:SetPoint( "TOPLEFT", frame, "TOPLEFT", left_side_width, 0 )
end
local og_set_width = frame.SetWidth
frame.SetWidth = set_width( og_set_width, update )
return frame
end
---@param parent Frame
local function body( parent )
local frame = frame_builder.new()
:name( "RollForLootFrame" )
:parent( parent )
:width( 280 )
:height( 100 )
:gui_elements( { dropped_item = dropped_item } )
:build()
frame:ClearAllPoints()
frame:SetPoint( "TOP", parent, "BOTTOM", 0, 1 )
local og_set_width = frame.SetWidth
frame.SetWidth = set_width( og_set_width )
return frame
end
---@param parent Frame
local function footer( parent )
local frame = frame_builder.new() ---@type Frame
:name( "RollForLootFrameFooter" )
:parent( parent )
:width( min_width )
:height( footer_height )
:gui_elements( {} )
:movable()
:build()
local bottomleft = texture( frame )
local bottomright = texture( frame )
local function update( width )
local left_side_width = width - right_side_width
bottomleft:SetTexCoord( 0, left_side_width / td.total.width, 1.001 - (td.bottomleft.height / td.total.height), 0.999 )
bottomleft:SetWidth( left_side_width )
bottomleft:SetHeight( td.bottomleft.height )
bottomleft:SetPoint( "TOPLEFT", frame, "TOPLEFT", 0, 0 )
bottomright:SetTexCoord( 1 - (td.bottomright.width / td.total.width), 1, 1.001 - (td.bottomright.height / td.total.height), 0.999 )
bottomright:SetWidth( td.bottomright.width )
bottomright:SetHeight( td.bottomright.height )
bottomright:SetPoint( "TOPRIGHT", frame, "TOPRIGHT", 0, 0 )
end
local og_set_width = frame.SetWidth
frame.SetWidth = set_width( og_set_width, update )
return frame
end
local function get_item_height()
return item_height
end
local function get_footer_height()
return footer_height
end
---@type LootFrameSkin
return {
header = header,
body = body,
dropped_item = dropped_item,
footer = footer,
get_item_height = get_item_height,
get_footer_height = get_footer_height
}
end
m.OgLootFrameSkin = M
return M

68
src/PlayerInfo.lua Normal file
View File

@ -0,0 +1,68 @@
RollFor = RollFor or {}
local m = RollFor
if m.PlayerInfo then return end
---@class PlayerInfo
---@field get_name fun(): string
---@field get_class fun(): string
---@field is_master_looter fun(): boolean
---@field is_leader fun(): boolean
---@field is_assistant fun(): boolean
local M = {}
---@param api table
function M.new( api )
local function get_name()
return api.UnitName( "player" )
end
local function get_class()
return api.UnitClass( "player" )
end
local function is_master_looter()
if not api.IsInGroup() then return false end
local loot_method, id = api.GetLootMethod()
if loot_method ~= "master" or not id then return false end
if id == 0 then return true end
if api.IsInRaid() then
local name = api.GetRaidRosterInfo( id )
return name == get_name()
end
return api.UnitName( "party" .. id ) == get_name()
end
local function is_leader()
return api.UnitIsGroupLeader( "player" )
end
local function is_assistant()
if not api.IsInRaid() then return false end
local my_name = get_name()
for i = 1, 40 do
local name, rank = api.GetRaidRosterInfo( i )
if name and name == my_name then
return rank and rank > 0 or false
end
end
end
---@type PlayerInfo
return {
get_name = get_name,
get_class = get_class,
is_master_looter = is_master_looter,
is_leader = is_leader,
is_assistant = is_assistant
}
end
m.PlayerInfo = M
return M

170
src/PopupBuilder.lua Normal file
View File

@ -0,0 +1,170 @@
RollFor = RollFor or {}
local m = RollFor
if m.PopupBuilder then return end
local M = {}
local getn = m.getn
---@class Popup : Frame
---@field resize fun( self: Popup, lines: table )
---@class PopupBuilder
---@field name fun( self: PopupBuilder, name: string ): PopupBuilder
---@field parent fun( self: PopupBuilder, parent: Frame ): PopupBuilder
---@field height fun( self: PopupBuilder, height: number ): PopupBuilder
---@field width fun( self: PopupBuilder, width: number ): PopupBuilder
---@field point fun( self: PopupBuilder, p: table ): PopupBuilder
---@field sound fun( self: PopupBuilder ): PopupBuilder
---@field frame_level fun( self: PopupBuilder, frame_level: number ): PopupBuilder
---@field backdrop_color fun( self: PopupBuilder, r: number, g: number, b: number, a: number ): PopupBuilder
---@field bg_file fun( self: PopupBuilder, bg_file: string ): PopupBuilder
---@field esc fun( self: PopupBuilder ): PopupBuilder
---@field gui_elements fun( self: PopupBuilder, gui_elements: table ): PopupBuilder
---@field frame_style fun( self: PopupBuilder, frame_style: FrameStyle ): PopupBuilder
---@field on_drag_stop fun( self: PopupBuilder, callback: function ): PopupBuilder
---@field movable fun( self: PopupBuilder ): PopupBuilder
---@field resizable fun( self ): PopupBuilder
---@field on_resize fun( self: PopupBuilder, callback: function ): PopupBuilder
---@field border_size fun( self: PopupBuilder, border_size: number ): PopupBuilder
---@field on_show fun( self: PopupBuilder, on_show: function ): PopupBuilder
---@field on_hide fun( self: PopupBuilder, on_hide: function ): PopupBuilder
---@field border_color fun( self: PopupBuilder, r: number, g: number, b: number, a: number ): PopupBuilder
---@field self_centered_anchor fun( self: PopupBuilder ): PopupBuilder
---@field scale fun( self: PopupBuilder, scale: number ): PopupBuilder
---@field strata fun( self: PopupBuilder, strata: FrameStrata ): PopupBuilder
---@field build fun( self: PopupBuilder ): Popup
---@param frame_builder FrameBuilderFactory
---@param bottom_margin number?
---@param bottom_button_margin number?
---@param side_margin number?
local function new( frame_builder, bottom_margin, bottom_button_margin, side_margin )
local m_button_padding = 10
local m_bottom_button_margin = bottom_button_margin or 8
local m_bottom_margin = bottom_margin or (30 + m_bottom_button_margin)
local m_side_margin = side_margin or 35
local function align_buttons( popup, lines )
if not popup.buttons_frame then
local frame = m.api.CreateFrame( "Frame", nil, popup )
frame:SetPoint( "BOTTOM", 0, m_bottom_button_margin )
popup.buttons_frame = frame
end
local total_width = 0
local max_height = 0
local last_anchor = nil
local buttons = m.filter( lines, function( line ) return line.line_type == "button" end )
for _, button in ipairs( buttons ) do
local frame = button.frame
local height = frame:GetHeight()
local width = frame:GetWidth()
local scale = frame:GetScale()
if height > max_height then max_height = height end
if not last_anchor then
frame:SetPoint( "LEFT", popup.buttons_frame, "LEFT", 0, 0 )
else
frame:SetPoint( "LEFT", last_anchor, "RIGHT", m_button_padding, 0 )
total_width = total_width + (m_button_padding * scale)
end
total_width = total_width + (width * scale)
last_anchor = frame
end
popup.buttons_frame:SetWidth( total_width )
popup.buttons_frame:SetHeight( max_height )
end
local function get_total_width( buttons )
local result = 0
for _, button in ipairs( buttons ) do
local frame = button.frame
result = result + frame:GetWidth() * frame:GetScale()
end
return result
end
local function resize( popup, lines )
local max_width = 0
local height = 0
for _, line in ipairs( lines ) do
if line.line_type ~= "button" and line.line_type ~= "info" then
local frame = line.frame
local scale = frame.GetScale and frame:GetScale() or 1
local width = frame:GetWidth() * scale
height = height + frame:GetHeight() * scale
height = height + line.padding
if width > max_width then max_width = width end
end
end
local buttons = m.filter( lines, function( line ) return line.line_type == "button" end )
local button_count = getn( buttons )
local button_width = get_total_width( buttons ) + (button_count - 1) * m_button_padding
if button_width > max_width then max_width = button_width end
if button_count > 0 then
height = height + 23
end
popup:SetWidth( max_width + m_side_margin )
popup:SetHeight( height + (button_count > 0 and m_bottom_margin or 23) )
align_buttons( popup, lines )
end
local decoratee = frame_builder.new()
local build = decoratee.build
decoratee.build = function()
---@class Popup
local result = build( decoratee )
result.resize = resize
return result
end
return decoratee
end
---@param frame_builder FrameBuilderFactory
---@param bottom_margin number?
---@param bottom_button_margin number?
---@param side_margin number?
function M.modern( frame_builder, bottom_margin, bottom_button_margin, side_margin )
local builder = new( frame_builder, bottom_margin, bottom_button_margin, side_margin )
:frame_style( "Modern" )
:backdrop_color( 0, 0, 0, 0.6 )
return builder
end
---@param frame_builder FrameBuilderFactory
---@param bottom_margin number?
---@param bottom_button_margin number?
---@param side_margin number?
function M.classic( frame_builder, bottom_margin, bottom_button_margin, side_margin )
local builder = new( frame_builder, bottom_margin, bottom_button_margin, side_margin )
:frame_style( "Classic" )
:border_size( 25 )
return builder
end
m.PopupBuilder = M
return M

View File

@ -0,0 +1,137 @@
RollFor = RollFor or {}
local m = RollFor
if m.RaidRollRollingLogic then return end
local M = {}
local getn = m.getn
local hl = m.colors.hl
local strategy = m.Types.RollingStrategy.RaidRoll
local roll_type = m.Types.RollType.MainSpec
local clear_table = m.clear_table
---@type MakeWinnerFn
local make_winner = m.Types.make_winner
-- TODO: Lots of similarity with InstaRaidRollRollingLogic. Perhaps refactor.
---@param chat Chat
---@param ace_timer AceTimer
---@param item Item
---@param item_count number
---@param winner_tracker WinnerTracker
---@param controller RollControllerFacade
---@param candidates ItemCandidate[]|Player[]
---@param roller PlayerInfo
function M.new(
chat,
ace_timer,
item,
item_count,
winner_tracker,
controller,
candidates,
roller
)
local m_rolling = false
local m_winners = {}
local function clear_winners()
clear_table( m_winners )
if m.vanilla then m_winners.n = 0 end
end
local function print_players( players )
local buffer = ""
for i, player in ipairs( players ) do
local separator = ""
if buffer ~= "" then separator = separator .. ", " end
local next_player = string.format( "[%d]:%s", i, player.name )
if (string.len( buffer .. separator .. next_player ) > 255) then
chat.announce( buffer )
buffer = next_player
else
buffer = buffer .. separator .. next_player
end
end
if buffer ~= "" then chat.announce( buffer ) end
end
local function raid_roll()
m_rolling = true
m.api.RandomRoll( 1, getn( candidates ) )
end
local function start_rolling()
m_rolling = true
clear_winners()
chat.announce( string.format( "Raid rolling %s%s...", item_count and item_count > 1 and string.format( "%sx", item_count ) or "", item.link ) )
print_players( candidates )
ace_timer.ScheduleTimer( M, function()
for _ = 1, item_count do
raid_roll()
end
end, 1 )
end
---@param player Player
---@param roll number
---@param min number
---@param max number
local function on_roll( player, roll, min, max )
if player.name ~= roller.get_name() then return end
if min ~= 1 or max ~= getn( candidates ) then return end
table.insert( m_winners, candidates[ roll ] )
if getn( m_winners ) < item_count then return end
local winners = m.map( m_winners,
---@param p ItemCandidate|Player
function( p )
if type( p ) == "table" then -- Fucking lua50 and its n.
local winner = make_winner( p.name, p.class, item, p.type == "ItemCandidate" or false, roll_type, nil )
winner_tracker.track( winner.name, item.link, roll_type, nil, m.Types.RollingStrategy.RaidRoll ) -- TODO: Get the fuck outta here.
return winner
end
end )
controller.winners_found( item, item_count, winners, strategy )
controller.finish()
m_rolling = false
end
local function is_rolling()
return m_rolling
end
local function show_sorted_rolls()
if getn( m_winners ) == 0 then
chat.info( "There is no winner yet.", nil, "RaidRoll" )
return
end
for _, winner in ipairs( m_winners ) do
chat.info( string.format( "%s won %s.", hl( winner.name ), item.link ), nil, "RaidRoll" )
end
end
---@type RollingStrategy
return {
start_rolling = start_rolling, -- This probably doesn't belong here either.
on_roll = on_roll,
is_rolling = is_rolling,
show_sorted_rolls = show_sorted_rolls,
get_type = function() return m.Types.RollingStrategy.RaidRoll end,
cancel_rolling = m.noop(),
stop_accepting_rolls = m.noop()
}
end
m.RaidRollRollingLogic = M
return M

1274
src/RollController.lua Normal file

File diff suppressed because it is too large Load Diff

34
src/RollForAd.lua Normal file
View File

@ -0,0 +1,34 @@
RollFor = RollFor or {}
local m = RollFor
if m.RollForAd then return end
local M = {}
local url = "https://github.com/obszczymucha/roll-for-vanilla/releases/download/latest/RollFor.zip"
---@param player_info PlayerInfo
function M.new( player_info )
local function on_chat_msg( channel )
return function( message, player_name )
if message == "RollFor" and player_name == player_info.get_name() then
m.api.SendChatMessage( url, channel )
end
end
end
local function on_chat_msg_whisper_inform( message, player_name )
if message == "RollFor" then
m.api.SendChatMessage( url, "WHISPER", nil, player_name )
end
end
return {
on_chat_msg_party = on_chat_msg( "PARTY" ),
on_chat_msg_raid = on_chat_msg( "RAID" ),
on_chat_msg_whisper_inform = on_chat_msg_whisper_inform
}
end
m.RollForAd = M
return M

222
src/RollResultAnnouncer.lua Normal file
View File

@ -0,0 +1,222 @@
RollFor = RollFor or {}
local m = RollFor
if m.RollResultAnnouncer then return end
local M = {}
local getn = m.getn
local RT = m.Types.RollType
local RS = m.Types.RollingStrategy
local hl = m.colors.hl
local grey = m.colors.grey
---@param chat Chat
---@param roll_controller RollController
---@param softres GroupAwareSoftRes
---@param config Config
function M.new( chat, roll_controller, softres, config )
---@param winners Winner[]
---@param top_roll boolean
local announce_winner = function( winners, top_roll )
local roll_value = winners[ 1 ].winning_roll
if not roll_value then
return
end
local roll_type = winners[ 1 ].roll_type
local roll_type_str = roll_type == RT.MainSpec and "" or string.format( " (%s)", m.roll_type_abbrev_chat( roll_type ) )
local rerolling = winners[ 1 ].rerolling
local item = winners[ 1 ].item
local function sr_plus( value )
local sr_players = softres.get( item.id )
local sr_player = m.find( winners[ 1 ].name, sr_players, 'name' )
if sr_player and sr_player.sr_plus then
local plus_value = sr_player.sr_plus
value = value - plus_value
return string.format( "%s+%s=%s", value, plus_value, value + plus_value )
end
return value
end
local function message( rollers, f )
return string.format(
"%s %srolled the %shighest (%s) for %s%s.",
rollers,
rerolling and "re-" or "",
top_roll and "" or "next ",
f and f( sr_plus( roll_value ) ) or sr_plus( roll_value ),
-- item_count and item_count > 1 and string.format( "%sx", item_count ) or "",
item.link,
roll_type_str
)
end
local rollers = m.prettify_table( winners, function( p ) return p.name end )
chat.info( message( rollers, hl ) )
chat.announce( message( rollers ) )
end
---@param winners Winner[]
---@return table<number, Winner[]>
local function split_winners_by_roll( winners )
if getn( winners ) == 0 then return {} end
local result = {}
local i = 0
local last_roll
for _, winner in ipairs( winners ) do
if not last_roll or last_roll ~= winner.winning_roll then
table.insert( result, { winner } )
i = i + 1
last_roll = winner.winning_roll
else
table.insert( result[ i ], winner )
end
end
return result
end
---@param data WinnersFoundData
local function on_winners_found( data )
if not data then return end
local item, item_count, winners, strategy = data.item, data.item_count, data.winners, data.rolling_strategy
local winner_count = getn( winners )
if winner_count == 0 then
return
end
if strategy == RS.RaidRoll or strategy == RS.InstaRaidRoll then
for _, winner in ipairs( winners ) do
chat.announce( string.format( "%s wins %s (raid-roll).", winner.name, item.link ) )
end
return
end
if strategy == RS.SoftResRoll and winner_count == item_count and not winners[ 1 ].winning_roll then
local ressed_by = m.prettify_table( m.map( winners, function( winner ) return winner.name end ) )
chat.announce( string.format( "%s soft-ressed %s.", ressed_by, item.link ), true )
return
end
for i, winners_by_roll in ipairs( split_winners_by_roll( winners ) ) do
announce_winner( winners_by_roll, i == 1 )
end
end
---@param data { players: RollingPlayer[], item: Item, item_count: number, roll_type: RollType, roll: number, rerolling: boolean?, top_roll: boolean? }
local function on_tie( data )
local players = data.players
local roll_type = data.roll_type
local roll_value = data.roll
local rerolling = data.rerolling
local top_roll = data.top_roll
local item = data.item
local player_names = m.map( players,
function( p )
if type( p ) == "table" then -- Fucking lua50 and its n.
return p.name
end
end )
local top_rollers_str = m.prettify_table( player_names )
local top_rollers_str_colored = m.prettify_table( player_names, hl )
local roll_type_str = roll_type == RT.MainSpec and "" or string.format( " (%s)", m.roll_type_abbrev_chat( roll_type ) )
local function message( rollers, f )
return string.format(
"%s %srolled the %shighest (%s) for %s%s.",
rollers,
rerolling and "re-" or "",
top_roll and "" or "next ",
f and f( roll_value ) or roll_value,
-- item_count and item_count > 1 and string.format( "%sx", item_count ) or "",
item.link,
roll_type_str
)
end
chat.info( message( top_rollers_str_colored ) )
chat.announce( message( top_rollers_str ) )
end
---@param event_data TieStartData
local function on_tie_start( event_data )
local data, iteration = event_data.tracker_data, event_data.iteration
if not data or not iteration then return end
local player_count = getn( iteration.rolls )
if player_count == 0 then return end
local roll_type = iteration.rolls[ 1 ].roll_type
local item, item_count, winners = data.item, data.item_count, data.winners
local winner_count = getn( winners )
local count = item_count - winner_count
local prefix = count > 1 and string.format( "%sx", count ) or ""
local suffix = count > 1 and string.format( " %s top rolls win.", count ) or ""
local player_names = m.map( iteration.rolls,
---@param roll_data RollData
function( roll_data )
return roll_data.player_name
end )
local top_rollers_str = m.prettify_table( player_names )
local roll_threshold_str = config.roll_threshold( roll_type ).str
chat.announce( string.format( "%s %s for %s%s now.%s", top_rollers_str, roll_threshold_str, prefix, item.link, suffix ) )
end
local function on_tick( data )
if not data or not data.seconds_left then return end
local seconds_left = data.seconds_left
if seconds_left == 3 then
chat.announce( "Stopping rolls in 3" )
elseif seconds_left < 3 then
chat.announce( tostring( seconds_left ) )
end
end
---@param event_data RollingFinishedData
local function on_finish( event_data )
local data = event_data.roll_tracker_data
if not data or not data.item then return end
local winner_count = getn( data.winners )
if winner_count == 0 then
local message = string.format( "No one rolled for %s.", data.item.link )
chat.info( message )
chat.announce( message )
end
end
---@param data LootAwardedData
local function on_loot_awarded( data )
local player_name = data.player_class and m.colorize_player_by_class( data.player_name, data.player_class ) or grey( data.player_name )
chat.info( string.format( "%s received %s.", player_name, data.item_link ) )
end
roll_controller.subscribe( "finish", on_finish )
roll_controller.subscribe( "winners_found", on_winners_found )
roll_controller.subscribe( "there_was_a_tie", on_tie )
roll_controller.subscribe( "tie_start", on_tie_start )
roll_controller.subscribe( "tick", on_tick )
roll_controller.subscribe( "loot_awarded", on_loot_awarded )
end
m.RollResultAnnouncer = M
return M

358
src/RollTracker.lua Normal file
View File

@ -0,0 +1,358 @@
RollFor = RollFor or {}
local m = RollFor
if m.RollTracker then return end
-- I hold the entire journey of rolls.
-- The first iteration starts with either a normal or soft-res rolling.
-- Then there's either a winner or a tie.
-- For each tie we have a new iteration, because a tie can result in another tie.
local M = m.Module.new( "RollTracker" )
local getn = m.getn
local clear_table = m.clear_table
local RS = m.Types.RollingStrategy
local RT = m.Types.RollType
local S = m.Types.RollingStatus
---@class RollData
---@field player_name string
---@field player_class string
---@field roll_type RollType
---@field roll number?
---@class RollIteration
---@field rolling_strategy RollingStrategyType
---@field message string
---@field rolls RollData[]
---@field ignored_rolls RollData[]?
---@field tied_roll number?
-- The status data is different for each type. TODO: split this.
---@class RollStatus
---@field type RollingStatus
---@field seconds_left number?
---@field winners RollingPlayer[]?
---@field ml_candidates ItemCandidate[]?
---@alias RollTrackerData {
--- item: Item|MasterLootDistributableItem,
--- item_count: number,
--- status: RollStatus,
--- iterations: RollIteration[],
--- winners: Winner[],
--- ml_candidates: ItemCandidate[] }
---@class RollTracker
---@field preview fun( count: number, ml_candidates: ItemCandidate[], soft_ressers: RollingPlayer[], hard_ressed: boolean )
---@field start fun( rolling_strategy: RollingStrategyType, count: number, seconds: number?, message: string?, required_rolling_players: RollingPlayer[]? )
---@field waiting_for_rolls fun()
---@field add_winners fun( winners: Winner[] )
---@field finish fun( ml_candidates: ItemCandidate[] )
---@field rolling_canceled fun()
---@field tie fun( required_rolling_players: RollingPlayer[], roll_type: RollType, roll: number )
---@field tie_start fun()
---@field add fun( player_name: string, player_class: string, roll_type: RollType, roll: number )
---@field add_ignored fun( player_name: string, roll_type: RollType, roll: number, reason: string )
---@field get fun(): RollTrackerData, RollIteration
---@field tick fun( seconds_left: number )
---@field clear fun()
---@field loot_awarded fun( player_name: string, item_id: number )
---@field create_roll_data fun( players: RollingPlayer[] ): RollData[]
---@param item_on_roll Item
function M.new( item_on_roll )
local status
local item_on_roll_count = 0
local iterations = {}
local current_iteration = 0
local master_loot_candidates = {}
---@type Winner[]
local winners = {}
local function lua50_clear_table( t )
clear_table( t )
if m.vanilla then t.n = 0 end
end
local function update_roll( rolls, data )
M.debug.add( "update_roll" )
for _, line in ipairs( rolls ) do
if line.player_name == data.player_name and not line.roll then
line.roll = data.roll
return
end
end
end
local function sort( rolls )
table.sort( rolls, function( a, b )
if a.roll_type ~= b.roll_type then return a.roll_type < b.roll_type end
if a.roll and b.roll then
if a.roll == b.roll then
return a.player_name < b.player_name
end
return a.roll > b.roll
end
if a.roll then
return true
end
if b.roll then
return false
end
return a.player_name < b.player_name
end )
end
local function add( player_name, player_class, roll_type, roll )
if current_iteration == 0 then return end
M.debug.add( "add" )
---@type RollData
local data = { player_name = player_name, player_class = player_class, roll_type = roll_type, roll = roll }
local iteration = iterations[ current_iteration ]
if roll and (iteration.rolling_strategy == RS.SoftResRoll or iteration.rolling_strategy == RS.TieRoll) then
update_roll( iteration.rolls, data )
else
table.insert( iteration.rolls, data )
end
sort( iteration.rolls )
end
---@param players RollingPlayer[]
local function create_roll_data( players )
local result = {}
for _, player in ipairs( players ) do
for _ = 1, player.rolls do
---@type RollData
local data = { player_name = player.name, player_class = player.class, roll_type = RT.SoftRes }
table.insert( result, data )
end
end
return result
end
---@param count number
---@param ml_candidates ItemCandidate[]
---@param soft_ressers RollingPlayer[]
---@param hard_ressed boolean
local function preview( count, ml_candidates, soft_ressers, hard_ressed )
M.debug.add( "preview" )
current_iteration = 1
status = { type = S.Preview }
item_on_roll_count = count
local soft_ressed = getn( soft_ressers ) > 0
local ressed_item = soft_ressed or hard_ressed
table.insert( iterations, {
rolling_strategy = ressed_item and RS.SoftResRoll or RS.NormalRoll,
rolls = {}
} )
if soft_ressed then
status.winners = soft_ressers
for _, player in ipairs( soft_ressers or {} ) do
for _ = 1, player.rolls or 1 do
add( player.name, player.class, RT.SoftRes )
end
end
end
if ressed_item then
status.ml_candidates = ml_candidates
end
end
---@param rolling_strategy RollingStrategyType
---@param count number
---@param seconds number
---@param message string
---@param required_rolling_players RollingPlayer[]?
local function start( rolling_strategy, count, seconds, message, required_rolling_players )
M.debug.add( "start" )
lua50_clear_table( iterations )
lua50_clear_table( winners )
lua50_clear_table( master_loot_candidates )
current_iteration = 1
status = { type = S.InProgress, seconds_left = seconds }
item_on_roll_count = count
table.insert( iterations, {
rolling_strategy = rolling_strategy,
message = message,
rolls = {}
} )
for _, player in ipairs( required_rolling_players or {} ) do
for _ = 1, player.rolls or 1 do
add( player.name, player.class, rolling_strategy == RS.SoftResRoll and RT.SoftRes or RS.TieRoll )
end
end
end
---@param new_winners Winner[]
local function add_winners( new_winners )
M.debug.add( "add_winners" )
for _, winner in ipairs( new_winners ) do
table.insert( winners, winner )
end
end
---@param ml_candidates ItemCandidate[]
local function update_ml_candidates( ml_candidates )
lua50_clear_table( master_loot_candidates )
for _, ml_candidate in ipairs( ml_candidates ) do
table.insert( master_loot_candidates, ml_candidate )
end
end
---@param ml_candidates ItemCandidate[]
local function finish( ml_candidates )
M.debug.add( "finish" )
status = { type = S.Finished }
update_ml_candidates( ml_candidates )
end
--- @param players RollingPlayer[]
--- @param roll_type RollType
--- @param roll number
local function tie( players, roll_type, roll )
M.debug.add( "tie" )
current_iteration = current_iteration + 1
status = { type = S.TieFound }
table.insert( iterations, {
rolling_strategy = RS.TieRoll,
tied_roll = roll,
rolls = {}
} )
for _, player in ipairs( players or {} ) do
add( player.name, player.class, roll_type )
end
end
local function tie_start()
M.debug.add( "tie_start" )
status = { type = S.Waiting }
end
local function add_ignored( player_name, roll_type, roll, reason )
M.debug.add( "add_ignored" )
if current_iteration == 0 then return end
iterations[ current_iteration ].ignored_rolls = iterations[ current_iteration ].ignored_rolls or {}
local rolls = iterations[ current_iteration ].ignored_rolls
local data = { player_name = player_name, roll_type = roll_type, roll = roll, reason = reason }
table.insert( rolls, data )
end
local function get()
M.debug.add( "get" )
return {
item = item_on_roll,
item_count = item_on_roll_count,
status = status,
iterations = iterations,
winners = winners,
ml_candidates = master_loot_candidates
}, current_iteration > 0 and iterations[ current_iteration ] or nil
end
local function tick( seconds_left )
M.debug.add( "tick" )
if status.type == S.InProgress then
status.seconds_left = seconds_left
end
end
local function waiting_for_rolls()
M.debug.add( "waiting_for_rolls" )
status.type = S.Waiting
end
local function rolling_canceled()
M.debug.add( "rolling_canceled" )
if not status then return end
status.type = S.Canceled
end
local function clear()
error( "Nothing should be clearing this.", 2 )
-- M.debug.add( "clear" )
-- lua50_clear_table( iterations )
-- lua50_clear_table( winners )
-- lua50_clear_table( master_loot_candidates )
-- current_iteration = 0
-- status = nil
-- item_on_roll = nil
-- item_on_roll_count = 0
-- M.debug.add( "cleared" )
end
local function mark_as_awarded_if_no_more_items()
if item_on_roll_count == 0 then
status.type = S.Awarded
end
end
---@param player_name string
---@param item_id number
local function loot_awarded( player_name, item_id )
if item_on_roll.id ~= item_id then return end -- TODO: this makes no sense now
item_on_roll_count = item_on_roll_count - 1
local w = status.type == S.Preview and status.winners or winners
for i, winner in ipairs( w ) do
if winner.name == player_name then
table.remove( w, i )
mark_as_awarded_if_no_more_items()
return
end
end
mark_as_awarded_if_no_more_items()
end
---@type RollTracker
return {
preview = preview,
start = start,
waiting_for_rolls = waiting_for_rolls,
add_winners = add_winners,
finish = finish,
rolling_canceled = rolling_canceled,
tie = tie,
tie_start = tie_start,
add = add,
add_ignored = add_ignored,
get = get,
tick = tick,
clear = clear,
loot_awarded = loot_awarded,
create_roll_data = create_roll_data
}
end
m.RollTracker = M
return M

304
src/RollingLogic.lua Normal file
View File

@ -0,0 +1,304 @@
RollFor = RollFor or {}
local m = RollFor
if m.RollingLogic then return end
local M = {}
local getn = m.getn
local RS = m.Types.RollingStrategy
---@alias SoftresRollsAvailableCallback fun( rollers: RollingPlayer[] )
---@alias RollingFinishedCallback fun(
--- item: Item,
--- item_count: number,
--- winning_rolls: Roll[],
--- rerolling: boolean? )
---@class RollingLogic
---@field on_softres_rolls_available SoftresRollsAvailableCallback
---@field on_rolling_finished RollingFinishedCallback
---@field is_rolling fun(): boolean
---@field on_roll fun( player: Player, roll_value: number, min: number, max: number )
---@field show_sorted_rolls fun( limit: number? )
---@param chat Chat
---@param ace_timer AceTimer
---@param roll_controller RollController
---@param strategy_factory RollingStrategyFactory
---@param master_loot_candidates MasterLootCandidates
---@param winner_tracker WinnerTracker
function M.new( chat, ace_timer, roll_controller, strategy_factory, master_loot_candidates, winner_tracker, config )
---@type RollingStrategy | nil
local m_rolling_strategy
---@param rollers RollingPlayer[]
local function on_softres_rolls_available( rollers )
local remaining_rollers = m.reindex_table( rollers )
local transform = function( player )
local rolls = player.rolls == 1 and "1 roll" or string.format( "%s rolls", player.rolls )
return string.format( "%s (%s)", player.name, rolls )
end
roll_controller.waiting_for_rolls()
local message = m.prettify_table( remaining_rollers, transform )
chat.announce( string.format( "SR rolls remaining: %s", message ) )
end
---@param strategy RollingStrategy
---@param item Item?
---@param item_count number?
---@param seconds number?
---@param message string?
---@param rolling_players RollingPlayer[]?
local function roll( strategy, item, item_count, seconds, message, rolling_players )
if m_rolling_strategy and m_rolling_strategy.is_rolling() then
m.err( "Rolling is already in progress." )
return
end
m_rolling_strategy = strategy
if item and item_count then
roll_controller.rolling_started( strategy.get_type(), item, item_count, seconds, message, rolling_players )
end
m_rolling_strategy.start_rolling()
end
local function is_rolling()
return m_rolling_strategy and m_rolling_strategy.is_rolling() or false
end
---param winning_rolls Roll[]
local function count_top_rolls( winning_rolls )
local roll_count = winning_rolls and getn( winning_rolls ) or 0
if roll_count == 0 then return 0 end
local top_roll = winning_rolls[ 1 ].roll
local result = 1
for i = 2, roll_count do
if winning_rolls[ i ].roll == top_roll then result = result + 1 end
end
return result
end
---@param rolls Roll[]
---@param item_count number
---@return Roll[], Roll[]
local function split_winners_and_tied_rollers( rolls, item_count )
local top_roll_count = count_top_rolls( rolls )
if top_roll_count >= item_count then return {}, rolls end
local winning_rolls, tied_rolls = {}, {}
for i, top_roll in ipairs( rolls ) do
if i <= top_roll_count then
table.insert( winning_rolls, top_roll )
else
table.insert( tied_rolls, top_roll )
end
end
return winning_rolls, tied_rolls
end
---@type RollControllerFacade
local facade = {
roll_was_ignored = roll_controller.add_ignored,
roll_was_accepted = roll_controller.add,
tick = roll_controller.tick,
winners_found = roll_controller.winners_found,
finish = roll_controller.finish
}
---@param item Item
---@param item_count number
---@param rolls Roll[]
---@param rerolling boolean
local function there_was_a_tie( item, item_count, rolls, rerolling, on_rolling_finished )
local winning_rolls, tied_rolls = split_winners_and_tied_rollers( rolls, item_count )
local count = item_count
local winners = m.map( winning_rolls,
---@param winning_roll Roll
function( winning_roll )
return master_loot_candidates.transform_to_winner( winning_roll.player, item, winning_roll.roll_type, winning_roll.roll, rerolling )
end )
local winner_count = getn( winners )
count = count - winner_count
if winner_count > 0 then
roll_controller.winners_found( item, item_count, winners, RS.TieRoll )
end
local roll_type = tied_rolls[ 1 ].roll_type
local roll_value = tied_rolls[ 1 ].roll
---@type RollingPlayer[]
local players = m.map( tied_rolls,
---@param tied_roll Roll
function( tied_roll )
return tied_roll.player
end )
roll_controller.there_was_a_tie( players, item, count, roll_type, roll_value, rerolling, getn( winning_rolls ) == 0 or false )
local strategy = strategy_factory.tie_roll( players, item, count, on_rolling_finished, roll_type, facade )
if not strategy then return end
ace_timer.ScheduleTimer( M,
function()
roll_controller.tie_start()
m_rolling_strategy = nil
roll( strategy )
end, 2 )
end
---@param item Item
---@param item_count number
---@param winning_rolls Roll[]
---@param rerolling boolean?
---@type RollingFinishedCallback
local function on_rolling_finished( item, item_count, winning_rolls, rerolling )
local winning_roll_count = getn( winning_rolls )
if winning_roll_count == 0 then
roll_controller.finish()
if not rerolling and config.auto_raid_roll() and m_rolling_strategy and m_rolling_strategy.get_type() ~= RS.SoftResRoll then
-- At some point item_count gets to 0.
if item_count == 0 then
m.trace( "Item count is 0." )
end
m_rolling_strategy = nil
roll_controller.start( "RaidRoll", item, item_count )
elseif m_rolling_strategy and not m_rolling_strategy.is_rolling() then
chat.info( string.format( "Rolling for %s finished.", item.link ) )
end
return
end
if winning_roll_count > item_count then
there_was_a_tie( item, item_count, winning_rolls, rerolling or false, on_rolling_finished )
return
end
local function handle_winners()
local strategy = m_rolling_strategy and m_rolling_strategy.get_type()
if not strategy then
m.err( "Rolling strategy is missing." )
return
end
local winners = m.map( winning_rolls,
---@param winning_roll Roll
function( winning_roll )
return master_loot_candidates.transform_to_winner( winning_roll.player, item, winning_roll.roll_type, winning_roll.roll, rerolling )
end )
roll_controller.winners_found( item, item_count, winners, strategy )
m.map( winners, function( winner )
winner_tracker.track( winner.name, item.link, winner.roll_type, winner.winning_roll, strategy ) -- TODO: remove from here and subscribe to the event.
end )
roll_controller.finish()
end
handle_winners()
if not is_rolling() then
chat.info( string.format( "Rolling for %s finished.", item.link ) )
end
end
local function cancel_rolling()
if not m_rolling_strategy then return end
m_rolling_strategy.cancel_rolling()
roll_controller.rolling_canceled()
end
---@param player Player
---@param roll_value number
---@param min number
---@param max number
local function on_roll( player, roll_value, min, max )
if m_rolling_strategy and m_rolling_strategy.is_rolling() then
m_rolling_strategy.on_roll( player, roll_value, min, max )
end
end
local function finish_rolling_early()
if m_rolling_strategy then m_rolling_strategy.stop_accepting_rolls( true ) end
end
---@param limit number
local function show_sorted_rolls( limit )
if m_rolling_strategy then m_rolling_strategy.show_sorted_rolls( limit ) end
end
---@param data RollControllerStartData
local function start( data )
---@return RollingStrategy?
---@return RollingPlayer[]?
local function make_strategy()
local seconds = data.seconds or config.default_rolling_time_seconds()
if data.strategy_type == RS.SoftResRoll then
return strategy_factory.softres_roll(
data.item,
data.item_count,
data.message,
seconds,
on_rolling_finished,
on_softres_rolls_available,
facade
)
elseif data.strategy_type == RS.NormalRoll then
return strategy_factory.normal_roll(
data.item,
data.item_count,
data.message,
seconds,
on_rolling_finished,
facade
)
elseif data.strategy_type == RS.RaidRoll then
return strategy_factory.raid_roll( data.item, data.item_count, facade )
elseif data.strategy_type == RS.InstaRaidRoll then
return strategy_factory.insta_raid_roll( data.item, data.item_count, facade )
end
end
local strategy, rolling_players = make_strategy()
if not strategy then return end
winner_tracker.start_rolling( data.item.link )
roll( strategy, data.item, data.item_count, data.seconds, data.message, rolling_players )
end
roll_controller.subscribe( "finish_rolling_early", finish_rolling_early )
roll_controller.subscribe( "cancel_rolling", cancel_rolling )
roll_controller.subscribe( "start", start )
---@type RollingLogic
return {
on_rolling_finished = on_rolling_finished,
on_softres_rolls_available = on_softres_rolls_available,
is_rolling = is_rolling,
on_roll = on_roll,
show_sorted_rolls = show_sorted_rolls
}
end
m.RollingLogic = M
return M

Some files were not shown because too many files have changed in this diff Show More