SnugSwap is a helper library for FFXI Windower GearSwap that lets you describe your gear setups in a more declarative and readable way.
Instead of writing a lot of if spell.type == ... then equip(sets.foo) logic, you mostly just:
- Define your modes (DD / tank / hybrid / etc)
- Define your gear sets
- Attach simple conditions (status, spell type, weather, pet out, etc)
- Let SnugSwap handle
precast,midcast,aftercast,status_change, and pet events for you
-
Simple default sets for:
- weapons
- idle
- engaged
- fast cast
- weapon skills
- precast / midcast
-
Modes (e.g.
style = dd / tank / hybrid / magic / layered) with easy in-game toggles -
Condition-based sets using a friendly chain style:
- only when Engaged
- only when weather is Light
- only when you have a pet out
- only when HP% < X, TP > Y, etc.
-
Mode-aware gear via
gearset_from_mode -
Built-in support for:
gs c toggle <mode>gs c set <mode> <value>gs c list modes- Utility sets like
warp,nexus,speed - e.g:
gs c util warpwill equip the warp utility set
-
Put
snugswap.luain your GearSwaplibsfolder, for example:Windower/ addons/ GearSwap/ libs/ snugswap.lua -
In your job lua, include SnugSwap at the top:
include('snugswap')
-
Wire SnugSwap into GearSwap
SnugSwap can automatically install all standard GearSwap callbacks for you. This is the easiest and most common setup.
include('snugswap.lua')
function get_sets()
-- define all your sets and modes here using snugs
end
-- Automatically install any missing GearSwap hooks
snugs:wire_all()wire_all() will:
- Create
precast,midcast,aftercast,status_change,self_command,pet_change,pet_midcast,pet_aftercast - Wrap your existing
get_sets()so SnugSwap can initialize modes afterwards - Not overwrite any hook you have already defined
This is typically all you need.
If you prefer to control the callbacks yourself, you can forward the events to SnugSwap manually:
function get_sets()
-- define all your sets here using snugs
end
function precast(spell) snugs:do_precast(spell) end
function midcast(spell) snugs:do_midcast(spell) end
function aftercast(spell) snugs:do_aftercast(spell) end
function status_change(n, o) snugs:do_status_change(n, o) end
function self_command(cmd) snugs:do_self_command(cmd) end
function pet_change(pet, gain) snugs:do_pet_change(pet, gain) end
function pet_midcast(spell) snugs:do_pet_midcast(spell) end
function pet_aftercast(spell) snugs:do_pet_aftercast(spell) endUse this approach if you want to insert your own logic before or after SnugSwap executes.
A mode is just a named setting you can switch in-game (like “playstyle”):
snugs:add_mode("style", {
initial_value = "hybrid",
description = "Playstyle",
cycle_values = {"dd", "tank", "hybrid", "magic", "layered"},
})In game, you can toggle it with:
//gs c toggle style
//gs c list modes
Or set it directly:
//gs c set style tank
You can tell SnugSwap what your “default” sets are:
snugs:default_weaponset({
main="Naegling",
sub="Blurred Shield +1",
})
snugs:default_idle({
-- your idle set here
})
snugs:default_engaged({
-- your engaged set here
})These are what you’ll fall back to whenever SnugSwap “resets” you after actions or status changes.
Instead of writing if style == 'tank' then ..., you can map mode values to sets:
snugs:default_weaponset(
gearset_from_mode("style", {
dd = {main="Naegling", sub="Blurred Shield +1"},
tank = {main="Nixxer", sub="Srivatsa"},
hybrid = {main="Naegling", sub="Srivatsa"},
magic = {main="Nixxer", sub="Aegis"},
layered= {main="Naegling", sub="Srivatsa"},
})
)Now when you do:
//gs c toggle style
your weapon set automatically changes to match the mode.
You can do the same for idle/engaged:
snugs:default_idle(
gearset_from_mode("style", {
dd = dd_idle_set,
tank = tank_idle_set,
hybrid = hybrid_idle_set,
magic = magic_idle_set,
layered= layered_idle_set,
})
)The gearset() function lets you attach conditions to a set in a readable way.
Example: only use this set while Engaged:
local sird_set = gearset({
head="Souv. Schaller +1",
ammo="Staunch Tathlum +1",
neck="Moonlight Necklace",
body="Chev. Cuirass +2",
}):when():status("Engaged")More examples:
local obi_set = gearset({
waist="Hachirin-no-Obi",
})
:when():weather("Light")
:or_instead( when():day("Light") )
:or_instead( when():buff("Aurorastorm") )local petdt_set = gearset({
-- pet DT gear
}):when():has_pet(true):otherwise(base_set)local low_hp_panic_set = gearset({
body="Max DT Body",
}):when():hpp_less_than(30)local high_tp_set = gearset({
waist="TP Bonus Belt",
}):when():tp_greater_than(2500)You can stack more than one condition by chaining them onto :when():
gearset(my_set):when()
:status("Engaged")
:tp_greater_than(1500)You’ll often want to start from a base set and override a few slots.
Use :and_combine(...) to layer sets:
local tank_set = { ... }
local magic_tank_set = gearset(tank_set):and_combine(gearset({
neck="Warder's Charm +1",
}))You can chain multiple:
gearset(tank_set)
:and_combine(gearset(extra_enmity))
:and_combine(gearset(extra_SIRD))For simple raw GearSwap-style combining, you can still use set_combine:
local dd_set = set_combine(base_set, {
head="Sakpata's Helm",
-- etc
})SnugSwap fully supports both.
SnugSwap has helper methods for precast/midcast/WS sets so you don’t have to write your own if spell.type ==... logic.
snugs:default_fastcast({
head="Carmine Mask +1",
feet="Carmine Greaves +1",
left_ring="Prolix Ring",
left_ear="Loquac. Earring",
})
snugs:fastcast("Enhancing Magic", {
waist="Siegel Sash",
})default_fastcast= global fast cast setfastcast("Enhancing Magic", ...)= extra fast cast for that skill/spell
Define for specific spell names or skill types:
snugs:precast("Reward", {
ammo="Pet Food Theta",
})
snugs:midcast("Reward", {
-- midcast Reward set
})
-- or both at once:
snugs:premidcast("Call Beast", {
feet="Gleti's Boots",
hands="Ankusa Gloves +3",
})Apply to multiple spells at once:
local cure_spells = {"Cure", "Cure II", "Cure III", "Cure IV", "Curaga", "Curaga II", "Curaga III"}
snugs:midcast_all(cure_spells,
gearset(enmity_set)
:and_combine(cure_set)
:and_combine(cure_obi_set)
:and_combine(sird_set)
)Available helpers:
snugs:precast(name, set)snugs:midcast(name, set)snugs:premidcast(name, set)(both)snugs:precast_all({names}, set)snugs:midcast_all({names}, set)snugs:premidcast_all({names}, set)
You can also define skill-based sets ("Enhancing Magic", "Divine Magic", etc.) that apply to all spells of that skill if no spell-name-specific set is defined.
Set a default WS set:
snugs:default_weaponskill(base_ws_set)Override per WS:
snugs:weaponskill("Savage Blade", savage_blade_set)
snugs:weaponskill_all({"Mistral Axe", "Savage Blade"}, some_multi_ws_set)If a WS has no specific set defined, SnugSwap uses default_weaponskill.
Modes can also carry their own gear mappings (e.g. jug pet ammo):
snugs:add_mode("jug", {
initial_value = "sheep",
description = "Current Jug Pet",
gearset_mappings = {
sheep = {ammo="Lyrical Broth"},
diremite = {ammo="Crackling Broth"},
slime = {ammo="Putrescent Broth"},
-- etc...
}
})Then you can pull from that mode straight into your sets:
snugs:premidcast_all({"Call Beast", "Bestial Loyalty"},
gearset({
feet="Gleti's Boots",
hands="Ankusa Gloves +3",
}):and_combine(gearset_from_mode("jug"))
)Changing your jug mode in game (//gs c toggle jug) auto-updates the ammo used.
You can define general “utility” sets you call via gs c util or short commands.
snugs:util("warp", {
ring1="Warp Ring",
})
snugs:util("speed", {
legs="Carmine Cuisses +1",
})//gs c util myset (command syntax for any custom utility set)
//gs c warp (special shortcut)
//gs c speed (special shortcut)
//gs c nexus (if you define snugs:util("nexus", {...}))
SnugSwap handles equipping these sets for you.
All of these go through self_command and are handled by SnugSwap.
//gs c list modes
//gs c toggle <modeName>
//gs c set <modeName> <value>
Examples:
//gs c toggle style
//gs c set style tank
//gs c set debug true
//gs c set trace true
//gs c util <name>
//gs c warp
//gs c nexus
//gs c speed
If you defined named weapon sets with snugs:weaponset("name", set), you can cycle them:
//gs c cycle weapon
SnugSwap remembers the current weapon set and equips it on status resets.
If you want to see what SnugSwap is doing:
//gs c set debug true
Enables extra debug output (green text).
//gs c set trace true
Prints a full breakdown of the final gear set that’s being equipped, slot by slot. Very handy when you’re stacking multiple gearset(...):and_combine(...) layers and want to see the result.
Turn them off with:
//gs c set debug false
//gs c set trace false
If you want to go further, SnugSwap also provides some advanced tools:
- Custom predicates extensions with
snugs:extend_predicate(...). - Predicates directly:
when(),where(fn), etc. for custom logic. - Selectors:
use(set, condition)withchoose_from(...)/choose_all(...)to pick or combine sets based on conditions.
You don’t need these for normal usage, but they’re there if you want very complex “equip this only if X, Y, and Z are true” behavior.
SnugSwap supports middleware that runs whenever it builds a context (ctx) for an action phase. Middleware can inspect and mutate ctx before SnugSwap selects and evaluates gear.
This is useful for:
- adding extra lookup keys (spell families, aliases, categories)
- attaching metadata to
ctx.meta - building reusable “plugins” across jobs
Signature:
snugs:register_middleware(phase, function(ctx) ... end, opts)| Name | Description |
|---|---|
phase |
One of: "any", "precast", "midcast", "aftercast", "pet_midcast", "pet_aftercast", "pet_change", "status_change", "self_command" |
fn |
Middleware function (ctx) |
opts |
Optional: { priority = number, name = "string" } (higher priority runs first) |
Middleware runs in this order:
- all "any" middleware
- middleware for the specific phase
Each list is sorted by priority descending.
ctx is a SnugContext instance. Common fields:
ctx.phase -- string: current phase name
ctx.spell -- spell/action table (present for cast phases)
ctx.meta -- free-form table for middleware data
ctx.lookups -- ordered list of lookup keys SnugSwap uses to resolve setsHelper methods:
ctx:prepend_lookup(key) -- prepend a lookup key (deduped)
ctx:add_lookup(key) -- add a lookup key (deduped)
ctx:has_lookup(key) -- check if a key exists
ctx:set_meta(k, v)
ctx:get_meta(k)SnugSwap automatically seeds lookup keys from spells (when present), in this order:
- spell.english
- spell.type
- spell.skill (if present)
Then your middleware may add additional keys.
Example: adding spell family keys
snugs:register_middleware("any", function(ctx)
if not ctx.spell then return end
-- Ninjutsu: "Katon: Ichi" -> "AllKaton"
if ctx.spell.type == "Ninjutsu" then
local base = ctx.spell.english:match("^(.-):%s*(Ichi|Ni|San)$")
if base then ctx:add_lookup("All" .. base) end
return
end
-- Magic: "Cure IV" -> "AllCure"
local base = ctx.spell.english:match("^(.-)%s*(I|II|III|IV|V|VI)$")
if base then ctx:add_lookup("All" .. base) end
end, { name = "spell_families", priority = 0 })Now you can trigger use of family gearsets:
sets.midcast.AllCure = { ... }
sets.midcast.AllThunder = { ... }
sets.midcast.AllKaton = { ... }snugs:register_middleware("precast", function(ctx)
if ctx.spell and ctx.spell.skill == "Elemental Magic" then
ctx.meta.is_nuke = true
end
end)Later in your own gearsets or additional middleware:
if ctx.meta.is_nuke then
-- modify ctx.lookups, alter behavior, etc.
endSnugSwap middleware can influence what set gets picked by adding/prepending lookup keys.
snugs:register_middleware("precast", function(ctx)
if ctx.spell and ctx.spell.english == "Warp" and buffactive["Sneak"] then
-- Highest priority lookup key
ctx:prepend_lookup("NoWarpWhileSneak")
end
end, { name = "warp_sneak_guard", priority = 10 })Middleware provides a safe and extensible layer before SnugSwap selects gear or performs default logic. It enables:
- Compatibility layers without rewriting your profile
- Complex naming / grouping logic
- Optional behavior overrides
- Reusable “plugins” shared between jobs
Most users will never need it—but it unlocks deep customization for power users and framework authors.
SnugSwap exposes a small registry so you can add your own predicate extensions that plug into the predicate DSL. Predicate extensions are chainable methods you can register and then use on when() or gearset(...), like :hpp_less_than(30) or :my_custom_rule(...).
An extension factory returns either function(ctx) -> boolean or another Predicate (which will then be combined with and_also).
Signature:
snugs:extend_predicate(name, factory, opts)-
name– the method name you’ll call on a predicate or gearset (e.g."tp_below"→:tp_below(1000)). -
factory– a function that receives whatever arguments you pass (e.g.1000) and returns:- either a function
ctx -> boolean, or - another Predicate instance.
- either a function
-
opts– optional table:{ override = true }– replace any existing predicate with the same name.{ wrap = true }– wrap/extend the existing predicate instead of replacing it.
Example: register a simple tp_below predicate:
snugs:extend_predicate("tp_below", function(limit)
return function(ctx)
-- ctx is whatever SnugSwap passes; player is the global
return player.tp and player.tp < limit
end
end)Usage with when():
local low_tp_condition = when():tp_below(1000)Usage directly on a gearset:
snugs:engaged(
gearset({
legs="Some TP Pants",
}):tp_below(1000) -- will only apply when TP < 1000
)Suppose SnugSwap already has a buff predicate but you want to change its behavior:
snugs:extend_predicate("buff", function(buff_name)
return function(ctx)
-- Custom interpretation, maybe aliased buffs or grouped buffs
return buffactive[buff_name] or buffactive["Divine Seal"]
end
end, { override = true })Now anywhere you use:
when():buff("Haste")it will use your custom logic instead of the built-in one.
wrap = true lets you wrap an existing predicate factory.
snugs:extend_predicate("hpp_less_than", function(old_factory)
return function(limit)
local base = old_factory(limit) -- function(ctx) -> boolean
return function(ctx)
local ok = base(ctx)
if ok and snugs:is_debugging() then
windower.add_to_chat(
123,
"[snugs]: debug: hpp_less_than(" ..
tostring(limit) ..
") at " ..
tostring(player.hpp) ..
"%"
)
end
return ok
end
end
end, { wrap = true })You can construct predicates directly, without touching gearsets:
local p = when()
:status("Engaged")
:buff("Haste")
:tp_greater_than(1000)Then attach them to a gearset via the GearsetWithOptions DSL:
snugs:engaged(
gearset({ head="Nyame Helm" })
:and_also(p) -- combine with an existing predicate
)Or define everything inline:
snugs:engaged(
gearset({ head="Nyame Helm" })
:status("Engaged")
:buff("Haste")
:tp_greater_than(1000)
)Selectors let you build conditional sets and then either:
- choose the first matching one, or
- combine all matching ones together.
use creates a “selector”:
local regen_selector = use(
gearset({ body="Twilight Mail" }),
when():hpp_less_than(75)
)
local dt_selector = use(
gearset({ ring1="Defending Ring" }),
when():status("Engaged")
)Each selector is basically:
“If this condition is true, use this set.”
choose_from evaluates selectors in order and returns the first set whose condition is true.
local engaged_choice = choose_from(
use(gearset({ body="High DPS Mail" }), when():mode_is("style", "dd")),
use(gearset({ body="Tanky Mail" }), when():mode_is("style", "tank"))
)
snugs:default_engaged(engaged_choice)Behavior:
- If
style == "dd"and its condition matches → equips only the DD set. - Else if
style == "tank"→ equips only the tank set. - If none match → empty set (
{}).
Order matters: the first true selector wins.
choose_all evaluates all selectors and set-combines all sets whose conditions are true.
local engaged_combo = choose_all(
-- base engaged set
use(gearset({ body="Nyame Mail" }), when():status("Engaged")),
-- add DT pieces when in tank mode
use(gearset({ ring1="Defending Ring" }), when():mode_is("style", "tank")),
-- add emergency gear if HP low
use(gearset({ feet="Valorous Greaves" }), when():hpp_less_than(40))
)
snugs:default_engaged(engaged_combo)Behavior:
- All selectors whose conditions are true will be evaluated.
- Their sets are merged (via
set_combine) into a single final set. - Later selectors win on slot conflicts (standard
set_combinebehavior).
This is perfect for layering situational gear: base set + mode-based overlay + emergency overlay, etc.
Here’s a tiny, self-contained sketch:
include('snugswap')
function get_sets()
snugs:add_mode("style", {
initial_value = "dd",
description = "Playstyle",
cycle_values = {"dd", "tank"},
})
local dd_engaged = {
head="Nyame Helm",
body="Nyame Mail",
hands="Nyame Gauntlets",
legs="Nyame Flanchard",
feet="Nyame Sollerets",
}
local tank_engaged = {
head="Chev. Armet +2",
body="Chev. Cuirass +2",
hands="Chev. Gauntlets +2",
legs="Chev. Cuisses +2",
feet="Chev. Sabatons +2",
}
snugs:default_engaged(
gearset_from_mode("style", {
dd = dd_engaged,
tank = tank_engaged,
})
)
snugs:default_idle(dd_engaged)
snugs:default_weaponset({main="Naegling", sub="Blurred Shield +1"})
snugs:default_fastcast({
head="Carmine Mask +1",
})
snugs:midcast_all(
{"Cure", "Cure II"},
gearset({
legs="Souv. Diechlings +1",
}):when():status("Engaged")
)
end
-- Either use auto-wiring:
-- snugs:wire_all()
-- Or manual wiring if you want custom hook logic:
function precast(spell) snugs:do_precast(spell) end
function midcast(spell) snugs:do_midcast(spell) end
function aftercast(spell) snugs:do_aftercast(spell) end
function status_change(n, o) snugs:do_status_change(n, o) end
function self_command(cmd) snugs:do_self_command(cmd) end
function pet_change(p, g) snugs:do_pet_change(p, g) end
function pet_midcast(spell) snugs:do_pet_midcast(spell) end
function pet_aftercast(spell) snugs:do_pet_aftercast(spell) endThis project is released under the CC0 Public Domain Dedication. You may fork, adapt, reuse, or ignore it freely.
"Make art not law" - Nina Paley

