WARPGATE
WARP for 2025-07-16

Custom Jobs Guide

Everything you need to add custom jobs to a 2025-07-16 Ragnarok Online client using the Enable Custom Jobs (Reforged) WARP patch.

Patch Enable Custom Jobs (Reforged)Client 2025-07-16 · 175220998Example job EXAMPLE_JOB · 4435Updated 2026-09-02
Requirements
WARP 07-16, rAthena, sprite editor (optional)
Job ID Range
4351 to 9999 (MaxJob 10000)
Template
See examples/CustomJobs/

1 Choose a Job ID

Pick an unused ID between 4351 and 9999. The patch fills the sprite tables from 4351 up to MaxJob, which is 10000 and exclusive, so an ID below 4351 gets no body path and 10000 itself is never populated. 4351 to 4360 belong to Gravity's 2025 jobs (Druid, Karnos, Alitea and their baby and riding forms), so start at 4361 unless you mean to override one. The ID must match on both client and server.

Warning: IDs below 4345 collide with stock jobs (baby, transcendent, extended, Doram). 4345 is also the number every CustomJobs hook tests: at or above it the display-name, weapon and equip-view hooks take the custom path instead of the stock tables. The client still runs its own convert_job_to_trans_or_baby subtraction on IDs in that range, so the patch writes the sprite tables at index (job ID minus 3950) to match. Anything below 4345 is unsafe, and the usable range still starts at 4351: see above.

For this guide, we'll use EXAMPLE_JOB with ID 4435. A complete working example is included in the WARP; see the Inputs/Luafiles514/ folder and examples/CustomJobs/data/ for all the files.

Tip: start from the working example. The shipped EXAMPLE_JOB (ID 4435) already renders correctly out of the box. The most reliable way to build your own job is to copy that working set, then change one thing at a time (the ID, then the name, then the sprites), testing after each change. Rewriting several files at once, or pointing a value at another job, is the usual reason a job that "should work" ends up stuck on the Novice sprite.

2 Client-Side Lua Files

The CustomJobs patch uses 7 separate .lua files in data/luafiles514/lua files/JobInfo/. Each file handles one aspect of the custom job. Your custom entries are loaded at runtime; stock job data is loaded from the GRF automatically.

Loose data or GRF: These files can live in the data/ folder as loose files or packed into a GRF (listed in DATA.INI). The patch loads them via the client's file system, which checks both.
Exact filenames matter. The patch loads these seven files by name: PCIds.lua, PCPaths.lua, PCPals.lua, PCHands.lua, PCImfs.lua, PCNames.lua, and PCFuncs.lua (each is also tried with a .lub extension). A misspelled name such as PCName.lua (missing the "s") is never loaded, and the file the patch did want is reported as missing: with the default Popup error display you get a CustomJobs Error box naming PCNames. Match the spelling and capitalization exactly.

PCIds.lua: Job ID Definition

Defines your custom job's numeric ID. This is the master ID that all other files reference. It must match the server-side JOB_ enum value exactly. Each custom job needs a unique ID in the range 4351 to 9999.

Warning: 4351 to 4360 are Gravity's 2025 jobs (Druid, Karnos, Alitea, their baby forms and their riding forms). The patch bakes a weapon-sprite row for 4351 to 4355 and hardcodes the client's baby scale for 4352 and 4354, so a custom job on one of those IDs picks up Gravity's hands and, at 4352 or 4354, renders shrunk. Start at 4361 or higher unless you mean to override an official job.
PCIds.lua
PCIds = PCIds or {}
PCIds.EXAMPLE_JOB = 4435
Note: The PCIds = PCIds or {} guard ensures the table exists even if the stock GRF hasn't loaded yet. All 7 files use this pattern; do not remove it.
Stock job constants are auto-populated. The shipped PCIds.lua contains a do-block that fills in every standard job ID (NOVICE, SWORDMAN, LORD_KNIGHT, LORD_KNIGHT2, PALADIN2, RUNE_KNIGHT, all baby / transcendent / 4th-class variants, mounted *2 forms) before your custom IDs are added. That means in PCNames.lua / PCPaths.lua / etc. you can write PCNames[PCIds.LORD_KNIGHT2] = "..." instead of the raw PCNames[4014] = "..."; both forms work. Aliases are provided for both rAthena (LORD_KNIGHT2, RUNE_KNIGHT_T) and legacy WARP (LORD_MOUNT, RUNE_KNIGHT_H) naming conventions.

PCNames.lua: Display Name

Sets the name shown in the BasicInfo window, character select screen, and party window. This is the human-readable name players see in the UI. Without this entry, your custom job will display as "Poring".

PCNames.lua
PCNames = PCNames or {}
PCNames[PCIds.EXAMPLE_JOB] = "Example Job"

-- You can also rename stock jobs. Both forms work, pick whichever:
PCNames[PCIds.WARLOCK]      = "Dark Wizard"     -- by name
PCNames[4055]               = "Dark Wizard"     -- by raw ID (same thing)
PCNames[PCIds.LORD_KNIGHT2] = "Holy Knight"     -- mounted variant
PCNames[4014]               = "Holy Knight"     -- same thing, raw
Stock jobs are auto-populated: PCFuncs.lua fills PCNames for all standard jobs from the GRF's PCJobNameTable at load time, and a second pass copies each base name onto its peco / dragon / mado mounted variant (e.g. Lord Knight 4008 → LORD_KNIGHT2 4014). You only need explicit PCNames[] entries for custom jobs or to rename a stock job.

Gender-specific names (optional)

Not wired on this client. PCNames_M and PCNames_F are read by GetPCNameOvrd in PCFuncs.lua, and the 2025-07-16 patch never calls it: both name hooks read PCNames[jobid] straight out of the table. Male and female forms of a custom job share one name here. Leave the tables out unless you are porting to a client that calls the helper.

Multi-language names (optional)

Not wired on this client. The LT_N convention belongs to GetValFromTbl in PCFuncs.lua, which the 2025-07-16 patch does not call. Every table here is read by raw job ID, so a nested PCNames["LT_1"] entry never loads. Put the one name you want in PCNames[jobid].

PCPaths.lua: Sprite Folder Name

Maps your job ID to the body sprite folder/file name. This value must match your .spr and .act filenames exactly; it is case-sensitive and ASCII only (no Korean characters). The client appends the CP949 gender suffix automatically.

Example: if you set "EXAMPLE_JOB", the client looks for EXAMPLE_JOB_³².spr (male) and EXAMPLE_JOB_¿©.spr (female) in the body sprite directory.

PCPaths.lua
PCPaths = PCPaths or {}
PCPaths[PCIds.EXAMPLE_JOB] = "EXAMPLE_JOB"
This is a plain text string, not a link to another job. A very common mistake is trying to "clone" a stock job by writing PCPaths[PCIds.EXAMPLE_JOB] = PCPaths[PCIds.LORD_KNIGHT]. That does not work. Stock jobs load their sprite paths from the GRF, so they are never stored in this Lua table; PCPaths[PCIds.LORD_KNIGHT] reads back as nil, your job ends up with no body path, and it falls back to the Novice sprite. Always give PCPaths a literal folder-name string, then place matching sprite files (Step 3). To reuse another job's look, copy that job's .spr and .act out of the GRF and rename them to your path, including the costume_1/ copies.

PCPals.lua: Palette Prefix

Controls which palette files are used when players change hair/clothes colors via @dye or the stylist NPC. Palette files follow the pattern jobname_gender_N.pal where N is the color index. For most custom jobs, inherit from an existing job (Novice is the safest default). If you create your own palette files, set this to your custom prefix string. This is a filename prefix, not a folder. The .pal files sit flat in the body palette folder, data/palette/¸ö/ (, Body), as <prefix>_<gender>_<N>.pal. A folder named after the prefix is never opened.

You setThe client opens
PCPals[PCIds.EXAMPLE_JOB] = "LORD_KNIGHT_EXAMPLE"data/palette/¸ö/LORD_KNIGHT_EXAMPLE_³²_1.pal (male, colour 1), data/palette/¸ö/LORD_KNIGHT_EXAMPLE_¿©_1.pal (female), and so on for every colour index your server offers
PCPals[PCIds.EXAMPLE_JOB] = (PCPals[PCIds.NOVICE] or "")the Novice palettes already in the GRF; nothing to add
a costume palettedata/palette/¸ö/costume_1/LORD_KNIGHT_EXAMPLE_³²_1_1.pal, that is the same name with the costume slot appended, inside a costume_<N> folder
Copying another job's palettes: the copies keep the old prefix. Rename them, ±â»ç_³²_1.pal (Knight, male, 1) becomes LORD_KNIGHT_EXAMPLE_³²_1.pal, and leave them in data/palette/¸ö/. The same rule does not apply to PCPaths and PCHands, which are folder names; PCPals is the odd one out.
PCPals.lua
PCPals = PCPals or {}
-- Inherit Novice's palettes. The (or "") fallback handles cases
-- where PCPals hasn't been populated from GRF yet.
PCPals[PCIds.EXAMPLE_JOB] = (PCPals[PCIds.NOVICE] or "")

PCHands.lua: Weapon Sprite Folder

Points your job at a weapon/hand sprite folder, so that each weapon type (sword, axe, bow, and so on) draws the right sprite when your job equips it. This table is read when you apply the patch, not by the client: WARP bakes every quoted row into the patched exe, after the five official rows Gravity ships for 2025 (Druid 4351 with its baby form 4352, Karnos 4353 with its baby form 4354, and Alitea 4355; the ids and folder names are Gravity's), so re-apply WARP after editing it. Only a quoted string counts as a row; an inheritance form such as (PCHands[PCIds.NOVICE] or "") means "stock hands" and adds nothing. The WARP log lists the rows it baked and the lines it skipped.

PCHands.lua
PCHands = PCHands or {}
PCHands[PCIds.EXAMPLE_JOB] = (PCHands[PCIds.NOVICE] or "")   -- stock (Novice) hands: the example ships no weapon sprites
-- PCHands[PCIds.EXAMPLE_JOB] = "example_job"                -- a quoted folder name is a baked row. Only when you ship
--                                                            -- data\sprite\<race>\example_job\example_job_<gender>_<weapon>.spr
Where weapon sprites go. This trips people up because it is not the same shape as the body. Measured from the client's own data:
  • Body: data\sprite\<race>\<body>\<gender>\<name>_<gender>.spr, a flat filename.
  • Weapon: data\sprite\<race>\<name>\<name>_<gender>_<weapon>.spr, inside a folder named after the job, with the name repeated in the filename.
Stock jobs follow exactly this: the Knight's weapons live in a Knight folder, one file per weapon type. If your weapon files sit beside the body instead of in their own folder, nothing will draw when you equip a weapon.
Custom jobs use the Novice equip view. The client's GetEquipViewIndexByJob switch only knows standard jobs, and a custom job falls into its default where only weapon type 1 draws. The patch answers that lookup with the Novice index for every ID at or above 4345 and every weapon type, so equipment and weapons on a custom job draw at Novice offsets, not at the offsets of whatever job you copied.
Why apply time. On the 2025-07-16 client the runtime Lua lookup for this table crashes, so the patch bakes the rows into the exe instead (2026-08-27, and data-driven from your PCHands.lua since 2026-09-02). Nothing changes for you except which file and when: edit Inputs/Luafiles514/Lua Files/JobInfo/PCHands.lua inside your WARP folder, then apply WARP. Editing the copy already in your client's data/ folder bakes nothing. The row's key must be a raw number or a PCIds.NAME that Inputs' PCIds.lua defines on its own line as a number; a key written against any other table (for example PCMounts.X) is not recognised as a row and is not even listed among the skipped lines in the log. Those five rows are built in; a literal row for one of their ids overrides it. The value has to be a quoted string and nothing else on that line: a trailing -- comment or a semicolon makes the row read as an inheritance form, so it bakes nothing, and a console or profile build says nothing about it. Put any comment on its own line above.

PCImfs.lua: IMF Animation Reference

Links your job to its IMF (animation timing) files. IMF files control head positioning and animation frame timing. Keeps the seven-file set complete. The client builds the IMF filename from the sprite name plus the gender suffix, so the inheritance form below is all you need here; what your job cannot do without is the pair of .imf files in data/imf/ (Step 4). Without those, your character renders without a head.

PCImfs.lua
PCImfs = PCImfs or {}
PCImfs[PCIds.EXAMPLE_JOB] = (PCImfs[PCIds.NOVICE] or "")
About the (... or "") guard used in PCPals / PCHands / PCImfs (PCHands is read at apply time, see its section above): the fallback handles the case where the parent table hasn't been merged with stock GRF data at the moment your custom file is loaded. Without the guard, indexing a missing parent returns nil, which the binary's lookup treats as an invalid string. Note that a stock parent such as PCHands[PCIds.LORD_KNIGHT] is never in this Lua table either, so it is always nil: the (... or "") collapses it to an empty string, which the client reads as "use the default", not as a copy of that job's weapon or palette.

PCFuncs.lua: Core Helper Functions

Defines the classic helper functions (ReqPCPath, ReqPCJobName, GetValFromTbl, GetHalter and friends). The 2025-07-16 patch never calls them: it reads the tables straight through the Lua C API, so they are kept for compatibility with other clients, not because this one needs them. What earns this file its place are the three passes at the bottom: it auto-populates display names for stock jobs from the GRF, mirrors those names onto their peco/dragon/mado mounted variants, and wraps InitSkillTreeView so JOB_SKILL_TIER overrides (used by multi-tier custom job chains) take effect. Do not remove or rename any functions in this file; deploy it unedited. You should not need to edit this file; add your custom job data to the other 6 files instead. The WARP Inputs include a working copy.

One exception aside, no re-WARP needed: the JobInfo Lua files are read at runtime, so a new job is an edit plus a client restart. PCHands.lua is the exception: its rows are baked into the exe when you apply the patch, so run WARP again after touching that one file.

Inheritance Tables (optional)

Warning: the inherit tables belong to the Lua helper set, not to the 2025-07-16 binary patch. This build resolves PCPaths, PCPals, PCNames, PCHands and PCRace with a direct table read and never consults PCPathInheritTbl or the MapPC* functions, so an inherit entry has no effect here. Give every tier its own literal value.

An alternative to the (PCPals[PCIds.NOVICE] or "") snapshot fallback. PCFuncs.lua exposes one inheritance table per lookup type, queried by the matching MapPC* function whenever the primary lookup misses:

Inherit tableResolves through
PCNameInheritTblMapPCJobName(jobid, lt)
PCPathInheritTblMapPCPath(jobid, lt)
PCImfInheritTblMapPCImf(jobid, lt)
PCPalInheritTblMapPCPal(jobid, lt)
PCHandInheritTblMapPCHandPath(jobid, lt)

Each entry maps a job ID to its parent job ID. When a primary lookup (PCPaths[jobid]) returns nil, the helper asks the matching Map* function, which reads the inherit table to find the parent and re-runs the full lookup against it; so chains traverse correctly even multiple levels deep.

PCFuncs.lua (or any of the data files)
PCPathInheritTbl = PCPathInheritTbl or {}
PCImfInheritTbl  = PCImfInheritTbl  or {}
PCPalInheritTbl  = PCPalInheritTbl  or {}
PCHandInheritTbl = PCHandInheritTbl or {}
PCNameInheritTbl = PCNameInheritTbl or {}

-- Tier 3 inherits from tier 2, tier 2 inherits from tier 1
PCPathInheritTbl[PCIds.EXAMPLE_JOB_3RD] = PCIds.EXAMPLE_JOB_2ND
PCPathInheritTbl[PCIds.EXAMPLE_JOB_2ND] = PCIds.EXAMPLE_JOB
-- (Repeat for any of the other four tables you want to chain.)

Inherit tables are best for genuine tier-style relationships where the parent's value should be used verbatim. The (or "") snapshot is fine for one-off "use Novice" fallbacks where the parent's data is already loaded.

Skills, baby forms, mounts and Doram

Skill Tree Files (optional)

If your custom job has skills, you also need two files in data/luafiles514/lua files/skillinfoz/:

jobinheritlist.lub: Skill Inheritance

Defines which parent job your custom job inherits skills from. Add your job's ID to the JOBID table and a JOB_INHERIT_LIST entry:

jobinheritlist.lub
-- In the JOBID table:
JOBID.JT_EXAMPLE_JOB = 4435

-- In the JOB_INHERIT_LIST table:
[JOBID.JT_EXAMPLE_JOB] = JOBID.JT_NOVICE   -- inherits from Novice

skilltreeview.lub: Skill Tree Tab Name

Controls the tab label shown in the skill tree window. Without this, your job's tab will say "Etc":

skilltreeview.lub
-- In the SKILL_TREEVIEW_FOR_JOB table:
[JOBID.JT_EXAMPLE_JOB] = JOBID.JT_EXAMPLE_JOB

-- After the table, set the tab name:
JobSkillTab.ChangeSkillTabName(JOBID.JT_EXAMPLE_JOB, "Example Job")
Important: Unlike the JobInfo .lua files above, these skillinfoz files must use the .lub extension; the stock client loader only recognizes .lub for this path. Place them in your data/ folder or in a GRF with higher priority than the stock GRF in DATA.INI. These files replace the stock version entirely, so you must include ALL existing entries plus your additions. The llchrisll Translation Project includes these files and can be used as a base.
Naming convention, JT_ vs JOB_: The skillinfoz .lub files use the JOBID.JT_NAME form, while the server-side rAthena code in Step 7 uses JOB_NAME. Both refer to the same numeric ID; the prefixes are just convention; JOBID is the client's Lua jobid table, JOB_ is the server's C++ enum. Keep the bare NAME portion identical on both sides to avoid confusion.

Multi-Tier Job Chains (advanced)

For job systems with multiple tiers (like Night Watch: Gunslinger → Rebellion → Night Watch), the skill tree tabs are controlled by tiers. The C++ assigns tier 0 to the root job (whose parent is Novice). Tiers 0 and 1 merge on the first tab. Override tiers for higher classes via JOB_SKILL_TIER in PCIds.lua:

PCIds.lua
-- Example: 3-tier job chain (Base → 2nd → 3rd)
-- The base class is NOT listed, C++ naturally gives it tier 0
-- (same as Novice), so they merge on the first tab.
JOB_SKILL_TIER = JOB_SKILL_TIER or {}
JOB_SKILL_TIER[PCIds.EXAMPLE_JOB_2ND] = 1   -- 2nd tab
JOB_SKILL_TIER[PCIds.EXAMPLE_JOB_3RD] = 2   -- 3rd tab

The InitSkillTreeView wrapper in PCFuncs.lua reads this table at runtime. Use consecutive tier numbers (0, 1, 2); skipping a tier creates an empty tab. Tab names are set via ChangeSkillTabName in skilltreeview.lub (same convention as Night Watch: base class name first, then "2nd", "3rd"):

skilltreeview.lub
-- 3 named tabs + auto "Etc" tab (matches Night Watch: "Gunslinger", "2nd", "3rd")
JobSkillTab.ChangeSkillTabName(JOBID.JT_EXAMPLE_BASE, "Base Job", "2nd", "3rd")
JobSkillTab.ChangeSkillTabName(JOBID.JT_EXAMPLE_2ND,  "Base Job", "2nd", "3rd")
JobSkillTab.ChangeSkillTabName(JOBID.JT_EXAMPLE_3RD,  "Base Job", "2nd", "3rd")

The inheritance chain in jobinheritlist.lub defines the tab grouping:

jobinheritlist.lub
[JOBID.JT_EXAMPLE_BASE] = JOBID.JT_NOVICE        -- Novice+Base merge on tab 1
[JOBID.JT_EXAMPLE_2ND]  = JOBID.JT_EXAMPLE_BASE   -- tab 2
[JOBID.JT_EXAMPLE_3RD]  = JOBID.JT_EXAMPLE_2ND    -- tab 3

Baby Class Support (limited)

Baby variants of custom jobs need sprite scaling entries to render at 75% size (like stock baby classes). Add to PCIds.lua:

PCIds.lua
-- Baby class sprite scaling (shrinks baby jobs to 0.75x)
-- Scales[1] = "3F400000" = 0.75 in IEEE 754 float
Scales = Scales or { "3F400000", "3F51EB85", "3F4CCCCD" }
Shrink_Map = Shrink_Map or {}
Shrink_Map[PCIds.EXAMPLE_JOB_B] = Scales[1]
On this client the scale is hardcoded, not read from Lua. The patch shrinks only the two official baby IDs 4352 and 4354, plus the stock baby ranges; a Shrink_Map entry for your own job is ignored, so a custom baby renders at full size. Keep the entry for forward compatibility and expect full-size sprites until the table is read at runtime.

The baby job ID must also be defined in PCIds.lua and have its own sprite files, PCPaths, PCPals, etc.

Mount Support

Warning: unverified on the 2025-07-16 client. Halter_Table and GetHalter belong to the Lua helper set; the patch itself never reads them and never registers the callback, so custom mounts are untested here. Stock mounts work because the client resolves them itself. Treat everything below as the shape to aim for, not a supported feature.
Stock mounts work automatically: Knight, Lord Knight, Crusader, Paladin, Rune Knight (5 dragon colors), Royal Guard, Mechanic, Ranger, and the 4th-class jobs already have Halter_Table + sprite/name data populated from the GRF. You do not need to add anything for stock job mounts; this section only applies to custom jobs.

To enable the Boarding Halter (item 12622) for a custom job, define the mount ID and halter mapping in PCIds.lua:

PCIds.lua
-- Mount definitions
PCMounts = PCMounts or {}
PCMounts.EXAMPLE_JOB_RIDING = 4437   -- riding sprite job ID

Halter_Table = Halter_Table or {}
Halter_Table[PCIds.EXAMPLE_JOB] = PCMounts.EXAMPLE_JOB_RIDING

When the Boarding Halter is used, rAthena applies SC_ALL_RIDING and the client calls GetHalter(jobid); the returned riding ID becomes the actor's effective job for sprite/name resolution. That means the riding ID needs its own complete entry set across all five Lua data tables; treat it as a separate custom job:

FileRequired entry
PCPaths.luaPCPaths[PCMounts.EXAMPLE_JOB_RIDING] = "EXAMPLE_RIDING"
PCNames.luaPCNames[PCMounts.EXAMPLE_JOB_RIDING] = "Example Job" (usually same as base)
PCPals.luaPCPals[PCMounts.EXAMPLE_JOB_RIDING] = (PCPals[PCIds.NOVICE] or "")
PCHands.luaPCHands[PCMounts.EXAMPLE_JOB_RIDING] = (PCHands[PCIds.NOVICE] or "") (an inheritance form: stock hands, adds no row)
PCImfs.luaPCImfs[PCMounts.EXAMPLE_JOB_RIDING] = (PCImfs[PCIds.NOVICE] or "")

Plus the actual asset files for the riding variant: body .spr/.act, costume_1/ .spr/.act, IMF (male + female), and icon BMP (icon_jobs_RIDING_ID.bmp + _die.bmp); all the same artifacts a base custom job needs (Steps 3-5). If you support multiple mount types (like Rune Knight's five dragon colors), each gets its own ID and full entry set, and Halter_Table can map the same base job to multiple mounts via additional Lua logic.

Doram Race

Stock Doram works automatically: the client hardcodes the Summoner block (4217 to 4221) plus Spirit Handler (4308) and 4315 into its race check. This section only applies to custom jobs that should render as Doram instead of Human.

The client routes sprite/offset/scale/nameplate lookups through is_doram_job_id, which by default only knows the stock Summoner IDs. CustomJobs adds a Lua-table override so any custom job can opt in. Define PCRace in any JobInfo Lua file:

PCIds.lua (or PCNames.lua / a new PCRace.lua)
PCRace = PCRace or {}
PCRace[PCIds.MY_DORAM_NOVICE] = "doram"
PCRace[PCIds.MY_DORAM_2NDCLASS] = "doram"

Any non-nil value enables the Doram path; "doram" reads well and reserves room for future race values. Once set, the job's body sprite loads from data/sprite/도람족/몸통/ (CP949 µµ¶÷Á·/¸öÅë/) instead of 인간족/몸통/, and item offsets, sprite scale, shadow position, and nameplate Y-offset all follow the Doram code paths the stock Summoner uses.

Your sprite files for Doram custom jobs go in the Doram folder, not the human folder shown in Step 3:

FileClient Path
Male base spritedata/sprite/µµ¶÷Á·/¸öÅë/³²/EXAMPLE_DORAM_³².spr
Female base spritedata/sprite/µµ¶÷Á·/¸öÅë/¿©/EXAMPLE_DORAM_¿©.spr

(Same .act, costume_1/, IMF, palette, and icon rules as Step 3, just with µµ¶÷Á· instead of Àΰ£Á· for the body sprites.)

3 Sprite Files

Sprites go in the human race body directory. The Korean folder names are CP949-encoded on disk.

Sprite Files (4 base, 4 costume)

File Client Path
Male base sprite data/sprite/Àΰ£Á·/¸öÅë/³²/EXAMPLE_JOB_³².spr
Male base animation data/sprite/Àΰ£Á·/¸öÅë/³²/EXAMPLE_JOB_³².act
Female base sprite data/sprite/Àΰ£Á·/¸öÅë/¿©/EXAMPLE_JOB_¿©.spr
Female base animation data/sprite/Àΰ£Á·/¸öÅë/¿©/EXAMPLE_JOB_¿©.act
Male costume sprite data/sprite/Àΰ£Á·/¸öÅë/³²/costume_1/example_job_³²_1.spr
Male costume animation data/sprite/Àΰ£Á·/¸öÅë/³²/costume_1/example_job_³²_1.act
Female costume sprite data/sprite/Àΰ£Á·/¸öÅë/¿©/costume_1/example_job_¿©_1.spr
Female costume animation data/sprite/Àΰ£Á·/¸öÅë/¿©/costume_1/example_job_¿©_1.act
Note: Base sprite filenames use UPPERCASE to match the PCPaths value. Costume filenames use lowercase with a _1 suffix. Base sprites are required. The costume pair is optional on a patched client: if a costume_1/ file is missing, the patch strips the costume folder and the _1 suffix and loads the base sprite instead. Ship them when you want a distinct costume look.

Korean Folder Reference

KoreanEnglishCP949 on disk
인간족Human RaceÀΰ£Á·
몸통Body¸öÅë
Body (the palette folder)¸ö
Male³²
Female¿©
유저인터페이스User InterfaceÀ¯ÀúÀÎÅÍÆäÀ̽º

Using Existing Sprites as Placeholders

If you don't have custom sprites yet, copy an existing job's sprites and rename them. examples/CustomJobs/data/sprite/ includes Novice sprites you can use as a starting point.

4 IMF Files

One IMF per gender, carrying the head offsets for every frame of every action. Place them in data/imf/:

data/imf/example_job_³².imf    (male, ³² is CP949 for 남)
data/imf/example_job_¿©.imf    (female, ¿© is CP949 for 여)

Copy from any existing job. examples/CustomJobs/data/imf/ includes working IMF files.

Critical: Without IMF files, your character will render without a head in-game. This is the most commonly missed step.
The client will not tell you. The patch jumps over the three "Resource File Loading fail" chat messages, because a missing IMF fired them and spammed chat. A resource that fails to register now does so quietly, so check the head by eye and check the IMF filename against your PCPaths value plus the gender suffix. SPR, ACT and PAL failures still report through their own paths.

5 Icon Files

Job icons go in data/texture/À¯ÀúÀÎÅÍÆäÀ̽º/renewalparty/ (À¯ÀúÀÎÅÍÆäÀ̽º is CP949 for 유저인터페이스):

icon_jobs_4435.bmp          (normal icon, 25x25 BMP)
icon_jobs_4435_die.bmp      (dead icon, 25x25 BMP)

Replace 4435 with your actual job ID. Copy from any existing job icon as a placeholder. examples/CustomJobs/data/texture/À¯ÀúÀÎÅÍÆäÀ̽º/renewalparty/ includes the Novice icons.

25x25 is the size the shipped example icons use; copy one of them and repaint it rather than guessing the dimensions.

Critical: Missing icon files will crash the client on the character select screen if a character with that job exists on the account.

6 WARP Patch

In WARP, open the SPECIAL CUSTOMIZATIONS group and tick Enable Custom Jobs (Reforged). MaxJob is hardcoded to 10000, so there is nothing to configure.

Applying the patch asks up to three questions:

Copy Lua Files

Whether to copy the seven Lua files out of WARP's Inputs/ folder for you. Answer Yes and WARP asks for a target folder next, then writes them to data\Luafiles514\Lua Files\JobInfo inside whatever you pick. The default is the folder holding the patched exe, so choose your client folder to use them straight away, or take the default and move them across with the exe.

Error Display ($showFailures)

How Lua loading errors are reported:

OptionBehavior
SilentErrors swallowed; files that fail to load are silently skipped
Log FileErrors written to customjobs_error.log in the client folder
Popup (default)Windows MessageBox with the Lua error details
After this patch: new jobs are a Lua edit and a client restart, not another WARP run. The single exception is PCHands.lua, whose rows are baked in at apply time.

7 Server-Side (rAthena)

Your rAthena server needs to know about the new job: 6 source files, 1 message file and 4 database files, plus an optional skill tree entry. Make every edit, then rebuild the server.

A. Job ID: src/common/mmo.hpp

Add your job to the e_job enum, before JOB_MAX:

mmo.hpp
JOB_EXAMPLE_JOB = 4435,
JOB_MAX,

B. Map ID: src/map/map.hpp

Add a corresponding MAPID entry to the e_mapid enum (before the 2-1 Jobs section):

map.hpp
MAPID_EXAMPLE_JOB,

C. Job Conversion: src/map/pc.cpp

Add entries to both conversion switch statements:

pc.cpp
// In pc_jobid2mapid(), converts job ID to map ID:
case JOB_EXAMPLE_JOB:    return MAPID_EXAMPLE_JOB;

// In pc_mapid2jobid(), converts map ID back to job ID:
case MAPID_EXAMPLE_JOB:  return JOB_EXAMPLE_JOB;

D. Job Validation: src/map/pc.hpp

Update the pcdb_checkid macro so the server recognizes your job ID as valid. Add or extend the range to include your job:

pc.hpp
// In pc.hpp, pcdb_checkid_sub is one long chain of ranges joined by ||
// and every line ends with a backslash. Add yours as another term,
// after the last one and before the closing parenthesis:
||	( (class_) >= JOB_EXAMPLE_JOB && (class_) <= JOB_EXAMPLE_JOB ) \
Tip: If you have multiple custom jobs, use a range: JOB_FIRST_CUSTOM to JOB_LAST_CUSTOM.

E. Script Constants: src/map/script_constants.hpp

Export the job constant so NPC scripts can reference it. Add both entries:

script_constants.hpp
// Near the other job exports:
export_constant(JOB_EXAMPLE_JOB);

// Near the EAJ_ exports:
export_constant2("EAJ_EXAMPLE_JOB",MAPID_EXAMPLE_JOB);

F. Job Name: src/char/inter.cpp

Add a case to the job name function so the char-server can display the name. Pick an unused msg_txt ID below 300, the char-server's message-table size (check conf/msg_conf/char_msg.conf; the stock file stops at 229). An ID of 300 or higher is rejected at load with an "Invalid message ID" warning, and the name comes back as ??.

inter.cpp
case JOB_EXAMPLE_JOB:
    return msg_txt( 230 );  // first free slot

Then add the name string to conf/msg_conf/char_msg.conf:

char_msg.conf
230: Example Job

G. Database Files: db/re/ (or db/pre-re/)

Add your job to four YAML database files. The easiest approach is to add your job to an existing group (e.g., Knight or Novice). Find a Jobs: block and add your job name:

db/re/job_stats.yml
    Jobs:
      Knight: true
      Example_Job: true   # <-- add this line

Do this in all four files:

FilePurpose
job_stats.ymlHP/SP multipliers, weight limit
job_aspd.ymlAttack speed per weapon type
job_basepoints.ymlStat points per level
job_exp.ymlEXP table (base + job)
Critical: Without these DB entries, the map-server will crash on startup with "Job Example_Job does not exist". The YAML job name is the constant name without the JOB_ prefix (here Example_Job); match the capitalization style of the neighboring entries in the same file.

H. Skill Tree (optional): db/import/skill_tree.yml

skill_tree.yml
- Job: Example_Job
  Inherit:
    - Job: Novice
  Tree:
    - Skill: SM_SWORD
      MaxLevel: 10
    # ... add your skills

I. Job Change NPC (example)

npc script
prontera,150,180,5	script	Job Master	2_M_SAGE,{
    if (BaseLevel < 10) {
        mes "Come back at base level 10.";
        close;
    }
    jobchange JOB_EXAMPLE_JOB;
    mes "You are now an Example Job!";
    close;
}

Or use the GM command in-game: @job 4435

How you know it worked. Log in on that character and read four things in order: the Basic Info window shows the name from PCNames.lua, the character has a head, the body is your sprite and not the Novice one, and the party window draws your icon. The first three have their own rows in Troubleshooting below; a missing icon shows up there as a crash on character select.

File Checklist

Check off items as you complete them. Progress is saved in your browser.

Client Side

0 / 16
  • PCIds.lua; Job ID defined
  • PCNames.lua; Display name
  • PCPaths.lua; Sprite path
  • PCPals.lua; Palette name
  • PCHands.lua; Weapon sprites
  • PCImfs.lua; IMF reference
  • PCFuncs.lua; deployed unedited from WARP's Inputs/
  • Male base sprite .spr + .act
  • Female base sprite .spr + .act
  • Male costume_1 sprite .spr + .act
  • Female costume_1 sprite .spr + .act
  • IMF files (male + female)
  • Icon BMP + die variant in renewalparty/
  • jobinheritlist.lub + skilltreeview.lub (if job has skills)
  • Shrink_Map entry in PCIds.lua (if baby variant exists)
  • Halter_Table + PCMounts in PCIds.lua (if mountable)

Server Side

0 / 12
  • mmo.hpp; JOB_ enum entry (before JOB_MAX)
  • map.hpp; MAPID_ enum entry
  • pc.cpp; Both conversion switch cases
  • pc.hpp; pcdb_checkid macro range
  • script_constants.hpp; export_constant + EAJ_ export
  • inter.cpp; Job name msg_txt case
  • char_msg.conf; the name string for that msg ID
  • job_stats.yml; HP/SP/weight entry
  • job_aspd.yml; Attack speed entry
  • job_basepoints.yml; Stat points entry
  • job_exp.yml; EXP table entry
  • skill_tree.yml; Skill tree (optional)

Files in the repository

Two folders in the WARP0716 repository, under examples/CustomJobs/. A WARPGATE install already has them on disk, because the WARP tab installs the whole repository.

The example job

examples/CustomJobs/data/ is a complete working job using job ID 4435 (with baby variant 4436) and Novice placeholder sprites: 31 files, the seven Lua files, both genders' sprites with their costume slots, the four IMFs and the party icons. The folder mirrors the actual data/ directory structure with correct CP949-encoded Korean paths; you can copy the contents directly into your client's data/ folder or pack them into a GRF, add the job on the server (step 7), and it renders. The fastest way to build your own job is to copy this set and rename.

examples/CustomJobs/data/ ├── imf/ │ ├── example_job_³².imf (male) │ ├── example_job_¿©.imf (female) │ ├── example_job_b_³².imf (baby male) │ └── example_job_b_¿©.imf (baby female) ├── luafiles514/lua files/JobInfo/ │ ├── PCIds.lua Job ID + baby ID + Shrink_Map │ ├── PCNames.lua Display names (normal + baby) │ ├── PCPaths.lua Sprite folder names │ ├── PCPals.lua Palette prefixes │ ├── PCHands.lua Weapon sprite folders │ ├── PCImfs.lua IMF references │ └── PCFuncs.lua Core functions (don't edit) ├── sprite/Àΰ£Á·/¸öÅë/ (인간족/몸통 = Human Race/Body) │ ├── ³²/ (남 = Male) │ │ ├── EXAMPLE_JOB_³².spr + .act │ │ ├── EXAMPLE_JOB_B_³².spr + .act (baby) │ │ └── costume_1/ │ │ ├── example_job_³²_1.spr + .act │ │ └── example_job_b_³²_1.spr + .act (baby) │ └── ¿©/ (여 = Female) │ ├── EXAMPLE_JOB_¿©.spr + .act │ ├── EXAMPLE_JOB_B_¿©.spr + .act (baby) │ └── costume_1/ │ ├── example_job_¿©_1.spr + .act │ └── example_job_b_¿©_1.spr + .act (baby) └── texture/À¯ÀúÀÎÅÍÆäÀ̽º/renewalparty/ (유저인터페이스 = UI) ├── icon_jobs_4435.bmp + _die.bmp └── icon_jobs_4436.bmp + _die.bmp (baby)

The WARP Inputs also include this example; enable the CustomJobs patch and these files work out of the box.

The Lua reference

examples/CustomJobs/lua-reference/ holds the seven complete JobInfo tables with every stock job listed: PCIds, PCNames, PCPaths, PCPals, PCHands, PCImfs and PCFuncs. Read them to see how an existing job is written before you add yours, and to check that an ID is free. Nothing in them needs editing.

Troubleshooting

Symptom
Cause
Fix
@dye or the stylist does nothing; the job keeps its default colours
The palette files sit in a folder named after the PCPals prefix, or still carry the prefix of the job they were copied from
Put them flat in data/palette/¸ö/ as <prefix>_<gender>_<N>.pal, renamed to your prefix (Step 2, PCPals)
Weapons are missing, or lie beside the character instead of in the hand
Weapon sprites are not in their own folder named after the PCHands value with the name repeated in the filename, or the exe was patched before 2026-08-27, when the hands lookup never ran
Lay them out as data/sprite/<race>/<name>/<name>_<gender>_<weapon>.spr, edit PCHands.lua, then re-apply the current WARP (Step 2, PCHands)
"Cannot find File" error on login
A missing base sprite, or a PCPaths value with no matching .spr / .act
Add the four base files (both genders, .spr + .act) and check the PCPaths string against the filenames
Character has no head in-game
Missing IMF files
Copy IMF from an existing job and rename
Crash on character select
Missing icon BMP files
Add icon_jobs_ID.bmp and _die.bmp to renewalparty/
Shows "Poring" as job name
PCNames entry missing, or outdated PCFuncs.lua deployed
Custom jobs: add PCNames[PCIds.YOUR_JOB] = "Name" to PCNames.lua. Stock peco/dragon/mado mounts: re-WARP and let it copy fresh Lua files (or sync data/.../JobInfo/PCFuncs.lua from the WARP Inputs/ folder) so the mount-variant auto-populate runs.
Wrong sprite (shows Novice)
PCPaths doesn't match filename
Ensure PCPaths value matches sprite filename (case-sensitive, ASCII)
Job stays Novice after "inheriting" a stock job
PCPaths (or PCHands/PCPals/PCImfs) set to [PCIds.LORD_KNIGHT] or another stock job, which reads back as nil
Give PCPaths a literal folder-name string (e.g. "EXAMPLE_JOB") and add the matching sprite files. Stock jobs are not stored in these Lua tables, so you cannot copy them by reference.
"CustomJobs Error" popup
Lua file has syntax error or can't be found
Read the popup: it names the file and the Lua error. Fix the syntax or the path and restart the client, no re-WARP needed. To get a record instead of a popup, apply the patch again and answer Log File at the Lua Error Display prompt, then read customjobs_error.log in the client folder.
"file not found" for all Lua files
Files not in data/ folder or GRF
Place .lua files in data/luafiles514/lua files/JobInfo/ or pack into a GRF listed in DATA.INI
"Cannot find File" during char creation
Outdated CustomJobs.qjs
Update to latest CustomJobs.qjs from the WARP0716 repo