Info
Content

Overview

Note that this may not be completely accurate, as this page is intended to describe the intended features of Sequoia, not what is currently implemented. Sequoia is still under heavy development and is not available publicly yet.

Sequoia is a custom MapleStory server emulator written from the ground up in pure Kotlin (excluding external libraries). To be precise, Sequoia is the API layer of the server. In order to facilitate custom server features, the API and implementation are kept separate, allowing for plugins to be written against the API without having to modify the core server code. For those familiar with Minecraft servers, Sequoia is basically intended to be the "Bukkit" of MapleStory servers.

Unlike nearly every public Java source out there, Sequoia is not Odin-based, and was written to get away from the instability and bugs that Odin-based sources typically have in common. As a result, Sequoia is rock solid and performant.

Since Sequoia is intended to support multiple versions of MapleStory, the API is separated into multiple "editions":

  • Sequoia Classic: Supports versions of MapleStory prior to the Big Bang update (v93/v94?)
    • v62: Implemented (currently in use by MapleHelios)
    • v83: In development
  • Sequoia Big Bang: Supports versions of MapleStory starting with the Big Bang update.
    • ??
  • Sequoia Chaos, other big updates: Some editions may be combined if they don't differ too much.

Technologies

  • Developed and run against Java 8, but is also working on Java 11.
  • Kotlin for the core server code, using Gradle for building.
  • Lua (via LuaJ) for scripts.
  • Netty for networking.
  • HOCON for configuration files.

Administration

Sequoia includes numerous features that make the lives of server administrators easier. Some of these include:

  • Persistent map changes
    • NPCs can be added or removed from a map without having to modify WZ.
    • NPC scripts can also be overridden without having to modify WZ, even if there are duplicate NPCs within a map.
  • In-depth logging, both for the server itself and player actions (trade/drop logs, chat logs, etc.)
  • Overridable experience and drop rates on a map or area basis
  • Strong anticheat and autobanning
    • Packet edit detection is packet handlers wherever possible. Any little (guaranteed) packet edit will autoban the player. Non-guaranteed packet edits will still send a warning message to online staff.
    • Damage ranges are calculated server side, for stronger damage hack detection and prevention.
  • Crashed channels can be restarted while the server is running.
    • Players are also unable to login to crashed channels (and an error message is shown when they attempt to).
  • Hide properly hides GMs at all times, even during channel changes.
  • Built-in REST API for doing stuff such as sending messages to online players (useful for external vote/donation listeners).
  • Lots and lots of commands for nearly everything.

Development

Some notable internal features:

  • Every item can have arbitrary JSON data attached to it, which is how pet/mount data is stored internally instead of using a separate database table.
    • As a result, pets are easily tradable and won't lose their stats in the process.
    • GM spawned items have tracking data associated with them, also stored as JSON.
  • Nearly no deadlocks due to minimal multithreading. Only a few things are done asynchronously, most of which are database calls. Because of this, it's unnecessary to use locks or synchronized blocks for most of the codebase, leading to almost no deadlocks as a result.
  • Instanced map support.
    • Multiple instances of a map can exist within a channel and can be teleported to by GMs easily.
    • Shindigs (party quests and bosses) can run instanced, meaning multiple instances of the shindig can run concurrently within a single channel.
    • Minidungeons use instanced maps, allowing an unlimited number of players to use the same minidungeon within a single channel (or at least as many players as the server can handle).

Random Stuff

  • Multiaccount support - multiple characters within a single account can be logged in simultaneously.
    • Storage keepers can only be used by one player at a time.
    • Cash shop storage is synchronized when multiple characters are in the cash shop at the same time.
  • Nearly all skills work correctly
  • Working friendship, crush, and marriage rings effects (both first and third person).
  • Nice ban messages upon login attempts

Database and Storage

Sequoia's storage layer is abstracted in order to support multiple backends easily. The primary database implementation is PostgreSQL. MySQL support exists, but is outdated/unmaintained until further stabilization of the source. SQLite support is also planned for easy server setup, although it probably wouldn't be ideal for real world use.

By default, the server autosaves online players every five minutes. When data is saved, only changed data is actually saved to the database, which significantly reduces the amount of work the database has to do compared to doing a full save like other sources do. From live use with 1 to 30 players, the autosaver takes less than 500ms to run.

Scripting

The scripts provided with Sequoia are entirely Lua-based. However, Sequoia itself is not tied to any specific scripting language. As long as a loader for a language is implemented and registered, it can be used for scripts. Due to the fact that all of the scripts were newly written for Sequoia, code duplication is minimal compared to other sources. Common NPC logic is split off into separate files, which are then included as necessary. This significantly reduces the amount of code for certain types of NPCs, namely stylists, item crafters, party quest starters, and boss expedition starters.

In order to simplify the process of writing scripts for NPCs, portals, reactors, and party quests, there are a few core features provided in Sequoia:

Conversation API

The conversation API is simply a built in Lua library that makes it significantly easier to model the flow of NPC conversations. Most Odin-based sources have scripts with a monstrous amount of magic numbers for checking the current state of a conversation, such as this:

From HeavenMS

var status = -1;

function start() {
    cm.sendNext("Bowmen are blessed with dexterity and power, taking charge of long-distance attacks, providing support for those at the front line of the battle. Very adept at using landscape as part of the arsenal.");
}

function action(mode, type, selection) {
    status++;
    if (mode != 1){
        if(mode == 0)
           cm.sendNext("If you wish to experience what it's like to be a Bowman, come see me again.");
        cm.dispose();
        return;
    }
    if (status == 0) {
        cm.sendYesNo("Would you like to experience what it's like to be a Bowman?");
    } else if (status == 1){
	cm.lockUI();
        cm.warp(1020300, 0);
        cm.dispose();
  }
}

The same script looks like this with Sequoia's Lua coroutine-based conversation API:

require("core.coconversation")

function conversation.onStart(ctx, conv)
    conv:monologue(
        "Bowmen are blessed with dexterity and power, taking charge of long-distance attacks, "..
            "providing support for those at the front line of the battle. Very adept at using "..
            "landscape as part of the arsenal."
    )

    if conv:askYesNo("Would you like to experience what it's like to be a Bowman?") then
        ctx:warp(1020300)
    else
        conv:monologue("If you wish to experience what it's like to be a Bowman, come see me again.")
    end
end

Shindig API

The Shindig API is intended for basically any type of "event script", as other sources refer to it. This includes GM events, party quests, boss expeditions, etc. The primary motivation behind this system is to reduce redundant code, making it easier not only to write new shindigs, but also maintain them.

Most of a shindig's logic is actually implemented via a simple configuration file, although scripts can easily be added as necessary. These scripts will only ever be executed if the player is actually in an instance of the shindig.

In shindig instance Not in shindig instance

As an example, here's the shindig.conf file for Kerning PQ (note that it may be changed before a public release of Sequoia).

# Shindig configuration
# Since this shindig is purely script-based, this is where the Kotlin wrapper will grab custom
# configuration for the Shindig, along with the descriptor.

# REQUIRED
# Shindig descriptor
descriptor {
  name = "kerning"
  category = "party_quest"
  description = "Basic Kerning PQ implementation"
  version = "1.0.0"
}

# REQUIRED
# The map to warp players to upon being removed from the shindig.
# Hidden Street - 1st Accompaniment <Exit>
exit map = 103000890

# OPTIONAL
# Max duration of the shindig
timer = 30m

# TODO: Not sure I like this name
# How other game behavior should interact with an instance of this shindig
internal config {
  # Popup message configuration.
  popup {
    # TODO: I don't really like the name of "cash item"
    # Type can be:
    # - simple [default]: Send message in chat
    # - cash item: Send mesage using cash item (for PQs)
    #     - Requires "item ID" to be set
    type = "cash item"
    item ID = 5120017
  }
}

# REQUIRED
# How players participate in this shindig.
players {
  # REQUIRED
  # The grouping mode for players. Can be one of the following:
  # - party (max 6): Typically used for party quests
  # - expedition (max 30, not supported in v62): Typically used for bosses
  # - signup (no max): Includes whoever signs up via the NPC once the leader initiates sign up.
  #   Typically used as an alternative for bosses since v62 doesn't support expeditions.
  # - in map (no max): Includes whoever is in the starting map when the shindig starts.
  mode = "party"

  # REQUIRED
  # The available slots for this shindig.
  slots {
    # Hard minimum = absolute minimum number of players required for the shindig.
    # KPQ CAN be completed with 3 players, but it requires 4 to start.
    # Defaults to min.
    hard min = 3
    # OPTIONAL [default: 1]
    # Must be >= 1
    min = 4
    # REQUIRED
    # Must be <= the max for the grouping mode specified.
    max = 6
  }

  # OPTIONAL
  # The required level range for participants.
  level range {
    # OPTIONAL [default: 1]
    min = 21
    # OPTIONAL [default: 200]
    max = 200
  }
}

npc aliases {
  # Cloto
  9020001 = "stageAdvance"
  # Nella
  9020002 = "exit"
}

# OPTIONAL
# Shared stage definitions
global stage {
  end actions = [
    { type = "play effect", name = "quest/party/clear" }
    { type = "play sound", name = "Party1/Clear" }
    { type = "show map object", name = "gate" }
  ]

  # If true, overrides drops with this shindig's definitions, on a per-stage basis.
  override drops = true
}

# REQUIRED
# Stage definitions
stages = [
  # Stage 1
  {
    initial map = 103000800

    # Popup message to show when entering the initial map
    popup = "Please collect the same number of coupons as the number of Cloto's 1:1 questions!"

    # Condition(s) required for the stage to be completed.
    # These are essentially end actions, but they must all pass a check before actually being applied.
    end conditions = [
      {
        type = "take item"
        item = ${custom.items.pass}
        quantity = "P - 1"
        exact = true
        from leader = true
        take all = true
      }
    ]

    # Actions to perform on stage completion.
    end actions = [
      { type = "experience", amount = 100 }
    ]

    loot table {
      mob {
        # Ligator
        9300001 {
          mode = "all"
          entries = [
            { name = ${custom.items.coupon}, quantity = 1, weight = -1 }
          ]
        }
      }
    }
  }
  # Stage 2
  {
    initial map = 103000801

    popup = "Please find the 3 ropes that can open the door to the next stage and cling on to them!"

    end conditions = [
      # If no regions are defined, they are taken from the Map.wz
      { type = "map combo", slots = 3 }
    ]

    end actions = [
      { type = "experience", amount = 200 }
    ]
  }
  # Stage 3
  {
    initial map = 103000802

    popup = "Please find the 3 platforms that can open the door to the next stage!"

    end conditions = [
      { type = "map combo", slots = 3 }
    ]

    end actions = [
      { type = "experience", amount = 400 }
    ]
  }
  # Stage 4
  {
    initial map = 103000803

    popup = "Please find the 3 containers that can open the door to the next stage!"

    end conditions = [
      { type = "map combo", slots = 3 }
    ]

    end actions = [
      { type = "experience", amount = 800 }
    ]
  }
  # Stage 5 (boss)
  {
    initial map = 103000804

    popup = "Please defeat all the monsters and collect 10 passes!"

    end conditions = [
      { type = "take item", item = ${custom.items.pass}, quantity = 10 }
    ]

    end actions = [
      { type = "experience", amount = 1500 }
    ]

    loot table {
      # Items that aren't tagged as part of the shindig, and therefore can be taken out of it.
      persistent {
        mode = "whitelist"
        entries = [
          ${custom.items.squishy shoes}
        ]
      }

      overrides {
        mob {
          # Slime - use normal loot table and don't mark as shindig items
          210100 = "skip"
        }
      }

      mob {
        # Jr. Necki
        9300000 {
          mode = "all"
          entries = [
            { name = ${custom.items.pass}, quantity = 1, weight = -1 }
          ]
        }

        # Curse Eye
        9300002 {
          mode = "all"
          entries = [
            { name = ${custom.items.pass}, quantity = 1, weight = -1 }
          ]
        }

        # King Slime
        9300003 {
          mode = "all"
          entries = [
            {
              group = "special"
              mode = "one"
              weight = -1
              entries = [
                # Squishy Shoes rate = weight / 100 (25 + 75 from null entry)
                { name = ${custom.items.squishy shoes}, quantity = 1, weight = 25 }
                { name = "null", weight = 75 }
              ]
            }
            {
              group = "normal"
              mode = "all"
              weight = -1
              entries = [
                { name = ${custom.items.pass}, quantity = 1, weight = -1 }
              ]
            }
          ]
        }
      }
    }
  }
  # Stage 6 (bonus)
  {
    initial map = 103000805

    is extra stage = true

    override drops = false
  }
]
Back to top