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 CustomJobs (Reforged) WARP patch.

Requirements
WARP 07-16, rAthena, sprite editor (optional)
Job ID Range
434510000 (hardcoded max)
Template
See the example/ folder

1 Choose a Job ID

Pick an unused ID between 4345 and 10000. The ID must match on both client and server.

Warning: IDs below 4345 conflict with existing rAthena jobs (Summoner, Doram, etc).

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 docs/CustomJobs/example/ for all the files.

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.

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 4345–10000.

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.

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 job will display as "Poring".

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

-- You can also rename stock jobs using their numeric IDs.
-- Stock PCIds constants (NOVICE, SWORDMAN, etc.) are NOT available
-- in this file — use the raw job number instead.
-- PCNames[4055] = "Dark Wizard"     -- renames Warlock
-- PCNames[4054] = "Holy Knight"     -- renames Rune Knight

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"

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.

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

Determines which weapon/hand sprite folder is used when your job equips weapons. Each weapon type (sword, axe, bow, etc.) has separate sprites organized by job. Inheriting from an existing job means your custom job uses that job's weapon animations. For a unique look, create your own weapon sprite folder and set the name here.

PCHands.lua
PCHands = PCHands or {}
PCHands[PCIds.EXAMPLE_JOB] = (PCHands[PCIds.NOVICE] or "")

PCImfs.lua — IMF Animation Reference

Links your job to its IMF (animation timing) files. IMF files control head positioning and animation frame timing. You must also have actual .imf binary files in data/imf/ (see Step 4). This Lua entry tells the client which IMF prefix to look up. Without it, your character will render without a head.

PCImfs.lua
PCImfs = PCImfs or {}
PCImfs[PCIds.EXAMPLE_JOB] = (PCImfs[PCIds.NOVICE] or "")

PCFuncs.lua — Core Helper Functions

Contains the internal helper functions that the CustomJobs binary patch calls at runtime (ReqPCPath, ReqPCJobName, GetValFromTbl, etc.). It also auto-populates display names for stock jobs from the GRF. Do not remove or rename any functions in this file — the binary patch depends on them. 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.

No re-WARP needed: Lua files are loaded at runtime, not baked into the exe at patch time. You can add new custom jobs by editing the Lua files without re-WARPing. Only the initial WARP patch (to enable CustomJobs) is required.

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.

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

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]

Without this, baby characters render at full size. The baby job ID must also be defined in PCIds.lua and have its own sprite files, PCPaths, PCPals, etc.

Mount Support

To enable the Boarding Halter (item 12622) for your 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

The riding job ID needs its own sprite files (body + costume_1), IMFs, palettes, and a PCPaths entry. The Boarding Halter item triggers SC_ALL_RIDING which swaps the sprite to the riding variant.

3 Sprite Files

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

Required Files (8 total)

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. Costume files can be copies of the base sprites — they just need to exist.

Korean Folder Reference

KoreanEnglishCP949 on disk
인간족Human RaceÀΰ£Á·
몸통Body¸öÅë
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. The example/ folder includes Novice sprites you can use as a starting point.

4 IMF Files

IMF files control head positioning and animation timing. 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. The example/ folder includes working IMF files.

Critical: Without IMF files, your character will render without a head in-game. This is the most commonly missed step.

5 Icon Files

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

icon_jobs_4435.bmp          (normal icon, 24x24 BMP)
icon_jobs_4435_die.bmp      (dead icon, 24x24 BMP)

Replace 4435 with your actual job ID. Copy from any existing job icon as a placeholder. The example/ folder includes the Novice icon.

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

Enable the CustomJobs (Reforged) patch in WARP. MaxJob is hardcoded to 10000 — no configuration needed.

When applying the patch, WARP will prompt you for two options:

Copy Lua Files

Whether to copy the Lua files from WARP's Inputs/ folder to the output data/ folder. Choose Yes if you want the example files deployed automatically.

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
GRF support: Custom Lua files work from the loose data/ folder or packed into a GRF listed in DATA.INI. The patch uses the client's native file loader which checks both. No re-WARP needed when adding new jobs — just update the Lua files.

7 Server-Side (rAthena)

Your rAthena server needs to know about the new job. This requires changes to 7 source files and 4 database files. After editing, 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 the pcdb_checkid_sub macro, add a range check:
( (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 (check conf/msg_conf/char_msg.conf):

inter.cpp
case JOB_EXAMPLE_JOB:
    return msg_txt( 833 );  // use an unused msg ID

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

char_msg.conf
833: 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 job name in YAML must match the enum name with JOB_ removed and underscores preserved.

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

File Checklist

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

Client Side

0 / 15
  • 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
  • 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 / 11
  • 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
  • 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)

Example Files

The example/ folder contains a complete working example using job ID 4435 (with baby variant 4436) and Novice placeholder sprites. 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.

example/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.

Troubleshooting

"Cannot find File" error on login
Missing sprite or costume_1 files
Add all 8 sprite files (base + costume, both genders)
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
Add PCNames[PCIds.YOUR_JOB] = "Name" to PCNames.lua
Wrong sprite (shows Novice)
PCPaths doesn't match filename
Ensure PCPaths value matches sprite filename (case-sensitive, ASCII)
"CustomJobs Error" popup
Lua file has syntax error or can't be found
Check the error message — fix the Lua syntax or file path. Set error display to "Log File" for persistent diagnostics
"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