npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

l2js-client

v1.0.1

Published

Lineage 2 Client Library

Downloads

16

Readme

Lineage 2 JavaScript Client

This project was made while experimenting with TypeScript and es6. The idea is to have an NCSoft Lineage 2 client library, that allows other projects to build L2 client functionalities (like bots, game helpers, etc.) on top of it. It can be also used as a framework for building Lineage2 automated tests for L2 private servers.

Table Of Contents

Supported L2 Chronicles

For now the library supports only Lineage 2 HighFive:

  • protocol version 267 - HighFive
  • protocol version 268 - HighFive Update 1
  • protocol version 271 - HighFive Update 2
  • protocol version 273 - HighFive Update 3

If you are interested in other L2 versions, please leave a comment / open an issue.

Protocols

| Protocol | Chronicle | Status | Protocol | Chronicle | Status | | -------- | ----------------------- | ------ | -------- | ----------------------------- | ------ | | 110 | Grand Crusade: Update 1 | | 216 | Freya | | 109 | Grand Crusade | | 152 | Gracia Epilogue Update 1 | | 64 | Helios | | 148 | Gracia Epilogue | | 28 | Underground | | 87 | Gracia Final Update 1 | | 24 | Infinite Odyssey | | 83 | Gracia Final | | 610 | Ertheia Update 3 | | 17 | Gracia Part 2 Update 1 | | 607 | Ertheia Update 2 | | 12 | Gracia Part 2 | | 606 | Ertheia Update 1 | | 851 | Gracia Part 1 | | 603 | Ertheia | | 831 | Hellbound | | 583 | Valiance Update 4 | | 828 | The Kamael | | 581 | Valiance Update 3 | | 746 | Interlude Update 3 | | 580 | Valiance Update 2 | | 744 | Interlude Update 2 | | 578 | Valiance Update 1 | | 740 | Interlude Update 1 | | 575 | Valiance | | 737 | Interlude | | 558 | Lindvior Update 4 | | 709 | C5 Oath Of Blood Update 2 | | 557 | Lindvior Update 3 | | 694 | C5 Oath Of Blood Update 1 | | 533 | Lindvior Update 2 | | 693 | C5 Oath Of Blood | | 532 | Lindvior Update 1 | | 660 | C4 Scions Of Destiny Update 1 | | 531 | Lindvior | | 656 | C4 Scions Of Destiny | WIP | | 488 | Glory Days Update 2 | | 560 | C3 Rise Of Darkness Update 3 | | 480 | Glory Days Update 1 | | 557 | C3 Rise Of Darkness Update 2 | | 479 | Glory Days | | 555 | C3 Rise Of Darkness Update 1 | | 449 | Tauti Update 1 | | 530 | C3 Rise Of Darkness | | 448 | Tauti | | 485 | C2 Age Of Splendor Update 1 | | 410 | Harmony | | 478 | C2 Age Of Splendor | | 415 | Goddess Of Destruction | | 419 | C1 Harbingers Of War | | 273 | High Five Update 3 | WIP | 377 | Prelude | | 271 | High Five Update 2 | WIP | 336 | Prelude PTS | | 268 | High Five Update 1 | WIP | | 267 | High Five | WIP |

* WIP - work in progress


Installation

npm install l2js-client

///@todo

Examples

Logging in

import Client from "l2js-client/Client";

const l2 = new Client();
l2.enter({
  /* required */ Username: "admin",
  /* required */ Password: "admin",
  /* required */ Ip: "127.0.0.1",
  /* optional */ ServerId: 1, //Bartz
  /* optional */ CharSlotIndex: 0,
}); // return a Promise, a.k.a. you can use .then() after "enter()"

Chat

l2.on("LoggedIn", () => {
  l2.say("Hello from " + l2.Me.Name);
  l2.shout("Hello world !!!");
  l2.tell("hi there", "myMainCharName");
  l2.sayToParty("Hello party");
  l2.sayToClan("Hello clan");
  l2.sayToTrade("Hello traders");
  l2.sayToAlly("Hello ppls");
});

Move to location

l2.on("LoggedIn", () => {
  let x = 50 + Math.floor(Math.random() * 50) + l2.Me.X;
  let y = 50 + Math.floor(Math.random() * 50) + l2.Me.Y;
  let z = l2.Me.Z;
  l2.moveTo(x, y, z);
});

Fight back

import { EAttacked } from "l2js-client/events/EventTypes";

l2.on("Attacked", (e: EAttacked) => {
  if (Array.from(e.data.subjects).indexOf(l2.Me.ObjectId) !== -1) {
    l2.hit(e.data.object);
    l2.hit(e.data.object);
  }
});

Follow a character

import { EStartMoving } from "l2js-client/events/EventTypes";

l2.on("StartMoving", (e: EStartMoving) => {
  if (e.data.creature.Name === "Adm") {
    l2.moveTo(e.data.creature.Dx, e.data.creature.Dy, e.data.creature.Dz);
  }
});

Simple bot (auto-target and auto-close-combat-hit)

import L2Creature from "l2js-client/entities/L2Creature";
import { ShotsType } from "l2js-client/enums/ShotsType";
import { EDie, EMyTargetSelected, EPartyRequest, EAttacked } from "l2js-client/events/EventTypes";

l2.on("LoggedIn", () => {
  l2.cancelTarget();
  l2.validatePosition();
  l2.moveTo(l2.Me.X + 1, l2.Me.Y + 1, l2.Me.Z);
  l2.autoShots(ShotsType.SSS, true); // enable SSS

  setInterval(() => {
    if (l2.DroppedItems.size > 0) {
      l2.hit(Array.from(l2.DroppedItems)[0]);
    } else if (!l2.Me.Target || l2.Me.Target.ObjectId === l2.Me.ObjectId) {
      let creature: L2Creature | undefined = l2.nextTarget();
      if (creature instanceof L2Creature) {
        l2.hit(creature);
      }
    }
  }, 500);
})
  .on("MyTargetSelected", (e: EMyTargetSelected) => {
    if (l2.Me.Target) {
      l2.hit(l2.Me.Target);
      l2.attack(l2.Me.Target);
    }
  })
  .on("Die", (e: EDie) => {
    if (l2.Me.Target && e.data.creature.ObjectId === l2.Me.Target.ObjectId) {
      l2.cancelTarget();
      l2.CreaturesList.forEach((c) => {
        c.calculateDistance(l2.Me);
      });
    }
  })
  .on("PartyRequest", (e: EPartyRequest) => {
    l2.acceptJoinParty();
  })
  .on("Attacked", (e: EAttacked) => {
    if (Array.from(e.data.subjects).indexOf(l2.Me.ObjectId) !== -1) {
      l2.hit(e.data.object);
      l2.hit(e.data.object);
    }
  });

Add a custom command

import AbstractGameCommand from "l2js-client/commands/AbstractGameCommand";
import GameClient from "l2js-client/network/GameClient";

l2.registerCommand("sayHello", {
  execute: function (): void {
    console.log("Hello. I am  " + this.Client.ActiveChar.Name);
  },
} as AbstractGameCommand<GameClient>);

l2.on("LoggedIn", () => {
  (l2 as any).sayHello();
});

Simple craft (Soulshot S-Grade)

import { ERecipeBook, ECraftResult } from "l2js-client/events/EventTypes";
import L2Recipe from "l2js-client/entities/L2Recipe";

const RECIPE_SSS = 0x18;
var craftIntervalId: ReturnType<typeof setInterval>;

l2.on("LoggedIn", () => {
  l2.dwarvenCraftRecipes();
})
  .on("RecipeBook", (e: ERecipeBook) => {
    if (e.data.isDwarven) {
      let recipeSSS = Array.from(l2.DwarfRecipeBook).find((r: L2Recipe) => r.Id === RECIPE_SSS);
      if (recipeSSS) {
        clearInterval(craftIntervalId);

        craftIntervalId = setInterval(() => {
          l2.craft(RECIPE_SSS);
        }, 500);
      }
    }
  })
  .on("CraftResult", (e: ECraftResult) => {
    if (!e.data.success) {
      clearInterval(craftIntervalId);
    }
  });

API

Objects

L2Object
  |
  ├── L2Buff
  ├── L2Skill
  ├── L2Creature
  |     ├── L2PartyPet
  |     ├── L2Summon
  |     ├── L2Mob
  |     ├── L2Npc
  |     └── L2Character
  |           ├── L2User
  |           └── L2PartyMember
  ├── L2Mail
  ├── L2Recipe
  └── L2Item
        └── L2DroppedItem

Commands

| Command | Does what? | | ------------------- | ----------------------------------------------------------------------------------------------- | | say | Send a general message | | shout | Shout a message | | tell | Send a PM | | sayToParty | Send a party message | | sayToClan | Send a clan message | | sayToTrade | Send a trade message | | sayToAlly | Send an ally message | | moveTo | Move to location | | hit | Hit on target. Accepts L2Object object or ObjectId | | attack | Attack a target. Accepts L2Object object or ObjectId | | cancelTarget | Cancel the active target | | acceptJoinParty | Accepts the requested party invite | | declineJoinParty | Declines the requested party invite | | nextTarget | Select next/closest attackable target | | inventory | Request for inventory item list | | useItem | Use an item. Accepts L2Item object or ObjectId | | requestDuel | Request player a duel. If no char is provided, the command tries to request the selected target | | autoShots | Enable/disable auto-shots | | cancelBuff | Cancel a buff | | sitOrStand | Sit or stand | | validatePosition | Sync position with server | | dwarvenCraftRecipes | Dwarven craft recipe book | | craft | Craft an item |

Events

| Event Type | Event Data Type | When? | | ------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------ | | LoggedIn | void | logged in to Game server | | PacketReceived | EPacketReceived | a packet is received | | PacketSent | EPacketSent | a packet is sent | | PartyRequest | EPartyRequest | receive a party request | | Die | EDie | L2Creature is dead | | TargetSelected | ETargetSelected | L2Creature is selected by L2Creature | | MyTargetSelected | EMyTargetSelected | L2Creature is selected by L2User | | Attacked | EAttacked | L2User is beings attacked | | RequestedDuel | ERequestedDuel | receive a duel request | | StartMoving | EStartMoving | L2Creature starts moving | | StopMoving | EStopMoving | L2Creature stops moving | | CraftResult | ECraftResult | A result from crafting an item | | RecipeBook | ERecipeBook | A receipt book is received | | PartySmallWindow | EPartySmallWindow | The party small window updated | | PartyMemberPosition | EPartyMemberPosition | A party member position is updated | | CharInfo | ECharInfo | character info | | Revive | ERevive | creature is revived | | ConfirmDlg | EConfirmDlg | confirm dialog | | SystemMessage | ESystemMessage | server system message | | CreatureSay | ECreatureSay | creature says | | NpcHtmlMessage | ENpcHtmlMessage | receiving HTML from an NPC | | NpcQuestHtmlMessage | ENpcQuestHtmlMessage | receiving a quest HTML from an NPC |

///@todo

Lineage 2 Authorization Procedure

Two servers - an authorization server and a game server. Each of them encrypts packets for the client in a slightly different way.

General Information

  • Packets are byte arrays
  • Bytes arrive and are written in the reverse order to a TCP socket (see Little Endian)
  • Packets consist of length (2 bytes), packet type (1 byte) and content (any number of bytes)
  • The contents of the packet will be called the data transmitted in the packet (keys, session ID, etc.), excluding the length and type of packet
  • The length of the packet is not encrypted and is not taken into account when calculating the checksum, since it is calculated after. The type of package, as well as the contents, is taken into account when calculating the checksum. The packet type, contents and checksum are encrypted together using the Blowfish algorithm.
  • Packet Length - A number indicating the length of the entire packet. In other words, it also includes the length of the encrypted data (which consists of the contents and type of the packet, the checksum and is encrypted using the Blowfish algorithm) and two bytes for the number itself, indicating the length
  • Blowfish is a block cipher algorithm that processes blocks of 8 bytes each, which means that the length of the contents of the packet together with the type and checksum must be a multiple of 8 (for this, the checksum is beaten from the contents of the packet with the required number of zeros)
  • Please note that depending on whether you collect the package in advance in Little Endian order or later deploy the encryption and checksum calculation algorithms, respectively, it will be different. This may be important if, for example, the library selected for your programming language Blowfish can only encrypt bytes in direct order (Big Endian)

The order of interaction of the authorization server with the client

For convenience, I'll denote by the prefix S packets sent by the server, and C - sent by the client, because they can have the same ID, but different contents

S / 0x00 (Init) packet is not signed by the checksum, all other packets are signed by

S / 0x00 (Init) is encrypted using the XOR algorithm, all other packets are not encrypted.

All packets are encrypted using the Blowfish algorithm with a key randomly generated for each connection and sent in the S / 0x00 (Init) packet, except for the S / 0x00 (Init) packet which is encrypted with a key invented by the developers of the game (wired in the client)

Encryption using the Blowfish algorithm is always the last, i.e.:

# S / 0x00 packet encryption (Init)
data = xor.encrypt (data)
data = blowfish.encrypt (data, STATIC_KEY)

# Encryption of any other package
data = checksum.sign (data)
data = blowfish.encrypt (data, SESSION_KEY)

Decryption always goes the same:

# Decryption of any incoming packet
data = blowfish.decrypt (data, SESSION_KEY)
data = checksum.verify (data)

The contents of the C / 0x00 packet (RequestAuthLogin) comes with the RSA encrypted algorithm using the key that the server sends in the S / 0x00 (Init) packet. Only content is encrypted, but not the length or type of the packet. The packet itself is encrypted as usual. The content is encrypted additionally, as contains login and password

Interaction procedure for successful authorization:

  1. User initializes the client with login and password
  2. Client connects to a server (default socket port 2106)
  3. Server sends S / 0x00 packet (Init)
  4. Client sends C / 0x07 packet (AuthGameGuard)
  5. Server sends S / 0x0b packet (GGAuth)
  6. Client sends username and password in C / 0x00 packet (RequestAuthLogin)
  7. The server checks the username and password and sends S / 0x03 (LoginOk)
  8. The client requests a list of game servers with the C / 0x05 package (RequestServerList)
  9. The server sends a list of game servers in the S / 0x04 package (ServerList)
  10. The client selects a server from the list, and sends a C / 0x02 packet (RequestServerLogin)
  11. Server sends packet S / 0x07 (PlayOK)

Next, the client disconnects from the authorization server and connects to the game server.

Sending packets by the authorization server.

Packets are written as an array of bytes:

  1. Write 1 byte of packet type, for example, 0x00
  2. We form and add the contents of the package (session ID, server version, keys, zero-byte of Blowfish key end, etc.)
  3. We calculate the checksum for the current byte array
  4. We achieve the length of the current byte array with zero bytes up to a multiple of 8
  5. Add the checksum to the array
  6. We encrypt the current byte array using the Blowfish algorithm
  7. Calculate the length of the resulting array
  8. We add to the value of length 2 to take into account the two bytes of length themselves
  9. Add the length to the beginning of the array

From a TCP socket, bytes should be read in reverse order.

Protocol Overview

Data Transfer

| Header |    Content    |
| A | B  |C|D|E|F|G|H|I|…|

All fields (header and content) are written in little-endian (a.k.a. Intel's byte order). This includes both numeric and string fields.

Header

The only field in a packet's header is a 2-byte unsigned integer, specifying the packet's total size.

| Header |    Content    |
|  Size  |               |
| A | B  |C|D|E|F|G|H|I|…|

Thus, the largest packet size is 64K (65535 bytes), with 2 bytes reserved for size. The smallest valid packet is 2 bytes long and has no real meaning (as TCP makes keep-alive redundant).

Content

The packet's content is what server emulators typically call 'a packet'. The content starts with a unique dynamic-size prefix identifying the type of the packet, followed by the packet's actual content.

| Header |      Content      |
|  Size  | Opcode(s) |  Data |
| A | B  |C|D|E|F|G|H|I|J|K|…|

Opcodes

If we put protocol versions that predate Prelude BETA (336) aside, then each 'packet' (emulator-wise) starts with 1 to 3 opcodes, where the 1st opcode is a single byte, 2nd – two bytes and 3rd – four bytes.

| Header |                  Content                    |
|  Size  | Opcode1 |  Opcode2  |    Opcode3    |  Data |
| A | B  |    C    |  D  |  E  | F | G | H | I |J|K|L|…|

All transmitted data is enciphered. There are different protocol encryption schemes for login and for game server communications.

Login (Auth) Protocol

All login server/client packets are encrypted using a modified blowfish scheme. Each Blowfish encrypted block is 64 bits long. Once a client connects, the server initiates communications by sending an initialization packet. This packet is encrypted with a constant blowfish key (which can be found in the client). What is important, this packet contains the blowfish key used for further communications.

First packet from the server:

1. Write packet data
2. Extend packet size to a multiple of 8
3. Encipher packet data by a 32-bit XOR key
4. Append 8 bytes
5. Store XOR key in the first 4 appended bytes
6. Encipher packet using a blowfish key known in advance
7. Send packet

Other packets from the server:

1. Write packet data
2. Extend packet size to a multiple of 8
3. Calculate packet checksum (simple/fast, XOR-based scheme)
4. Append 8 bytes
5. Store checksum in the first 4 appended bytes
6. Encipher packet using the blowfish key sent in the first packet
7. Send packet

Packets from the client:

1. Write packet data
2. Extend packet size to a multiple of 8
3. Calculate packet checksum (simple/fast, XOR-based scheme)
4. Append 16 bytes
5. Store checksum in the first 4 appended bytes
6. Encipher packet using the blowfish key received from the server
7. Send packet

Game Protocol

The same encryption scheme (with only minor differences) was used both pre- and post-C4, so it was essentially unchanged.

All game server/client packets are enciphered using an XOR-based scheme.

The initial key is made of two parts: a dynamic part given by the game server and a pre-shared part known to the game client (and server) in advance. Legacy clients had two pre-shared key parts. The one to be selected was determined by evaluating the dynamic key part sent by the server.

During cipher operations, the last 4 bytes (DWORD) of the dynamic key part is incremented by the amount of bytes processed by each operation.

Once a client connects, it will immediately send an unenciphered protocol version packet. The server will respond with an unenciphered packet specifying whether the protocol is supported and disclose the mutable key part. The server, if applicable, will also identify itself and send an initial opcode obfuscation key for the client. If the opcode obfuscation key is not 0, the client will then shuffle most of its 1st and 2nd opcodes.

The CM obfuscation key also changes each time a character is logged in.

Except for the first packet, each game server packet is transmitted by taking the following steps:

1. Write packet data
2. Encipher payload using XOR with both parts of the key
3. Update the mutable part of the key
4. Send packet

Except for the first packet, each game client packet is transmitted by taking the following steps:

1. Write packet data
2. Obfuscate opcode(s)
3. Encipher payload using XOR with both parts of the key
4. Update the mutable part of the key
5. Send packet Game server/client packets are not padded.

To-Do List

  • complete the library with all packet handlers

Contributing

I welcome contributions of all types, as long as you enjoy it and do it for fun :-) !