Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: AlexAllocated/libchev
ref: 445c0cf5b3dd40d36461f6e0b9adc2feb8dfb16b
ref: bd83b65fcbd21d108d8f6c02677acddb30541c6a
path: library
- run: sudo apt-get update && sudo apt-get install -y lua${{ matrix.lua }}
- run: lua${{ matrix.lua }} scripts/test.lua
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.2.2 — 2026-09-24

Announce the installed version, supported clients and settings command once per login or UI reload. Include addon-specific CurseForge and GitHub feedback links; clicking a link opens a native-style copy window. Share message behavior and safe copy UI through private libchev 1.2.0. If link registration or the copy window is unavailable, show the full URL in chat. An unavailable welcome helper cannot interrupt normal addon startup.

Validation: 118 tests pass in both orders on Lua 5.1 and 5.2, with client API checks, Lua parsing and exact library vendor verification. NoPoizen smoke simulations exercise both feedback links on all seven client/ruleset profiles. Live rendering remains a separate check.

## 1.2.1 — 2026-09-24

Show Blizzard's crossed-swords icon beside PvPTogether in the AddOns list instead of the default question mark.
Expand Down
1 change: 1 addition & 0 deletions Core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -645,5 +645,6 @@ eventFrame:SetScript("OnEvent", function(_, eventName, ...)
if PvPTogether:GetOption("enabled") then
PvPTogether:Enable()
end
PvPTogether:PrintWelcomeMessage()
end
end)
33 changes: 26 additions & 7 deletions Libs/libchev/ReportWindow.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,40 @@ function LibChev.OpenReportWindow(owner, text, policy)
local success, allowed = pcall(policy.canMutate, region)
return success and LibChev.CanAccess(allowed) and allowed == true
end
local copyLink = policy.copyLink == true
local width, height = copyLink and 620 or 660, copyLink and 200 or 460
local frame = owner.diagnosticsWindow
if not frame then
frame = policy.createFrame("Frame", nil, policy.parent)
frame = policy.createFrame("Frame", nil, policy.parent, copyLink and "BackdropTemplate" or nil)
if not CanMutate(frame) then
return false
end
frame:SetSize(660, 460)
frame:SetSize(width, height)
frame:SetPoint("CENTER")
frame:SetFrameStrata("DIALOG")
frame:SetClampedToScreen(true)
frame:EnableMouse(true)
frame:Hide()
if copyLink then
frame:SetToplevel(true)
if frame.SetFlattensRenderLayers then
frame:SetFlattensRenderLayers(true)
end
frame:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true,
tileSize = 32,
edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 },
})
end
local background = frame:CreateTexture(nil, "BACKGROUND")
if not CanMutate(background) then
return false
end
background:SetAllPoints()
background:SetColorTexture(0.04, 0.04, 0.04, 0.97)
background:SetColorTexture(0.04, 0.04, 0.04, copyLink and 0 or 0.97)
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
if not CanMutate(title) then
return false
Expand All @@ -49,7 +65,10 @@ function LibChev.OpenReportWindow(owner, text, policy)
return false
end
hint:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -10)
hint:SetText("Report selected: press Ctrl+C to copy. Use the mouse wheel to scroll.")
hint:SetText(
copyLink and "Press Ctrl+C to copy this address, then open it in your browser."
or "Report selected: press Ctrl+C to copy. Use the mouse wheel to scroll."
)
local scroll = policy.createFrame("ScrollFrame", nil, frame)
if not CanMutate(scroll) then
return false
Expand All @@ -61,7 +80,7 @@ function LibChev.OpenReportWindow(owner, text, policy)
if not CanMutate(box) then
return false
end
box:SetWidth(600)
box:SetWidth(width - 60)
box:SetHeight(1)
box:SetMultiLine(true)
box:SetAutoFocus(false)
Expand All @@ -72,7 +91,7 @@ function LibChev.OpenReportWindow(owner, text, policy)
if not CanMutate(measure) then
return false
end
measure:SetWidth(592)
measure:SetWidth(width - 68)
measure:Hide()
frame.TextBox, frame.Scroll, frame.Measure = box, scroll, measure
scroll:SetScript("OnMouseWheel", function(_, delta)
Expand Down Expand Up @@ -122,7 +141,7 @@ function LibChev.OpenReportWindow(owner, text, policy)
end
selectAll:SetSize(120, 24)
selectAll:SetPoint("BOTTOMLEFT", 20, 16)
selectAll:SetText("Select Report")
selectAll:SetText(copyLink and "Select Link" or "Select Report")
selectAll:SetScript("OnClick", function()
if CanMutate(box) then
box:SetFocus()
Expand Down
77 changes: 76 additions & 1 deletion Libs/libchev/libchev.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-- Private embedding: every addon receives its own library through its loader
-- namespace. No global registry, Blizzard mutation, hooks, events or saved data.
local _, namespace = ...
local LibChev = { VERSION = "1.1.3", API_VERSION = 1 }
local LibChev = { VERSION = "1.2.0", API_VERSION = 1 }
local unpackValues = unpack or table.unpack
local secret = type(issecretvalue) == "function" and issecretvalue or nil
local accessible = type(canaccessvalue) == "function" and canaccessvalue or nil
Expand Down Expand Up @@ -422,6 +422,81 @@ function LibChev.TestSummary(result)
return string.format("Test summary: %d passed, %d failed (%d total).", result.passed, result.failed, result.total)
end

-- The consumer owns all configuration, link registration and UI policies.
-- Only a primitive link string crosses into this controller from the client.
function LibChev.NewWelcomeController(policy)
local controller = {}
local destinations = {
{ key = "curseforge", label = "CurseForge", url = policy.curseforgeURL },
{ key = "github", label = "GitHub", url = policy.githubURL },
}
local function PlainText(value, fallback)
return (LibChev.Text(value, fallback):gsub("|", "||"))
end
function controller:HandleLink(link)
if not LibChev.CanAccess(link) or type(link) ~= "string" then
return false
end
for _, destination in ipairs(destinations) do
if link == policy.linkType .. ":" .. destination.key then
local ui = policy.ui
local ok, shown = pcall(LibChev.OpenReportWindow, self, destination.url, {
parent = ui.parent,
createFrame = ui.createFrame,
restricted = ui.restricted,
canMutate = ui.canMutate,
title = policy.addonName .. " Feedback",
copyLink = true,
})
if not ok or shown ~= true then
pcall(policy.print, "Feedback: " .. destination.url)
end
return true
end
end
return false
end
function controller:Announce()
if self.announced then
return false
end
if not self.linksRegistered then
local ok, registered = pcall(policy.registerLink, policy.linkType, function(link)
self:HandleLink(link)
-- The public link dispatcher treats nil as handled. No foreign
-- linkData/context tables or global response constants are needed.
end)
self.linksRegistered = ok and registered == true
end
local labels = {}
for index, destination in ipairs(destinations) do
labels[index] = self.linksRegistered
and ("|cff71d5ff|H" .. policy.linkType .. ":" .. destination.key .. "|h[" .. destination.label .. "]|h|r")
or (destination.label .. " (" .. destination.url .. ")")
end
local ok, version = pcall(policy.getVersion)
version = ok and PlainText(version, "unknown") or "unknown"
if version == "" then
version = "unknown"
end
local message = "v"
.. version
.. " loaded! Now supports "
.. policy.clients
.. ". Type "
.. policy.command
.. " for settings. If you run into any issues, please leave feedback on "
.. labels[1]
.. " or "
.. labels[2]
.. "."
local printed = pcall(policy.print, message)
self.announced = printed
return printed
end
return controller
end

if type(namespace) == "table" then
namespace.LibChev = LibChev
end
Expand Down
8 changes: 4 additions & 4 deletions Libs/libchev/manifest.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"schema": 1,
"repository": "https://github.com/AlexAllocated/libchev",
"revision": "445c0cf5b3dd40d36461f6e0b9adc2feb8dfb16b",
"version": "1.1.3",
"revision": "bd83b65fcbd21d108d8f6c02677acddb30541c6a",
"version": "1.2.0",
"files": {
"libchev.lua": "e0d6bae4a49d2bf9945fae6a6176af5a47b7af45384696b2c707e18aac897c4f",
"libchev.lua": "78e01683da6d82a6a8c3c783f3b804c5e4e384485f0c0520d7b0b5f5852a1f87",
"Debug.lua": "1a9b8354d9b9b4e38e8fff3bb97ba2a9916200bf03ad1fb494a071c22091e8b6",
"DebugWindow.lua": "b0777bf7f0cb25a0629c82ed09f50b330f7598aca53b9e862954acbb1cf59ac4",
"ReportWindow.lua": "925e0dde12cb01e0575442343b3cddf462c6b57e7c741b5aba0b9ddb35aaa125",
"ReportWindow.lua": "3ea726f617979f7c69d9b521fa736f53e929358b61c40879252a012e1081095e",
"SelfTests.lua": "0ba6b144616dd8264ce9417b05d3efbf73166fae876a730421f7de9112daaef0",
"LICENSE": "d1bc39a4c90fbac0fc97b6a98786200011eab7ff77a29c2274603ae4e12fe03b"
}
Expand Down
3 changes: 2 additions & 1 deletion PvPTogether.toc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## Interface-Camelot: 16001
## Title: PvPTogether
## IconTexture: Interface\Icons\Ability_DualWield
## Version: 1.2.1
## Version: 1.2.2
## Notes: Custom party, friendly, and enemy player nameplate borders.
## SavedVariablesPerCharacter: PvPTogetherDBChar
## Author: Chev
Expand All @@ -18,6 +18,7 @@ Libs/libchev/DebugWindow.lua
Libs/libchev/ReportWindow.lua
Libs/libchev/SelfTests.lua
Core.lua
Welcome.lua
Nameplates.lua
Options.lua
InGameTests.lua
76 changes: 76 additions & 0 deletions Welcome.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
-- Shared welcome formatting, session state and copy UI live in private libchev.
local Addon = _G.PvPTogether
local LibChev = Addon.LibChev

-- Register only our own link type through Blizzard's public dispatcher. The
-- handler consumes the primitive link string, never foreign context tables.
function Addon:RegisterWelcomeLink(linkType, callback)
if not self:CanAccessTable(LinkUtil) then
return false
end
local exists, register = LinkUtil.IsLinkHandlerRegistered, LinkUtil.RegisterLinkHandler
if
not self:CanAccessValue(exists)
or not self:CanAccessValue(register)
or type(exists) ~= "function"
or type(register) ~= "function"
then
return false
end
local ok, registered = pcall(exists, linkType)
if not ok or not self:CanAccessValue(registered) or registered ~= false then
return false
end
return pcall(register, linkType, callback)
end

function Addon:GetWelcomeUIPolicy()
return {
parent = UIParent,
createFrame = CreateFrame,
restricted = function()
return not self.IsNameplateAugmentationBlockedInCurrentContext
or self:IsNameplateAugmentationBlockedInCurrentContext()
end,
canMutate = LibChev.CanMutateOwnedRegion,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce native permission checks for feedback frames

When C_RestrictedActions.CheckAllowProtectedFunctions(region, true) denies a mutation while the coarse restriction predicates are inactive, this policy still authorizes every feedback-window setter because CanMutateOwnedRegion checks only accessibility and IsProtected. Unlike the existing debug UI policy in Core.lua, the feedback UI never delegates to CanMutateNameplateFrame, so clicking a feedback link can attempt denied mutations; the surrounding pcall can provide the chat fallback but cannot make those attempts taint-safe. Compose the owned-region check with the addon's native permission check before allowing mutations.

AGENTS.md reference: AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

}
end

function Addon:GetWelcomeController()
local controller = rawget(self, "welcomeController")
if not controller then
controller = LibChev.NewWelcomeController({
addonName = "PvPTogether",
linkType = "pvptogetherfeedback",
curseforgeURL = "https://www.curseforge.com/wow/addons/pvptogether",
githubURL = "https://github.com/AlexAllocated/PvPTogether",
command = "/pt",
clients = "Retail, WoW Forever, Classic Era, Hardcore, Season of Discovery, TBC Anniversary, Mists Classic, and Titan Reforged",
getVersion = function()
return self:GetDiagnosticVersion()
end,
registerLink = function(linkType, callback)
return self:RegisterWelcomeLink(linkType, callback)
end,
print = function(text)
self:Print(text)
end,
ui = self:GetWelcomeUIPolicy(),
})
self.welcomeController = controller
end
return controller
end

function Addon:PrintWelcomeMessage()
-- A partially updated installation must never prevent normal addon startup.
if type(LibChev.NewWelcomeController) ~= "function" then
return false
end
local ok, controller = pcall(self.GetWelcomeController, self)
if not ok then
return false
end
local announced, result = pcall(controller.Announce, controller)
return announced and result == true
end
34 changes: 34 additions & 0 deletions tests/core.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,38 @@
return function(H)
H.test("welcome login announces once routes feedback and tolerates unavailable helpers", function()
local handler
local addon, state = H.new({
coreOnly = true,
configure = function(env)
env.LinkUtil = {
IsLinkHandlerRegistered = function()
return false
end,
RegisterLinkHandler = function(kind, callback)
H.equal(kind, "pvptogetherfeedback")
handler = callback
end,
}
end,
})
state:emit("PLAYER_LOGIN")
state:emit("PLAYER_LOGIN")
H.truthy(addon.hasLoggedIn)
H.truthy(addon.isEnabled)
H.equal(#state.messages, 1)
H.truthy(state.messages[1][1]:find("v1.0.7 loaded!", 1, true))
H.truthy(state.messages[1][1]:find("Type /pt for settings.", 1, true))
handler("pvptogetherfeedback:curseforge")
H.truthy(state.messages[2][1]:find("https://www.curseforge.com/wow/addons/pvptogether", 1, true))
handler("pvptogetherfeedback:github")
H.truthy(state.messages[3][1]:find("https://github.com/AlexAllocated/PvPTogether", 1, true))
-- This library belongs to the offline environment, never a live addon.
addon.LibChev.NewWelcomeController = nil
state:emit("PLAYER_LOGIN")
H.truthy(addon.isEnabled)
H.equal(#state.messages, 3)
end)

H.test("legacy style preferences are preserved but no longer accepted as active settings", function()
local addon, state = H.new({
coreOnly = true,
Expand Down
1 change: 1 addition & 0 deletions tests/harness.lua
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,7 @@ return function(root)
state:load("Libs/libchev/ReportWindow.lua")
state:load("Libs/libchev/SelfTests.lua")
state:load("Core.lua")
state:load("Welcome.lua")
state:load("InGameTests.lua")
if not options.coreOnly then
state:load("Nameplates.lua")
Expand Down
2 changes: 1 addition & 1 deletion tests/integration.lua
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ return function(H)
local report = window.TextBox:GetText()
H.truthy(window:IsShown())
H.truthy(report:find("addon=PvPTogether", 1, true))
H.truthy(report:find("library=libchev 1.1.3", 1, true))
H.truthy(report:find("library=libchev 1.2.0", 1, true))
H.truthy(report:find("suite=PvPTogether in-game tests", 1, true))
H.truthy(
report:find("Addon-owned isolated checks; live-client behavior requires separate validation.", 1, true)
Expand Down
Loading