Everything you need to add custom jobs to a 2025-07-16 Ragnarok Online client using the CustomJobs (Reforged) WARP patch.
4345 – 10000 (hardcoded max)example/ folderPick an unused ID between 4345 and 10000. The ID must match on both client and server.
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.
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.
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.
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 = PCIds or {}
PCIds.EXAMPLE_JOB = 4435
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.
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 = 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
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 = PCPaths or {}
PCPaths[PCIds.EXAMPLE_JOB] = "EXAMPLE_JOB"
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 = 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 "")
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 = PCHands or {}
PCHands[PCIds.EXAMPLE_JOB] = (PCHands[PCIds.NOVICE] or "")
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 = PCImfs or {}
PCImfs[PCIds.EXAMPLE_JOB] = (PCImfs[PCIds.NOVICE] or "")
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.
If your custom job has skills, you also need two files in data/luafiles514/lua files/skillinfoz/:
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:
-- 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
Controls the tab label shown in the skill tree window. Without this, your job's tab will say "Etc":
-- 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")
.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.
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:
-- 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"):
-- 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:
[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 variants of custom jobs need sprite scaling entries to render at 75% size (like stock baby classes). Add to 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.
To enable the Boarding Halter (item 12622) for your custom job, define the mount ID and halter mapping in 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.
Sprites go in the human race body directory. The Korean folder names are CP949-encoded on disk.
| 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 |
_1 suffix. Costume files can be copies of the base sprites — they just need to exist.
| Korean | English | CP949 on disk |
|---|---|---|
| 인간족 | Human Race | Àΰ£Á· |
| 몸통 | Body | ¸öÅë |
| 남 | Male | ³² |
| 여 | Female | ¿© |
| 유저인터페이스 | User Interface | À¯ÀúÀÎÅÍÆäÀ̽º |
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.
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.
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.
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:
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.
How Lua loading errors are reported:
| Option | Behavior |
|---|---|
| Silent | Errors swallowed — files that fail to load are silently skipped |
| Log File | Errors written to customjobs_error.log in the client folder |
| Popup (default) | Windows MessageBox with the Lua error details |
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.
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.
src/common/mmo.hppAdd your job to the e_job enum, before JOB_MAX:
JOB_EXAMPLE_JOB = 4435,
JOB_MAX,
src/map/map.hppAdd a corresponding MAPID entry to the e_mapid enum (before the 2-1 Jobs section):
MAPID_EXAMPLE_JOB,
src/map/pc.cppAdd entries to both conversion switch statements:
// 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;
src/map/pc.hppUpdate the pcdb_checkid macro so the server recognizes your job ID as valid. Add or extend the range to include your job:
// In the pcdb_checkid_sub macro, add a range check:
( (class_) >= JOB_EXAMPLE_JOB && (class_) <= JOB_EXAMPLE_JOB ) \
JOB_FIRST_CUSTOM to JOB_LAST_CUSTOM.
src/map/script_constants.hppExport the job constant so NPC scripts can reference it. Add both entries:
// Near the other job exports:
export_constant(JOB_EXAMPLE_JOB);
// Near the EAJ_ exports:
export_constant2("EAJ_EXAMPLE_JOB",MAPID_EXAMPLE_JOB);
src/char/inter.cppAdd 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):
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:
833: Example Job
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:
Jobs:
Knight: true
Example_Job: true # <-- add this line
Do this in all four files:
| File | Purpose |
|---|---|
job_stats.yml | HP/SP multipliers, weight limit |
job_aspd.yml | Attack speed per weapon type |
job_basepoints.yml | Stat points per level |
job_exp.yml | EXP table (base + job) |
"Job Example_Job does not exist". The job name in YAML must match the enum name with JOB_ removed and underscores preserved.
db/import/skill_tree.yml- Job: Example_Job
Inherit:
- Job: Novice
Tree:
- Skill: SM_SWORD
MaxLevel: 10
# ... add your skills
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
Check off items as you complete them. Progress is saved in your browser.
PCIds.lua — Job ID definedPCNames.lua — Display namePCPaths.lua — Sprite pathPCPals.lua — Palette namePCHands.lua — Weapon spritesPCImfs.lua — IMF reference.spr + .act.spr + .act.spr + .act.spr + .actrenewalparty/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)mmo.hpp — JOB_ enum entry (before JOB_MAX)map.hpp — MAPID_ enum entrypc.cpp — Both conversion switch casespc.hpp — pcdb_checkid macro rangescript_constants.hpp — export_constant + EAJ_ exportinter.cpp — Job name msg_txt casejob_stats.yml — HP/SP/weight entryjob_aspd.yml — Attack speed entryjob_basepoints.yml — Stat points entryjob_exp.yml — EXP table entryskill_tree.yml — Skill tree (optional)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.
The WARP Inputs also include this example — enable the CustomJobs patch and these files work out of the box.
icon_jobs_ID.bmp and _die.bmp to renewalparty/PCNames[PCIds.YOUR_JOB] = "Name" to PCNames.luadata/luafiles514/lua files/JobInfo/ or pack into a GRF listed in DATA.INI