-- Dex Hood native TagStyles integration. Paste into Studio's EDIT-MODE COMMAND BAR. -- Never paste into the live developer console. No code downloads or game publication. assert(not game:GetService("RunService"):IsRunning(), "Stop Play/Test before installing.") assert(game.PlaceId == 0 or game.PlaceId == 108265213581634, "Open the main Dex Hood place before installing.") local editor = game:GetService("ScriptEditorService") local SSS = game:GetService("ServerScriptService") local RS = game:GetService("ReplicatedStorage") local native = RS:FindFirstChild("TagStyles") assert(native and native:IsA("ModuleScript"), "ReplicatedStorage.TagStyles is missing; nothing changed.") local loader = game:GetService("StarterPlayer").StarterPlayerScripts:FindFirstChild("PlayerScriptsLoader") assert(loader and loader:IsA("LocalScript") and not loader.Disabled, "The native PlayerScriptsLoader renderer is missing or disabled; nothing changed.") local loaderSource = editor:GetEditorSource(loader) assert(loaderSource:find("tagStyles", 1, true) and loaderSource:find("CustomNameTag", 1, true) and loaderSource:find("GetAttribute('Tags')", 1, true), "Unrecognized native tag renderer; nothing changed.") local nametag = SSS:FindFirstChild("Nametag") assert(nametag and nametag:IsA("Script") and not nametag.Disabled and editor:GetEditorSource(nametag):find("DexDashboardIsAdmin", 1, true), "The existing dashboard staff-permission adapter is missing or disabled; nothing changed.") local function resolve(path) local item = game for segment in path:gmatch("[^/]+") do item = assert(item:FindFirstChild(segment), "Missing path: " .. path) end return item end local changes = { {parent = "ServerScriptService", name = "DexNativeTags", class = "ModuleScript", source = [=[-- Pure validation and merge logic. TagStyles remains the sole style authority. local Native = {} Native.RegistryKey = "tags-native-v1" Native.CatalogKey = "tag-styles-v1" Native.CustomKey = "tag-styles-custom-v1" Native.BridgeKey = "bridge-native-v1" -- Keep in sync with MAX_TAGS in lib/native-tags.ts. Native.MaxTags = 250 function Native.empty() return {version = 2, revision = 0, tags = {}, audit = {}} end local function text(value, limit, required) assert(type(value) == "string" and utf8.len(value) and utf8.len(value) <= limit and not value:find("[<>%c]"), "Invalid text.") value = value:match("^%s*(.-)%s*$") assert(not required or value ~= "", "Text, target ID, and a native style are required.") return value end local function integer(value, minimum, maximum) assert(type(value) == "number" and value == value and value % 1 == 0 and value >= minimum and value <= maximum, "Invalid whole number.") return value end local function flag(value) assert(type(value) == "boolean", "Invalid toggle.") return value end function Native.validate(input) assert(type(input) == "table", "Tag required.") local id = text(input.id, 64, false) assert(id == "" or not id:find("[^%w_-]"), "Invalid tag ID.") local targetId = text(input.targetId, 16, true) assert(targetId:match("^[1-9]%d*$") and tonumber(targetId) <= 9007199254740991, "Invalid Roblox ID.") assert(input.targetType == "group" or input.targetType == "player", "Invalid target type.") return {id = id, targetId = targetId, targetType = input.targetType, rank = integer(input.rank, 0, 255), text = text(input.text, 24, true), style = text(input.style, 80, true), bold = flag(input.bold), priority = integer(input.priority, 0, 100), enabled = flag(input.enabled)} end function Native.registry(value) if value == nil then return Native.empty() end assert(type(value) == "table" and value.version == 2 and type(value.tags) == "table" and type(value.audit) == "table", "Unsupported native registry; no data changed.") integer(value.revision, 0, 9007199254740991) assert(#value.tags <= Native.MaxTags and #value.audit <= 100, "Registry limit exceeded.") local seen, count = {}, 0 for index, rule in pairs(value.tags) do integer(index, 1, #value.tags) local valid = Native.validate(rule) assert(valid.id ~= "" and not seen[valid.id], "Duplicate or missing tag ID.") if rule.authorUserId ~= nil then integer(rule.authorUserId, 1, 9007199254740991) end seen[valid.id] = true count += 1 end assert(count == #value.tags, "Invalid tag array.") return value end local MOTIONS = {sweep = true, pulse = true, static = true} -- Website-authored styles. Pure data; the game turns them into real style tables below. function Native.customRegistry(value) if value == nil then return {version = 1, revision = 0, styles = {}} end assert(type(value) == "table" and value.version == 1 and type(value.styles) == "table", "Unsupported custom style registry; no data changed.") integer(value.revision, 0, 9007199254740991) assert(#value.styles <= 100, "Custom style limit exceeded.") local seen = {} for index, style in ipairs(value.styles) do integer(index, 1, #value.styles) assert(type(style) == "table", "Invalid custom style.") local key = text(style.key, 80, true) assert(key:sub(1, 7) == "custom/", "Custom style keys must start with custom/.") assert(not seen[key], "Duplicate custom style key.") seen[key] = true text(style.name, 32, true) local crew = text(style.crewGroupId, 16, true) assert(crew:match("^[1-9]%d*$"), "Invalid crew group ID.") assert(type(style.speed) == "number" and style.speed >= 0.2 and style.speed <= 10, "Invalid style speed.") assert(MOTIONS[style.motion], "Invalid style motion.") assert(type(style.stops) == "table" and #style.stops >= 2 and #style.stops <= 8, "A style needs 2 to 8 gradient colors.") local previous = -1 for stopIndex, stop in ipairs(style.stops) do integer(stopIndex, 1, #style.stops) assert(type(stop) == "table" and type(stop.position) == "number" and stop.position > previous and stop.position >= 0 and stop.position <= 1, "Invalid gradient position.") assert(type(stop.color) == "string" and stop.color:match("^#%x%x%x%x%x%x$"), "Invalid gradient color.") previous = stop.position end assert(style.stops[1].position == 0 and style.stops[#style.stops].position == 1, "Gradients must run from 0 to 1.") if style.iconAssetId ~= nil and style.iconAssetId ~= "" then assert(text(style.iconAssetId, 16, true):match("^[1-9]%d*$"), "Invalid icon asset ID.") end end return value end function Native.catalog(styles) local keys = {} for key, definition in pairs(styles) do if type(key) == "string" and type(definition) == "table" and #key > 0 and key == key:match("^%s*(.-)%s*$") and utf8.len(key) and utf8.len(key) <= 80 and not key:find("[<>%c]") then table.insert(keys, key) end end table.sort(keys) assert(#keys > 0 and #keys <= 1000, "TagStyles must contain 1 to 1000 named styles.") return keys end function Native.escape(textValue) return textValue:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"):gsub('"', """):gsub("'", "'") end function Native.merge(existing, managed) -- Never replace equipped/crew tags owned by EmojiHandler. Only replace our entries. local result, seen = {}, {} for _, entry in ipairs(existing) do if type(entry) == "table" and entry.dexDashboardSource ~= "TagStyles-v1" then table.insert(result, entry) if type(entry.tag) == "string" then seen[entry.tag .. "\0" .. tostring(entry.style)] = true end end end for _, entry in ipairs(managed) do local key = entry.tag .. "\0" .. entry.style if not seen[key] then table.insert(result, entry); seen[key] = true end end return result end return Native ]=]}, {parent = "ServerScriptService", name = "DexDashboardBridge", class = "Script", source = [=[-- Uses the game's Tags attribute + CustomNameTag renderer. Creates no nametag UI. local Players = game:GetService("Players") local RS = game:GetService("ReplicatedStorage") local SSS = game:GetService("ServerScriptService") local Http = game:GetService("HttpService") local TextService = game:GetService("TextService") local Native = require(SSS:WaitForChild("DexNativeTags")) local TagStyles = require(RS:WaitForChild("TagStyles")) local styleNames = Native.catalog(TagStyles) local Http = game:GetService("HttpService") -- The nametag renderer runs on the client, so custom styles have to reach the client. The -- server keeps the definitions for scope checks and republishes them here; DexTagEffects -- builds the real style tables from this value and injects them into the shared TagStyles table. local customBridge = RS:FindFirstChild("DexCustomStyles") if not customBridge then customBridge = Instance.new("StringValue"); customBridge.Name = "DexCustomStyles"; customBridge.Parent = RS end assert(customBridge:IsA("StringValue"), "ReplicatedStorage.DexCustomStyles must be a StringValue.") local customDefs, customRevision = {}, -1 local function applyCustomRegistry(registry) if registry.revision == customRevision then return false end local byKey = {} for _, def in ipairs(registry.styles) do byKey[def.key] = def end customDefs, customRevision = byKey, registry.revision customBridge.Value = Http:JSONEncode(registry.styles) return true end -- Native TagStyles always wins a name clash; custom keys are namespaced so it cannot happen. local function styleFor(key) if type(TagStyles[key]) == "table" then return {native = true} end local def = customDefs[key] if def then return {crewGroupId = def.crewGroupId} end return nil end -- A crew style only applies to a tag that targets that crew's group. The website enforces this -- too; re-checking here means a forged datastore write cannot widen a style's audience. local function styleUsable(style, rule) if not style then return false end if not style.crewGroupId then return true end return rule.targetType == "group" and rule.targetId == style.crewGroupId end local store = game:GetService("DataStoreService"):GetDataStore("DexDashboard_v1") local current = Native.empty() local remote = RS:FindFirstChild("DexDashboard") or Instance.new("RemoteFunction") assert(remote:IsA("RemoteFunction"), "DexDashboard must be a RemoteFunction.") remote.Name = "DexDashboard" remote.Parent = RS local requests, generations, states, filterCache = {}, {}, {}, {} local filterCount = 0 -- GetRankInGroupAsync is a yielding web call. Without this cache every rule targeting a group -- costs one call per player per refresh, which throttles hard once the registry grows. local rankCache = {} local RankTtl = 300 local function isAdmin(player) local gate = SSS:FindFirstChild("DexDashboardIsAdmin") if not gate or not gate:IsA("BindableFunction") then return false end local ok, allowed = pcall(function() return gate:Invoke(player) end) return ok and allowed == true end local function freshRegistry() local options = Instance.new("DataStoreGetOptions") options.UseCache = false return Native.registry(store:GetAsync(Native.RegistryKey, options)) end local function filteredText(rule) if type(rule.authorUserId) ~= "number" or rule.authorUserId <= 0 then return nil end local key = tostring(rule.authorUserId) .. ":" .. rule.text if filterCache[key] then return filterCache[key] end local ok, safe = pcall(function() return TextService:FilterStringAsync(rule.text, rule.authorUserId):GetNonChatStringForBroadcastAsync() end) if not ok then return nil end -- Never render unfiltered website text. if filterCount >= 200 then table.clear(filterCache); filterCount = 0 end filterCache[key] = safe filterCount += 1 return safe end local function groupRank(player, groupId) local cache = rankCache[player.UserId] if not cache then cache = {}; rankCache[player.UserId] = cache end local entry = cache[groupId] if entry and os.clock() - entry.at < RankTtl then return entry.rank end local ok, rank = pcall(function() return player:GetRankInGroupAsync(groupId) end) if not ok or type(rank) ~= "number" then return nil end -- Leave stale/absent rather than guessing. cache[groupId] = {rank = rank, at = os.clock()} return rank end local function apply(player) local state = states[player] if not state or player.Character ~= state.character or not state.character.Parent then return end local character = state.character local encoded = character:GetAttribute("Tags") local existing = {} if encoded ~= nil then local ok, decoded = pcall(function() return Http:JSONDecode(encoded) end) if not ok or type(decoded) ~= "table" then return end -- Preserve malformed native data for its owner to fix. existing = decoded end local merged = Native.merge(existing, state.tags) local nextValue = #merged > 0 and Http:JSONEncode(merged) or nil if nextValue ~= encoded then state.lastWritten = nextValue; character:SetAttribute("Tags", nextValue) end if #merged > 0 and not character:HasTag("CustomNameTag") then character:AddTag("CustomNameTag") end end local function render(player) generations[player] = (generations[player] or 0) + 1 local generation = generations[player] local state = states[player] if not state then return end local active, groupIds = {}, {} for _, rule in ipairs(current.tags) do if rule.enabled and styleUsable(styleFor(rule.style), rule) then table.insert(active, rule) if rule.targetType == "group" then local groupId = tonumber(rule.targetId) if groupId then groupIds[groupId] = true end end end end local ranks = {} for groupId in pairs(groupIds) do ranks[groupId] = groupRank(player, groupId) if generations[player] ~= generation or states[player] ~= state then return end end local matches = {} for _, rule in ipairs(active) do local eligible if rule.targetType == "group" then local rank = ranks[tonumber(rule.targetId)] eligible = rank ~= nil and rank >= math.max(1, rule.rank) else eligible = rule.targetId == tostring(player.UserId) end if eligible then table.insert(matches, rule) end end table.sort(matches, function(a, b) if a.priority == b.priority then return a.id < b.id end return a.priority > b.priority end) local tags = {} for _, rule in ipairs(matches) do if #tags >= 8 then break end local safe = filteredText(rule) if safe then local escaped = Native.escape(safe) table.insert(tags, {tag = rule.bold and ("" .. escaped .. "") or escaped, style = rule.style, dexDashboardSource = "TagStyles-v1", dexDashboardId = rule.id}) end end if generations[player] ~= generation or states[player] ~= state or player.Character ~= state.character then return end state.tags = tags apply(player) end local function refreshPlayers() for _, player in ipairs(Players:GetPlayers()) do task.spawn(render, player) end end remote.OnServerInvoke = function(player, action, payload) local now = os.clock() if now - (requests[player.UserId] or -10) < 1 then return {ok = false, error = "Please wait a moment."} end requests[player.UserId] = now local allowed = isAdmin(player) if action == "status" then return {ok = true, admin = allowed, native = true} end if not allowed then return {ok = false, error = "Staff access required."} end local ok, result = pcall(function() if action == "list" then current = freshRegistry(); return current end assert(action == "save" or action == "delete", "Unknown operation.") assert(type(payload) == "table" and type(payload.revision) == "number" and payload.revision >= 0 and payload.revision % 1 == 0, "Invalid revision.") local rule, removeId if action == "save" then rule = Native.validate(payload.tag) local chosen = styleFor(rule.style) assert(chosen, "Choose an existing style.") assert(styleUsable(chosen, rule), "That crew style only applies to its own crew's group tags.") if rule.id == "" then rule.id = Http:GenerateGUID(false) end rule.authorUserId = player.UserId assert(filteredText(rule), "Roblox text filtering is unavailable. Try again shortly.") else assert(type(payload.id) == "string" and #payload.id <= 64, "Invalid tag ID.") removeId = payload.id end local conflict local saved = store:UpdateAsync(Native.RegistryKey, function(old) old = Native.registry(old) conflict = nil if old.revision ~= payload.revision then conflict = "Tags changed in another editor. Reload first."; return nil end local tags, found = {}, false for _, existing in ipairs(old.tags) do if existing.id == (rule and rule.id or removeId) then found = true; if rule then table.insert(tags, rule) end else table.insert(tags, existing) end end if rule and not found then table.insert(tags, rule) end if not rule and not found then conflict = "Tag not found. Reload first."; return nil end if #tags > Native.MaxTags then conflict = "The " .. Native.MaxTags .. "-tag limit has been reached."; return nil end local audit = {{at = DateTime.now():ToIsoDate(), actor = "roblox:" .. player.UserId, action = action, target = rule and (rule.targetType .. ":" .. rule.targetId) or removeId}} for i = 1, math.min(#old.audit, 99) do table.insert(audit, old.audit[i]) end return {version = 2, revision = old.revision + 1, tags = tags, audit = audit} end) assert(not conflict, conflict) assert(saved, "Save did not complete; reload before trying again.") current = Native.registry(saved) refreshPlayers() return current end) if not ok then warn("[DexNativeTags] Staff operation failed:", result); return {ok = false, error = tostring(result):sub(1, 240)} end return {ok = true, registry = result, styles = styleNames, native = true} end local function bindCharacter(player, character) local previous = states[player] if previous and previous.connection then previous.connection:Disconnect() end local state = {character = character, tags = {}, queued = false} states[player] = state state.connection = character:GetAttributeChangedSignal("Tags"):Connect(function() if state.queued or character:GetAttribute("Tags") == state.lastWritten then return end state.queued = true task.defer(function() state.queued = false; if states[player] == state then apply(player) end end) end) -- Only the retired bridge's specifically named billboard is removed. task.spawn(function() local head = character:WaitForChild("Head", 15) if head then local old = head:FindFirstChild("DexManagedTags"); if old and old:IsA("BillboardGui") then old:Destroy() end end if states[player] == state then render(player) end end) end local function bind(player) player.CharacterAdded:Connect(function(character) bindCharacter(player, character) end) if player.Character then bindCharacter(player, player.Character) end end Players.PlayerAdded:Connect(bind) Players.PlayerRemoving:Connect(function(player) local state = states[player] if state and state.connection then state.connection:Disconnect() end states[player], generations[player] = nil, nil requests[player.UserId], rankCache[player.UserId] = nil, nil end) for _, player in ipairs(Players:GetPlayers()) do bind(player) end task.spawn(function() while true do local ok, result = pcall(freshRegistry) local customChanged = false local customOk, customResult = pcall(function() local options = Instance.new("DataStoreGetOptions") options.UseCache = false return Native.customRegistry(store:GetAsync(Native.CustomKey, options)) end) if customOk then customChanged = applyCustomRegistry(customResult) else warn("[DexNativeTags] Custom styles unavailable; retaining the last valid set.") end if ok then local changed = result.revision ~= current.revision current = result if changed or customChanged then refreshPlayers() end elseif customChanged then refreshPlayers() else warn("[DexNativeTags] Registry unavailable; retaining the last valid rules.") end local synced, syncError = pcall(function() store:UpdateAsync(Native.CatalogKey, function(old) if type(old) == "table" and tonumber(old.updatedAt) and os.time() - old.updatedAt < 45 then return nil end return {version = 1, source = "ReplicatedStorage.TagStyles", styles = styleNames, updatedAt = os.time()} end) store:UpdateAsync(Native.BridgeKey, function(old) if type(old) == "table" and tonumber(old.updatedAt) and os.time() - old.updatedAt < 45 then return nil end return {version = 2, tagSystem = "ReplicatedStorage.TagStyles", updatedAt = os.time(), placeId = tostring(game.PlaceId), universeId = tostring(game.GameId), tagRevision = current.revision, styleCount = #styleNames, customStyleRevision = customRevision, registryOk = ok} end) end) if not synced then warn("[DexNativeTags] Website sync failed:", syncError) end task.wait(60) end end) ]=]}, {parent = "StarterPlayer/StarterPlayerScripts", name = "DexTagPanel", class = "LocalScript", source = [=[-- Native TagStyles editor. Y toggles; there is no corner shortcut or U menu. local Players = game:GetService("Players") local RS = game:GetService("ReplicatedStorage") local UIS = game:GetService("UserInputService") local player = Players.LocalPlayer local remote = RS:WaitForChild("DexDashboard") local styles = require(RS:WaitForChild("TagStyles")) local gui = Instance.new("ScreenGui") gui.Name = "DexTagPanel" gui.ResetOnSpawn = false gui.Enabled = false gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling gui.Parent = player:WaitForChild("PlayerGui") local function make(class, props, parent) local object = Instance.new(class) for key, value in pairs(props) do object[key] = value end object.Parent = parent return object end local root = make("Frame", {Size = UDim2.fromOffset(780, 550), Position = UDim2.fromScale(.5, .5), AnchorPoint = Vector2.new(.5, .5), BackgroundColor3 = Color3.fromRGB(18, 18, 20), BorderSizePixel = 0}, gui) make("UICorner", {CornerRadius = UDim.new(0, 16)}, root) local scale = make("UIScale", {}, root) local function resize() local camera = workspace.CurrentCamera if camera then scale.Scale = math.min(1, (camera.ViewportSize.X - 24) / 780, (camera.ViewportSize.Y - 24) / 550) end end resize() local function label(text, x, y, w, h, parent) return make("TextLabel", {Text = text, Position = UDim2.fromOffset(x, y), Size = UDim2.fromOffset(w, h), BackgroundTransparency = 1, TextColor3 = Color3.fromRGB(235, 235, 238), Font = Enum.Font.Gotham, TextSize = 14, TextXAlignment = Enum.TextXAlignment.Left, TextWrapped = true}, parent or root) end local function button(text, x, y, w, action, parent) local b = make("TextButton", {Text = text, Position = UDim2.fromOffset(x, y), Size = UDim2.fromOffset(w, 34), BackgroundColor3 = Color3.fromRGB(42, 42, 46), TextColor3 = Color3.new(1, 1, 1), Font = Enum.Font.GothamMedium, TextSize = 13, BorderSizePixel = 0}, parent or root) make("UICorner", {CornerRadius = UDim.new(0, 7)}, b) b.Activated:Connect(action) return b end local function input(title, x, y, w, initial) label(title, x, y, w, 20) local box = make("TextBox", {Position = UDim2.fromOffset(x, y + 23), Size = UDim2.fromOffset(w, 34), Text = initial, ClearTextOnFocus = false, BackgroundColor3 = Color3.fromRGB(29, 29, 32), TextColor3 = Color3.new(1, 1, 1), Font = Enum.Font.Gotham, TextSize = 14, BorderSizePixel = 0}, root) make("UICorner", {CornerRadius = UDim.new(0, 6)}, box) return box end local title = label("TAG PANEL", 24, 16, 500, 32) title.Font = Enum.Font.GothamBold title.TextSize = 23 label("Your game's TagStyles. Your existing nametag renderer.", 24, 50, 690, 24) button("Close", 680, 22, 76, function() gui.Enabled = false end) local status = label("Loading…", 24, 495, 732, 42) status.TextColor3 = Color3.fromRGB(190, 190, 199) local target = input("Roblox ID", 24, 130, 208, "") local tagText = input("Tag text (24 characters)", 250, 130, 282, "DEX") local rank = input("Minimum group rank", 24, 199, 208, "1") local priority = input("Priority (0–100)", 250, 199, 130, "0") local preview = label("Choose a style", 24, 305, 508, 52) preview.TextSize = 26 preview.Font = Enum.Font.GothamBold preview.TextXAlignment = Enum.TextXAlignment.Center preview.RichText = false local chosen = label("Style: none selected", 24, 365, 508, 28) local styleSearch = input("Search native styles", 550, 82, 206, "") local styleList = make("ScrollingFrame", {Position = UDim2.fromOffset(550, 147), Size = UDim2.fromOffset(206, 337), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 4, AutomaticCanvasSize = Enum.AutomaticSize.Y, CanvasSize = UDim2.new()}, root) make("UIListLayout", {Padding = UDim.new(0, 5)}, styleList) local savedList = make("ScrollingFrame", {Position = UDim2.fromOffset(24, 397), Size = UDim2.fromOffset(508, 48), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 4, AutomaticCanvasSize = Enum.AutomaticSize.X, CanvasSize = UDim2.new()}, root) make("UIListLayout", {FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 5)}, savedList) local loaded = false local registry, draft, busy, confirmDelete = {revision = 0, tags = {}}, {}, false, nil local cleanup = {} local typeButton, boldButton, enabledButton local function previewTag() for _, fn in ipairs(cleanup) do pcall(fn) end table.clear(cleanup) for _, child in ipairs(preview:GetChildren()) do child:Destroy() end preview.Text = "[" .. tagText.Text .. "]" preview.Font = draft.bold and Enum.Font.GothamBold or Enum.Font.Gotham chosen.Text = "Style: " .. (draft.style ~= "" and draft.style or "none selected") local style = styles[draft.style] if not style then return end if style.gradientAnimation then local ok, destroy = pcall(style.gradientAnimation, preview) if ok and type(destroy) == "function" then table.insert(cleanup, destroy) end end for _, child in pairs(style.tagChilds or {}) do local clone = child:Clone() clone.Parent = preview local handler = clone:FindFirstChild("_handler") if handler then local ok, destroy = pcall(function() return require(handler)(preview) end) if ok and type(destroy) == "function" then table.insert(cleanup, destroy) end end end end local function edit(rule) draft = table.clone(rule or {id = "", targetType = "group", targetId = "", rank = 1, text = "DEX", style = "", bold = true, priority = 0, enabled = true}) target.Text, tagText.Text, rank.Text, priority.Text = draft.targetId, draft.text, tostring(draft.rank), tostring(draft.priority) typeButton.Text = draft.targetType == "group" and "Apply to: group" or "Apply to: player" boldButton.Text = draft.bold and "Bold: on" or "Bold: off" enabledButton.Text = draft.enabled and "Enabled: on" or "Enabled: off" confirmDelete = nil previewTag() end typeButton = button("Apply to: group", 24, 90, 208, function() draft.targetType = draft.targetType == "group" and "player" or "group"; typeButton.Text = "Apply to: " .. draft.targetType end) boldButton = button("Bold: on", 398, 221, 134, function() draft.bold = not draft.bold; boldButton.Text = draft.bold and "Bold: on" or "Bold: off"; previewTag() end) enabledButton = button("Enabled: on", 250, 90, 130, function() draft.enabled = not draft.enabled; enabledButton.Text = draft.enabled and "Enabled: on" or "Enabled: off" end) local function showSaved() for _, child in ipairs(savedList:GetChildren()) do if child:IsA("TextButton") then child:Destroy() end end for _, rule in ipairs(registry.tags) do button(rule.text, 0, 0, 120, function() edit(rule) end, savedList) end end local function request(action, payload) if busy then return end busy = true status.Text = "Working…" local ok, result = pcall(function() return remote:InvokeServer(action, payload) end) busy = false if not ok or not result or not result.ok then status.Text = result and type(result) == "table" and result.error or "Connection failed. Try again."; return end registry = result.registry loaded = true showSaved() status.Text = action == "list" and "Loaded native tags. Select a saved tag to edit it." or "Saved. Other updated servers sync about every 60 seconds." if action ~= "list" then edit(nil) end end button("Save", 24, 453, 116, function() if not loaded then status.Text = "Reload the current tag list first."; return end if draft.style == "" then status.Text = "Choose a native TagStyles style first."; return end local rule = table.clone(draft) rule.text, rule.targetId, rule.rank, rule.priority = tagText.Text, target.Text, tonumber(rank.Text), tonumber(priority.Text) request("save", {revision = registry.revision, tag = rule}) end) button("New", 154, 453, 116, function() edit(nil) end) button("Reload", 284, 453, 116, function() request("list") end) button("Delete", 414, 453, 118, function() if draft.id == "" then status.Text = "Select a saved tag first."; return end if confirmDelete ~= draft.id then confirmDelete = draft.id; status.Text = "Press Delete again to remove this website-managed tag. Equipped and crew tags are unchanged."; return end request("delete", {revision = registry.revision, id = draft.id}) end) local names = {} for name, style in pairs(styles) do if type(name) == "string" and type(style) == "table" then table.insert(names, name) end end table.sort(names) for index, name in ipairs(names) do local b = button(name, 0, 0, 196, function() draft.style = name; previewTag() end, styleList) b.Name = name b.LayoutOrder = index end styleSearch:GetPropertyChangedSignal("Text"):Connect(function() for _, child in ipairs(styleList:GetChildren()) do if child:IsA("TextButton") then child.Visible = child.Name:lower():find(styleSearch.Text:lower(), 1, true) ~= nil end end end) tagText.FocusLost:Connect(previewTag) edit(nil) UIS.InputBegan:Connect(function(inputObject, processed) if processed or UIS:GetFocusedTextBox() or inputObject.KeyCode ~= Enum.KeyCode.Y then return end if gui.Enabled then gui.Enabled = false; return end if busy then return end busy = true local ok, result = pcall(function() return remote:InvokeServer("status") end) busy = false if not ok or not result or not result.admin or not result.native then return end resize() gui.Enabled = true task.wait(1.05) request("list") end) ]=]}, {parent = "StarterPlayer/StarterPlayerScripts", name = "DexTagEffects", class = "LocalScript", source = [=[-- Dex Hood custom tag styles (client). Builds website-authored styles into the same table -- shape PlayerScriptsLoader already consumes, then injects them into the shared TagStyles -- table. Native styles are never touched; custom keys are namespaced with custom/. local RS = game:GetService("ReplicatedStorage") local TweenService = game:GetService("TweenService") local Http = game:GetService("HttpService") local tagStyles = require(RS:WaitForChild("TagStyles")) local bridge = RS:WaitForChild("DexCustomStyles") local templates = Instance.new("Folder") templates.Name = "DexCustomStyleTemplates" templates.Parent = script local MOTIONS = {sweep = true, pulse = true, static = true} local function build(def) assert(type(def) == "table", "Invalid style.") local key = def.key assert(type(key) == "string" and key:sub(1, 7) == "custom/", "Invalid style key.") assert(type(def.stops) == "table" and #def.stops >= 2 and #def.stops <= 8, "Invalid gradient.") assert(type(def.speed) == "number" and def.speed >= 0.2 and def.speed <= 10, "Invalid speed.") assert(MOTIONS[def.motion], "Invalid motion.") local keypoints, previous = {}, -1 for _, stop in ipairs(def.stops) do assert(type(stop) == "table" and type(stop.position) == "number" and stop.position > previous, "Invalid gradient position.") assert(type(stop.color) == "string" and stop.color:match("^#%x%x%x%x%x%x$"), "Invalid gradient color.") table.insert(keypoints, ColorSequenceKeypoint.new(stop.position, Color3.fromHex(stop.color))) previous = stop.position end local sequence = ColorSequence.new(keypoints) local icon -- Unapproved icons are skipped: an in-moderation asset renders as a blank square. if def.iconState == "approved" and type(def.iconAssetId) == "string" and def.iconAssetId:match("^[1-9]%d*$") then icon = Instance.new("ImageLabel") icon.Name = "DexStyleIcon" icon.BackgroundTransparency = 1 icon.Image = "rbxassetid://" .. def.iconAssetId icon.Size = UDim2.fromScale(1, 1) icon.SizeConstraint = Enum.SizeConstraint.RelativeYY icon.ScaleType = Enum.ScaleType.Fit icon.Parent = templates end return { dexCustom = true, tagChilds = icon and {icon} or nil, gradientAnimation = function(label) local gradient = Instance.new("UIGradient") gradient.Color = sequence gradient.Parent = label if def.motion == "static" then return function() gradient:Destroy() end end local info = TweenInfo.new(def.speed, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut, -1, true) local goal = def.motion == "pulse" and {Rotation = 25} or {Offset = Vector2.new(1, 0)} local tween = TweenService:Create(gradient, info, goal) tween:Play() return function() tween:Cancel(); gradient:Destroy() end end, } end local function sync() local raw = bridge.Value local defs = {} if raw ~= "" then local ok, decoded = pcall(function() return Http:JSONDecode(raw) end) if not ok or type(decoded) ~= "table" then warn("[DexTagEffects] Unreadable custom style data; keeping the last valid set.") return end defs = decoded end local built = {} for _, def in ipairs(defs) do local ok, style = pcall(build, def) if ok then built[def.key] = style else warn("[DexTagEffects] Skipped custom style:", style) end end -- Replace only our own entries. A native style is never removed or overwritten. for key, value in pairs(tagStyles) do if type(value) == "table" and value.dexCustom and not built[key] then tagStyles[key] = nil end end templates:ClearAllChildren() for key, style in pairs(built) do if type(tagStyles[key]) ~= "table" or tagStyles[key].dexCustom then if style.tagChilds then style.tagChilds[1].Parent = templates end tagStyles[key] = style end end end sync() bridge:GetPropertyChangedSignal("Value"):Connect(sync) ]=]}, } -- Preflight every target before writing any source. Native modules stay untouched. local changed = false for _, change in ipairs(changes) do change.parentObject = resolve(change.parent) change.object = change.parentObject:FindFirstChild(change.name) change.existed = change.object ~= nil if change.object then assert(change.object.ClassName == change.class, "Unexpected class at " .. change.name) change.before = editor:GetEditorSource(change.object) local known = change.name == "DexDashboardBridge" and change.before:find("DexDashboard_v1", 1, true) or change.name == "DexTagPanel" and change.before:find("DexDashboard", 1, true) or change.name == "DexTagEffects" and (change.before:find("DexAnimationSpeed", 1, true) or change.before:find("Retired.", 1, true) or change.before:find("DexCustomStyles", 1, true)) or change.name == "DexNativeTags" and change.before:find("tags-native-v1", 1, true) assert(known, "Unrecognized source at " .. change.name .. "; nothing changed.") if change.object:IsA("BaseScript") then change.disabled = change.object.Disabled end else change.before = "" end change.afterDisabled = false if change.before ~= change.source or (change.class ~= "ModuleScript" and change.disabled ~= change.afterDisabled) then changed = true end end if not changed then print("DEX_NATIVE_TAGS_ALREADY_INSTALLED"); return end local storage = game:GetService("ServerStorage") local backups = storage:FindFirstChild("DexNativeTagBackups") assert(not backups or backups:IsA("Folder"), "Backup path is not a Folder; nothing changed.") if not backups then backups = Instance.new("Folder"); backups.Name = "DexNativeTagBackups"; backups.Parent = storage end local backup = Instance.new("Folder") backup.Name = "Backup_" .. game:GetService("HttpService"):GenerateGUID(false) backup:SetAttribute("CreatedAt", DateTime.now().UnixTimestampMillis) backup.Parent = backups for index, change in ipairs(changes) do local entry = Instance.new("Folder") entry.Name = tostring(index) entry:SetAttribute("Path", change.parent .. "/" .. change.name) entry:SetAttribute("Class", change.class) entry:SetAttribute("Existed", change.existed) entry:SetAttribute("Disabled", change.disabled) entry:SetAttribute("AfterDisabled", change.afterDisabled) for _, pair in ipairs({{"Before", change.before}, {"After", change.source}}) do local value = Instance.new("StringValue"); value.Name = pair[1]; value.Value = pair[2]; value.Parent = entry end entry.Parent = backup end local touched = {} local ok, problem = pcall(function() for _, change in ipairs(changes) do if not change.object then change.object = Instance.new(change.class) change.object.Name = change.name if change.object:IsA("BaseScript") then change.object.Disabled = true end change.object.Parent = change.parentObject end table.insert(touched, change) editor:UpdateSourceAsync(change.object, function(current) assert(current == change.before, "Source changed during installation: " .. change.name) return change.source end) if change.object:IsA("BaseScript") then change.object.Disabled = change.afterDisabled end assert(editor:GetEditorSource(change.object) == change.source, "Source verification failed: " .. change.name) end end) if not ok then local failures = {} for index = #touched, 1, -1 do local change = touched[index] local restored, reason = pcall(function() if not change.existed then change.object:Destroy(); return end editor:UpdateSourceAsync(change.object, function(current) assert(current == change.source or current == change.before, "Concurrent edit; preserving it") return change.before end) if change.object:IsA("BaseScript") then change.object.Disabled = change.disabled end end) if not restored then table.insert(failures, change.name .. ": " .. tostring(reason)) end end error("DEX_NATIVE_TAGS_ABORT: " .. tostring(problem) .. (#failures > 0 and ("; manual restore needed: " .. table.concat(failures, "; ")) or "; original scripts restored.") .. " Backup: " .. backup:GetFullName()) end backup:SetAttribute("Complete", true) game:GetService("ChangeHistoryService"):SetWaypoint("Dex Hood native TagStyles integration") print("DEX_NATIVE_TAGS_INSTALLED: Native TagStyles and PlayerScriptsLoader were not edited. Custom billboard/effects retired.") print("Backup: " .. backup:GetFullName()) print("Save and test this place, publish to Dex Hood, then join a NEW server to sync the website style list. Existing servers retain their old scripts until restarted.")