Compare commits

...

43 Commits

Author SHA1 Message Date
Ponk
5754b60ca0 Merge pull request #231 from P0nk/feat/project-update #minor
Upgrade Java version and dependencies
2024-05-10 22:29:51 +02:00
P0nk
dc1a712f52 Re-add section about running from jar 2024-05-10 22:26:04 +02:00
P0nk
7a6d3e1b68 Add feature request issue template 2024-05-10 21:29:09 +02:00
P0nk
970fb3155f Complement pom.xml 2024-05-09 22:29:15 +02:00
P0nk
0eb78e2d9e Use root db user by default 2024-05-09 22:08:28 +02:00
P0nk
7d42870abd Fix sql script not being runnable on newer versions of MySQL 2024-05-09 21:26:09 +02:00
P0nk
c7b9e0eee3 Major rewrite of README 2024-05-09 21:11:53 +02:00
P0nk
c5089881b3 Tidy pom
mvn tidy:pom
2024-05-09 14:24:03 +02:00
P0nk
9c1406f75d Update pull request template 2024-05-09 12:33:50 +02:00
P0nk
92226483be Merge branch 'refs/heads/master' into feat/project-update 2024-05-09 12:30:14 +02:00
Ponk
21ecfeef17 Add pull request template 2024-05-09 12:20:08 +02:00
Ponk
d5be9130aa Add bug report issue template 2024-05-09 11:53:54 +02:00
P0nk
f61fee829a Upgrade Java version and dependencies 2024-05-09 11:45:16 +02:00
Ponk
b5871c7be2 Merge pull request #219 from Silwhoon/mobskill-prop-handling #patch
Mob Skills now have a success rate
2024-02-07 06:49:24 +01:00
Silwhoon
f9b328b432 Mob Skills now have a success rate
This was only used for MAGIC_IMMUNITY and WEAPON_IMMUNITY for some reason? A lot of other MobSkills also have a 'prop' node in the WZ files such as; Darkness and Seduce
2024-02-05 17:58:43 +00:00
Ponk
830df4e5ca Merge pull request #218 from Silwhoon/makecharinfo #minor
Utilize the MakeCharInfo.img data in the WZ files
2024-02-05 18:09:12 +01:00
Silwhoon
6f68f4edfd Addressed nitpicks and removed unused item ID constants 2024-02-05 17:00:18 +00:00
Silwhoon
a7931c3e4d New characters now utilise the MakeCharInfo.img data in the WZ files 2024-02-05 16:03:41 +00:00
Ponk
799cb97564 Merge pull request #217 from P0nk/fix/210-clean-slate-scroll #minor
Fix able to gain infinite slots with clean slate scroll
2024-02-05 08:39:25 +01:00
P0nk
851b57e8ef Fix able to gain infinite slots with clean slate scroll
And remove a GM config option.
Thanks Crabo for the suggested solution.
2024-02-05 08:34:36 +01:00
Ponk
ceb2866aa1 Merge pull request #214 from leevccc/master #patch
fix: item lock cant extend lock time
2024-02-04 14:22:00 +01:00
Ponk
11c1e4655e Merge pull request #213 from yuzumika/ppq-npc-fix #patch
Fix bug in PPQ where non-party leader can get stuck in an NPC dialogue loop
2024-02-04 12:09:33 +01:00
Ponk
aca9cbf91d Merge pull request #212 from yuzumika/fix-selection-underflow #minor
NPCMoreTalkHandler: don't underflow selection
2024-02-04 12:07:46 +01:00
leevccc
08b089d9be fix: seal lock cant extend lock time 2024-01-14 22:13:16 +08:00
leevccc
8b254a294e fix: item lock cant extend lock time 2024-01-14 18:15:48 +08:00
yuzumika
7004de6e71 ppq: fix bug where non-party leader can get stuck in an npc dialogue loop 2024-01-04 14:30:59 -08:00
yuzumika
738e1b24e6 add unit tests for ByteBufInPacketTest.readUnsignedByte 2024-01-04 13:42:20 -08:00
yuzumika
5a4200cc8e implement ByteBufInPacket.readUnsignedByte 2024-01-04 10:55:07 -08:00
yuzumika
cb0320a471 NPCMoreTalkHandler: don't underflow selection 2024-01-03 16:10:23 -08:00
Ponk
058f034c2b Merge pull request #209 from yuzumika/minigame-npc-dispose #patch
Minigame NPC (Casey) tweaks
2024-01-03 20:13:59 +01:00
yuzumika
9c54f3a8ea minigame npc: add more disposes, fix typo 2023-12-27 15:35:45 -08:00
yuzumika
ee8cb545e1 minigame npc: use omokamount constant consistently; tiny text tweak to
make it more GMS-like
2023-12-27 15:18:22 -08:00
yuzumika
64bbff462d minigame npc: dispose after creating a set of match cards 2023-12-27 14:54:10 -08:00
Ponk
f63f7e13d4 Merge pull request #205 from yuzumika/face-hair-dedup #patch
use isFace and isHair in ItemInformationProvider.getStringData
2023-12-10 09:49:55 +01:00
yuzumika
db8666fc71 use isFace and isHair in ItemInformationProvider.getStringData 2023-11-24 01:38:34 -08:00
Ponk
93ea66e6fe Merge pull request #202 from peamy/master #patch
Bugfix: unable to complete scipted quests remotely
2023-11-10 21:11:37 +01:00
Ice Bear
7131e39c96 Merge pull request #1 from P0nk/master
Pull cosmic master into Peamy master
2023-11-10 11:40:55 +01:00
remsus
b80e9a3310 Bugfix: unable to complete scipted quests remotely 2023-11-10 00:20:52 +01:00
Ponk
03802666ef Merge pull request #190 from ormizj/character-exp-log #minor
added logs for character exp gain
2023-11-08 21:16:57 +01:00
ormizj
c145a53688 fixed wrong method call 2023-09-15 01:04:22 +03:00
spiderpig60
c744935dd0 changed starting server to vanilla values 2023-09-10 21:48:02 +03:00
spiderpig60
90b44c3a8b added timed thread, added batch, cleaned code
changed starting value to false
2023-09-10 21:36:52 +03:00
spiderpig60
60a44252ea added logs for character exp gain 2023-09-10 01:53:53 +03:00
33 changed files with 687 additions and 428 deletions

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,23 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.

View File

@@ -0,0 +1,15 @@
---
name: Feature request
about: Suggest an idea for this project, such as a missing gameplay feature
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.

11
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,11 @@
## Description
<!-- Describe your changes in detail -->
## Checklist before requesting a review
<!-- Mark with "x" inside the square brackets -->
- [ ] I have performed a self-review of my code
- [ ] I have tested my changes
- [ ] I have added unit tests that prove my changes work
## Screenshots
<!-- If applicable, add screenshots to help explain your changes -->

View File

@@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
java-version: '21'
distribution: 'corretto'
- name: Build with Maven (compile -> test -> package)
run: mvn -B package --file pom.xml

View File

@@ -4,15 +4,21 @@
#
# Cosmic JAR creation stage
#
FROM maven:3.9.1-eclipse-temurin-17 AS jar
FROM maven:3.9.6-amazoncorretto-21 AS jar
# Build in a separated location which won't have permissions issues.
WORKDIR /opt/cosmic
# Any changes to the pom will affect the entire build, so it should be copied first.
COPY pom.xml ./pom.xml
# Grab all the dependencies listed in the pom early, since it prevents changes to source code from requiring a complete re-download.
# Skip compiling tests since we don't want all the dependecies to be downloaded.
RUN mvn -f ./pom.xml clean dependency:go-offline -Dmaven.test.skip -T 1C
# Skip compiling tests since we don't want all the dependencies to be downloaded.
# RUN mvn -f ./pom.xml clean dependency:go-offline -Dmaven.test.skip -T 1C
# TODO: The above command stopped working as of Java 21 upgrade due to:
# Failed to execute goal org.apache.maven.plugins:maven-dependency-plugin:3.6.1:go-offline (default-cli) on project Cosmic: org.eclipse.aether.resolution.DependencyResolutionException: The following artifacts could
# not be resolved: io.netty:netty-tcnative:jar:${os.detected.classifier}:2.0.65.Final (absent): Could not find artifact io.netty:netty-tcnative:jar:${os.detected.classifier}:2.0.65.Final in central (https://repo.maven.apache.org/maven2) -> [Help 1]
# Source code changes may not change dependencies, so it can go last.
# Skip compiling tests since we don't want all the dependecies to be downloaded for plugins.
COPY src ./src
@@ -21,7 +27,7 @@ RUN mvn -f ./pom.xml clean package -Dmaven.test.skip -T 1C
#
# Server creation stage
#
FROM eclipse-temurin:17.0.6_10-jre
FROM amazoncorretto:21
# Host the server in a location that won't have permissions issues.
WORKDIR /opt/server

364
README.md
View File

@@ -1,303 +1,181 @@
# Cosmic - MapleStory v83 server
# Cosmic
Cosmic is a server emulator for Global MapleStory (GMS) version 83.
## Introduction
Cosmic launched as a successor to HeavenMS on March 21st 2021.
HeavenMS is archived, ie. it receives no further updates. This project aims to continue its development; mainly by improving code quality and make getting into PS development as easy as possible.
Cosmic launched on March 2021. It is based on code from a long line of server emulators spanning over a decade - starting with OdinMS (2008) and ending with HeavenMS (2019).
This is an open source project. Anyone may contribute by opening a pull request.
This is mainly a Java based project, but there are also a bunch of scripts written in JavaScript.
Only the server side is maintained. The client is directly copied from HeavenMS.
Head developer and maintainer: __Ponk__.\
Contributors: a lot of people over the years, and hopefully more to come. Big thanks to everyone who has contributed so far!
Join the Discord server where most of the discussions take place: https://discord.gg/JU5aQapVZK
Beware - ***This server emulator is not production ready.***
It can be useful for testing things locally or for trying out ideas, but launching a new private server based on this and opening it up to the public
without knowing what you're doing is not recommended.
### Goals
What we are working towards.
* __Vanilla gameplay__ - stay as close to the original game as possible (within reason).
* __Ease of use__ - getting started should be frictionless and contributing to the project straightforward.
* __Reduce technical debt__ - making changes should be easy without causing unintended side effects.
* __Modern tools & technologies__ - stay appealing by continuously improving the code and the project as a whole.
### Non-goals
Explicitly excluded from the scope of the project.
* __Custom gameplay features__ - existing custom features will be removed over time and new ones are unlikely to be added.
* __Client development__ - this project is focused on the server. Please go elsewhere for client related questions.
* __Public server__ - there will not be an official Cosmic server open to the public. Feel free to launch your own server __at your own risk__. No support will be provided.
## Development information
## Project setup
### Status (updated 2022-10-16)
### Contribute
You may contribute to the project in various ways, mainly through GitHub:
* Providing improvements to the code through a [Pull Request](https://github.com/P0nk/Cosmic/pulls) from your own fork.
* Reporting a bug by creating an [Issue](https://github.com/P0nk/Cosmic/issues).
* Providing information to existing issues or reviewing pull requests that others have made.
* ...and in other ways that I haven't thought of!
Development is currently <span style="color:Yellow">**sporadic**</span>.
### Continuous integration
A GitHub Actions pipeline is set up to run the build automatically when a new pull request is opened or commits are pushed to an existing one. This ensures that the code compiles and all the tests pass.
My time is very limited nowadays, but I try to keep up with the submitted pull requests. I may submit some stuff of my own, once in a while.
Once a pull request is merged, a tag with the new version is automatically created.
### Ways to contribute
* Submit a Pull Request (fork -> commit -> PR). If you don't know where to start, have a look at the issues on GitHub.
* Report a bug (preferably as an Issue on GitHub, as reports on Discord may be forgotten or lost)
* Spread the word about Cosmic
### Working with GitHub
Anyone with a GitHub account can contribute by making some changes in a branch and opening up a PR.
All activity on the GitHub repo (opening PR, commenting, creating issue, etc.) is automatically pushed (via webhook) to a public Discord channel for visibility.
Issues is the main place where bugs, issues or general improvements are tracked. Feel free to submit a new issue, but please keep it in English. By providing a good description, you increase the chance of a bug being fixed.
Tasks (past, present and future) are kept in the Cosmic project, which you get to via the "Projects" tab. This gives you an idea of where the project is moving.
### Discord integration
Most GitHub activity is pushed to a Discord channel for visibility. This works by leveraging a webhook. The activity includes (but is not limited to): merged commits, created PRs, comments, and new tags.
### Versioning
The project follows the [SemVer](https://semver.org/) versioning scheme using git tags.
As a pull request gets merged, a new version is automatically created.
Bug fixes result in bumped patch version: 1.2.__3__ -> 1.2.__4__
General improvements result in bumped minor version: 1.__2__.3 -> 1.__3__.3
Major changes result in bumped major version: __1__.2.3 -> __2__.2.3
### Cosmic
- GitHub: https://github.com/P0nk/Cosmic
- Discord: https://discord.gg/JU5aQapVZK
### HeavenMS
- GitHub: https://github.com/ronancpl/HeavenMS
- Discord: https://discord.gg/Q7wKxHX
## Tools / downloads
* **Java 17 SDK** - Needed to compile and run Java code. Install manually or through IntelliJ depending on how you prefer to launch the server. Not required for launching with Docker.
* Link: https://jdk.java.net/17/
* **IntelliJ IDEA** - Java IDE and your main tool for working with the source code. Community edition is good enough.
* Link: https://www.jetbrains.com/idea/
* **MySQL Community Server 8** - Database for game data.
* Link: https://dev.mysql.com/downloads/mysql/
* **MySQL Workbench 8** - Client for interacting with the database. Other clients do exist.
* Link: https://dev.mysql.com/downloads/workbench/
* **Docker Desktop** (optional) - For launching the game locally with less hassle.
* Link: https://www.docker.com/products/docker-desktop
* **Client files and general tools**
* Link: https://drive.google.com/drive/folders/1hgnb92MGL6xqEp9szEMBh0K9pSJcJ6IT?usp=sharing
* This is Ponk's own Google Drive, similar to how Ronan provides files for HeavenMS.
### MapleStory client
- Latest localhost client: https://hostr.co/amuX5SLeeVZx
**Important note about localhost clients**: these executables are red-flagged by antivirus tools as __potentially malicious software__,
this happens due to the reverse engineering methods that were applied onto these software artifacts.
Those depicted here have been put to use for years already and posed no harm so far, so they are soundly assumed to be safe.
The project follows the [semantic versioning](https://semver.org/) scheme using git tags.
* *Bug fixes* are treated as PATCH: 1.2.__3__ -> 1.2.__4__
* *General changes or improvements* are treated as MINOR: 1.__2__.3 -> 1.__3.0__
* *Major changes* are treated as MAJOR: __1__.2.3 -> __2.0.0__
## Getting started
The localhost MapleStory client needs to be installed, as well as the server that will host the game.
Follow along as I go through the steps to play the game on your local computer from start to finish. I won't go into extreme detail, so if you don't have prior experience with Java or git, you might struggle.
### Installing the client
We will set up the following:
- Database - the database is used by the server to store game data such as accounts, characters and inventory items.
- Server - the server is the "brain" and routes network traffic between the clients.
- Client - the client is the application used to _play the game_, i.e. MapleStory.exe.
1. Install MapleStory with "MapleGlobal-v83-setup.exe" in your folder of choice (e.g. "C:\Nexon\MapleStory") and follow their instructions.
2. Once done, erase these files: "HShield" (folder), "ASPLnchr.exe", "MapleStory.exe" and "Patcher.exe".
3. Extract into the client folder the "HeavenMS-localhost-WINDOW.exe" (from now on referred to as "localhost.exe") from the provided link.
4. Overwrite the original WZ files with the ones provided on the Google Drive: "CosmicWZ-v1-2021.05.10.zip"
- This is currently identical to the latest HeavenMS WZ files (except for the file name): "commit397_wz-20210321T173600Z-001.zip"
### 1 - Database
You will start by installing the database server and client, and then run some scripts to prepare it for the server.
#### Editing localhost IP target
#### Steps
If you are not using "localhost" as the target IP on the server's config file, you will need to HEX-EDIT localhost.exe to fetch your IP. Track down all IP locations by searching for "Type: String" "127.0.0.1", and applying the changes wherever it fits.
1. Download and install [MySQL Community Server 8+](https://dev.mysql.com/downloads/mysql/). You will have to set a root password, make sure you don't lose it because you will need it later.
2. Download and install [HeidiSQL](https://www.heidisql.com/download.php).
3. Open HeidiSQL and connect to the database ("New" -> "Session in root folder" -> fill in password -> "Open").
4. Run all four scripts located in database/sql in order. Starting with ``1-db_database.sql`` and ending with ``4-db-admin.sql``. In HeidiSQL: "File" -> "Run SQL File...".
5. The database is ready!
To hex-edit, install the Neo Hex Editor from "free-hex-editor-neo.exe" and follow their instructions. Once done, open localhost.exe for editing and overwrite the IP values under the 3 addresses. Save the changes and exit the editor.
### 2 - Server
You will start by cloning the repository, then configure the database properties and lastly start the server.
(TODO: find suitable alternative to Neo Hex Editor)
#### Prerequisites
* Java 21+ (I recommend [Amazon Corretto](https://aws.amazon.com/corretto))
* IDE (I recommend [IntelliJ IDEA](https://www.jetbrains.com/idea/))
#### Testing the localhost
#### Steps
Open the "localhost.exe" client.
If by any means the program did not open, and checking the server log your ping has been listened by the server
and you are using Windows 8, 10 or 11, it is probably some compatibility issue.
1. Clone Cosmic into a new project. In IntelliJ, you would create a new project from version control.
2. Open _config.yaml_. Find "DB_PASS" and set it to your database root user password.
3. Start the server. The main method is located in `net.server.Server`.
4. If you see "Cosmic is now online" in the console, it means the server is online and ready to serve traffic. Yay!
In some cases it helps to spam click the exe a few times (2-3 times usually works for me on W10).
Below, I list other ways of running the server which are completely optional.
In that case, extract "lolwut.exe" from "lolwut-v0.01.rar" and place it on the MapleStory client folder ("C:\Nexon\MapleStory").
Your "localhost.exe" property settings must follow these:
#### Docker
Support for Docker is also provided out of the box, as an alternative to running straight in the IDE. If you have [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed it's as easy as running `docker compose up`.
Note: "lolwut.exe" is currently not available in the Google Drive.
Making changes becomes a bit more tedious though as you have to rebuild the server image via `docker compose up --build`.
* Run in compatibility mode: Windows 7;
* Unchecked reduced color mode;
* 640 x 480 resolution;
* Unchecked disable display on high DPI settings;
* Run as an administrator;
* Opening "lolwut.exe", use Fraysa's method.
On the first launch, the database container will run the scripts which may take so long that the server fails to start. In that case, just wait until the database is done running the scripts and then retry (Ctrl+C and re-run the command).
Important: should the client be refused a connection to the game server, it may be because of firewall issues. Head to the end of this file to proceed in allowing this connection through the computer's firewall. Alternatively, one can deactivate the firewall and try opening the client again.
You can also search the server logs (/logs/cosmic-log.log) if any connection attempts have been made to ease debugging.
#### Jar
Another option is to start the server from a terminal by running a jar file. You first need to build the jar file from source which requires [Maven](https://maven.apache.org/).
---
### Installing the server
1. Configure the project
2. Set up the database
3. Launch the server
Building the jar file is as easy as running ``mvn clean package``. The project is configured to produce a "fat" jar which contains all dependencies (by utilizing the _maven-assembly-plugin_). Note that the WZ XML files are __not__ included in the jar.
If you are using Docker (quick start):
1. Configure the project
2. Launch the server
To run the jar, a ``launch.bat`` file is provided for convenience. Simply double-click it and the server will start in a new terminal window.
#### Configuring the project
Alternatively, run the jar file from the terminal. Just remember to provide the `wz-path` system property pointing to your wz directory.
The easiest way to set up your project is to clone the repository directly into a new IntelliJ project.
### 3 - Client
You will start by installing the game with the old installer, then overwrite some WZ files with our custom ones, and lastly get the localhost executable in place.
1. Install IntelliJ
2. Create a new "Project from Version Control..."
3. Enter the URL to this GitHub repository: "https://github.com/P0nk/Cosmic.git"
4. Click on "Clone". A new project will now be created with all the files from the repository.
#### Steps
#### Setting up the database
1. Download _MapleGlobal-v83-setup.exe_ from my [Google Drive](https://drive.google.com/drive/folders/1hgnb92MGL6xqEp9szEMBh0K9pSJcJ6IT). This is the official installer from back then.
2. Install it in a directory of your choice.
3. Delete the following files from the installation directory: _HShield_ (entire directory), _ASPLnchr.exe_, _MapleStory.exe_, and _Patcher.exe_.
4. Download _CosmicWZ-v1-2021.05.10.zip_ from my [Google Drive](https://drive.google.com/drive/folders/1hgnb92MGL6xqEp9szEMBh0K9pSJcJ6IT).
5. Unzip it and copy all .wz-files into the installation directory. Replace the existing ones.
6. Download _HeavenMS-localhost-WINDOW.exe_ from [hostr.co](https://hostr.co/amuX5SLeeVZx). This is a client modified to connect to your localhost instead of Nexon's server (along with some fixes and custom changes).
- Your antivirus will likely detect the file as a trojan or similar and automatically delete it. To prevent this from happening, add your _Downloads_ directory and the installation directory as exclusions in your antivirus software. On W11, this is under "Virus & threat protection settings" -> "Add or remove exclusions".
7. Move _HeavenMS-localhost-WINDOW.exe_ into the installation directory.
8. Done! Double-click the exe and the game should start.
- The client may be a bit fiddly. Sometimes it won't start, but if you see "Client connected" in the server console it's a good indication. Try spam-clicking it like 10+ times, that usually works for me.
1. Install MySQL Server 8 and MySQL Workbench 8.
2. Using Workbench, create a new user with username "cosmic_server" and password "snailshell".
This the default configuration in Cosmic.
* (Optional) Restrict the Schema Privileges for this new user for improved security.
Add a new entry with "Schemas matching pattern: cosmic" and only select "SELECT", "INSERT", "UPDATE", "DELETE" under "Object Rights"
3. Run the sql scripts in the "database/sql" directory of the project in the order indicated by their names.
* Make sure you are connected to the database with the "root" user to be able to run the scripts.
* Run scripts one by one through the menu: "File" -> "Run SQL Script" -> select the script file to run -> "Run"
* The 3rd script "3-db_shopupdate" is optional. It adds custom shop items for certain NPCs.
* The 4th script "4-db_admin" is also optional, but recommended if you are new. It adds an admin account to simplify the setup.
**Important note about localhost clients**: these executables are red-flagged by antivirus tools as potentially malicious software.
This happens due to the reverse engineering methods that were applied onto these software artifacts.
The one provided here has been in use for years already and posed no harm so far, so it is assumed to be safe.
Use this info when you connect to MySQL Server for the first time:
* Server Host: localhost
* Port: 3306
* Username: root
* Password: <whatever password you set during MySQL Server installation>
### 4 - Getting into the game
The client has started, and you're looking at the login screen.
At the end of the execution of these sql scripts, you should have installed a database schema named "cosmic".
REGISTER YOUR FIRST ACCOUNT to be used in-game by **manually creating** an entry in the table "accounts" in the database with a username and password.
### Running the server
Configure the IP you want to use for your MapleStory server in "config.yaml" file, or set it as "localhost" if you want to run it only on your machine.
Alternatively, you can use the IP given by Hamachi to use on a Hamachi network, or you can use a non-Hamachi method of port-forwarding. Neither will be approached here.
To launch the server, you may either:
* Launch inside IntelliJ
* Launch a built jar file
* Launch with Docker
#### Run inside IntelliJ
1. Open the file src/main/java/net/server/Server.java.
2. Click the green arrow to the left of the class definition "public class Server", and then "Run Cosmic".
* Alternatively (recommended), create a new Configuration that points to "net.server.Server".
3. The server launches in a terminal window inside IntelliJ.
#### Run from a jar file
1. Create the jar file
* The jar file is created by the Maven assembly plugin in the package lifecycle.
* If you already have Maven installed, simply run the command "mvn clean install" to create the jar file.
* IntelliJ also comes with built-in Maven support. Open a new terminal window inside IntelliJ, type "mvn clean install" (your command should now be marked green), then Ctrl+Enter to build the jar file.
2. Launch the jar file
* Double click on "launch.bat" (need to have Java 17 installed)
#### Run as containers with Docker
1. Start Docker
2. Run the command "docker compose up" at the root of the project.
* If you make any changes to the code, make sure you append the "--build" option at the end of the command to force rebuild the server image.
---
### Getting into the game
If you ran the admin sql script, there already exists an account in the database with an admin character on it (GM level 6).
Log in using these credentials:
#### Logging in
At this point, you can log in to the admin account using the following credentials:
* Username: "admin"
* Password: "admin"
* Pin: "0000"
* Pic: "000000"
Admin characters have "hide" mode enabled by default. This means your character will be translucent on your screen, and completely invisible to others.
It will also prevent you from controlling mobs (making them stand still). To toggle this mode on and off, type "@hide" in the in-game chat.
Or create a regular account by typing in your desired username & password and attempting to log in. This "automatic registration" feature lets you create new accounts to play around with. It is enabled by default (see _config.yaml_).
By default, the server source is set to allow AUTO-REGISTERING. This means that, by simply typing in a "Login ID" and a "Password", you're able to create a new account.
#### Entering the game
Create a new character as you normally would, and then select it to enter the game. Hooray, finally we're in!
After creating a character, experiment typing in all-chat "@commands".
This will display all available commands for the current GM level your character has.
If you log in to the "Admin" character, you'll notice that the character looks almost invisible. This is hide mode, which is enabled by default when you log in to a GM character. You won't be visible to normal players and no mobs will move if you're alone on the map. Toggle hide mode on or off by typing "@hide" in the in-game chat.
To change a character's GM level, make sure that character is not logged in, then:
Hide is one of many commands available to players, type "@commands" to see the full list. Higher ranked GMs have access to more powerful commands.
1. Open MySQL Workbench;
2. Expand "cosmic" schema;
3. Expand "Tables";
4. Right-click "characters" and click "Select Rows"
5. Find your character in Result Grid. Scroll to the right and find the "gm" column.
6. Edit your character's gm value and click "Apply", and then "Apply" again in the window that appeared, then "Finish".
* 0 is what ordinary players start with, and 6 is the highest gm value. Higher level gms have access to more commands in game.
That's it, have fun playing around in game!
---
### Some notes about WZ/WZ.XML EDITING
Brief introduction to WZ files: they are the asset/data files required by the client and server. The client can read the .wz files directly, but the server requires them in XML format.
The server also does not make use of any of the sprites, which is where different kinds of exporting comes into the picture. HaRepacker allows you to export to Private server XML, which is the .img files packaged in the .wz stripped of sprites and converted to XML.
## Advanced concepts
Some slightly more advanced concepts that might be useful once you're up and running.
Link to HaRepacker-resurrected, the standard tool for handling WZ files: https://github.com/lastbattle/Harepacker-resurrected
### Host on remote server
You don't have to host the server on your local machine to play. It's possible to host on a remote server such as a VPS or even a dedicated server.
NOTE: Be extremely wary when using server-side's XMLs data being reimported into the client's WZ, as some means of synchronization between the server and client modules, this action COULD generate some kind of bugs afterwards. Client-to-server data reimporting seems to be fine, though.
I leave it to you to figure out the server hosting part, but once you have that running you'll need to edit the client exe to point to your remote server ip.
#### Editing the v83 WZ's:
#### Edit client ip
1. Download and install a hex editor: [HxD](https://mh-nexus.de/en/hxd/)
2. Start HxD and open your client exe (I recommend making a copy of it first). At this point you should see a bunch of hex codes and a "Decoded text" column to the right of it.
3. Ctrl+f and search for Text-string "127.0.0.1". You should find three occurrences right above each other.
4. Place your cursor before the first "127" and start typing the desired ip, overwriting what is already there. Do the same on the other two and click on Save.
5. Done! Now the client will attempt to connect to that ip address instead when you launch it.
### WZ files
WZ files are the asset/data files required by the client and server. Typically, [HaRepacker-resurrected](https://github.com/lastbattle/Harepacker-resurrected) is used to handle (view, edit, export) the .wz files.
* Use the HaRepacker-resurrected 4.2.4 editor, encryption "GMS (old)".
* Open the desired WZ for editing and use the node hierarchy to make the desired changes (copy/pasting nodes may be unreliable in rare scenarios).
* Save the changed WZ, **overwriting the original content** at the client folder.
* Finally, **RE-EXPORT (using the "Private Server..." exporting option) the changed XMLs into the server's WZ.XML files**, overwriting the old contents.
The client can read the .wz files directly, but the server requires them in XML format. The server also does not make use of the sprites, which is the motivation for different kinds of exporting.
HaRepacker allows you to export to "Private server", which is the .img files packaged in the .wz stripped of sprites and converted to XML. This takes much less disk space.
**These steps are IMPORTANT, to maintain synchronization** between the server and client modules.
This server requires custom .wz files (unfortunately), as you may have noted during installation of the client. The intention is for these to be removed eventually and to solely run on vanilla .wz files.
---
### Portforwarding the SERVER
#### WZ editing
* Use the HaRepacker-resurrected editor, encryption "GMS (old)".
* Open the desired .wz for editing and use the node hierarchy to make the desired changes (copy/pasting nodes may be unreliable in rare scenarios).
* Save the changed .wz, overwriting the original content at the client folder.
* Finally, re-export (using the "Private Server" exporting option) the changed XMLs into the server's .wz XML files (found in the "wz" directory), overwriting the old contents.
To use portforward, you will need to have permission to change things on the LAN router. Access your router using the Internet browser. URLs vary accordingly with the manufacturer. To discover it, open the command prompt and type "ipconfig" and search for the "default gateway" field. The IP shown there is the URL needed to access the router. Also, look for the IP given to your machine (aka "IPv4 address" field), which will be the server one.
Make sure to always export from the client .wz files to the server XML, and not the other way around.
The default login/password also varies, so use the link http://www.routerpasswords.com/ as reference. Usually, login as "admin" and password as "password" completes the task well.
Editing the client .wz without exporting to the server may lead to strange behavior.
Now you have logged in the router system, search for anything related to portforwarding. Should the system prompt you between portforwarding and portriggering, pick the first, it is what we will be using.
### Client features
For more information about the client and its features, see [HeavenMS on GitHub](https://github.com/ronancpl/HeavenMS#download-items).
Now, it is needed to enable the right ports for the Internet. For Cosmic, it is basically needed to open ports 7575 to 7575 + (number of channels) and port 8484. Create a new custom service which enables that range of ports for the server's channel and opt to use TCP/UDP protocols. Finally, create a custom service now for using port 8484.
Optionally, if you want to host a webpage, portforward the port 80 (the HTTP port) as well.
It is not done yet, sometimes the firewalls will block connections between the LAN and the Internet. To overcome this, it is needed to create some rules for the firewall to permit these connections. Search for the advanced options with firewalls on your computer and, with it open, create two rules (one outbound and one inbound).
These rules must target "one application", "enable connections" and must target your MapleStory client (aka localhost).
After all these steps, the portforwarding process should now be complete.
---
### Client changelog
The following list, in bottom-up chronological order,
holds information regarding all changes that were applied from the starting localhost used in this development.
Some lines have a link attached, that will lead you to a snapshot of the localhost at that version of the artifact.
Naturally, later versions holds all previous changes along with the proposed changes.
**Change log:**
* Fixed Monster Magnet crashing the caster when trying to pull fixed mobs, credits to Shavit. https://gofile.io/?c=BW7dVM (dead link)
* Cleared need for administrator privileges (OS) to play the game, credits to Ubaware.
* Set a higher cap for AP assigning with AP Reset, credits to Ubaware.
* Fixed Monster Magnet crashing the caster when trying to pull bosses. Drawback: Dojo HPBar becomes unavailable. https://hostr.co/SvnSKrGzXhG0
* Fixed some 'rn' problems with quest icons & removed "tab" from party leader changed message. https://hostr.co/tsYsQzzV6xT0
* Removed block on applying attack-based strengthening gems on non-weapon equipments. https://hostr.co/m2bVtnizCtmD
* Set a higher cap for SPEED.
* Removed the AP assigning block for beginners below level 10. https://hostr.co/AHAHzneCti9B
* Removed block on party for beginners level 10 or below. https://hostr.co/JZq53mMtToCz
* Removed block on MTS entering in some maps, rendering the buyback option available.
* Removed "AP excess" popup and limited actions on Admin/MWLB, credits to kevintjuh93.
* Removed "You've gained a level!" popup, credits to PrinceReborn.
* Removed caps for WATK, WDEF, MDEF, ACC, AVOID.
* 'n' problem fixed.
* Fraysa's https://hostr.co/gJbLZITRVHmv
* Eric's MapleSilver starting on window-mode.
Some notable features:
* Opens in window mode by default
* Uncapped max speed

View File

@@ -161,8 +161,8 @@ server:
#Database Configuration
DB_URL_FORMAT: "jdbc:mysql://%s:3306/cosmic" # If the docker ENV for DB_HOST is anything but "db", this string format should be changed from 3306 to 3307 (or whichever port it was changed to in docker)
DB_HOST: "localhost"
DB_USER: "cosmic_server"
DB_PASS: "snailshell"
DB_USER: "root"
DB_PASS: ""
INIT_CONNECTION_POOL_TIMEOUT: 90 # Seconds
#Login Configuration
@@ -234,6 +234,7 @@ server:
USE_STARTING_AP_4: false #Use early-GMS 4/4/4/4 starting stats. To overcome AP shortage, this gives 4AP/5AP at 1st/2nd job advancements.
USE_AUTOBAN: false #Commands the server to detect infractors automatically.
USE_AUTOBAN_LOG: true #Log autoban related messages. Still logs even with USE_AUTOBAN disabled.
USE_EXP_GAIN_LOG: false #Logs characters exp gains; logs world rate & coupon exp, total gained exp, and current exp, level can be calculated from "ExpTable".
USE_AUTOSAVE: true #Enables server autosaving feature (saves characters to DB each 1 hour).
USE_SERVER_AUTOASSIGNER: false #HeavenMS-builtin autoassigner, uses algorithm based on distributing AP accordingly with required secondary stat on equipments.
USE_REFRESH_RANK_MOVE: true
@@ -346,7 +347,6 @@ server:
USE_PERFECT_SCROLLING: false #Scrolls doesn't use slots upon failure.
USE_ENHANCED_CHSCROLL: false #Equips even more powerful with chaos upgrade.
USE_ENHANCED_CRAFTING: false #Apply chaos scroll on every equip crafted.
USE_ENHANCED_CLNSLATE: false #Clean slates can be applied to recover successfully used slots as well.
SCROLL_CHANCE_ROLLS: 1 #Number of rolls for success on a scroll, set 1 for default.
CHSCROLL_STAT_RATE: 1 #Number of rolls of stat upgrade on a successfully applied chaos scroll, set 1 for default.
CHSCROLL_STAT_RANGE: 6 #Stat upgrade range (-N, N) on chaos scrolls.

View File

@@ -1,9 +1,4 @@
#EXECUTE THIS FIRST, THEN NEXT SQL: 'db_drops.sql'
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
@@ -21484,6 +21479,17 @@ CREATE TABLE IF NOT EXISTS `worldtransfers` (
INDEX (characterid)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
CREATE TABLE IF NOT EXISTS `characterexplogs` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`world_exp_rate` INT,
`exp_coupon` INT,
`gained_exp` BIGINT,
`current_exp` INT,
`exp_gain_time` DATETIME,
`charid` int(11) NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`charid`) REFERENCES `characters`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
ALTER TABLE `dueyitems`
ADD CONSTRAINT `dueyitems_ibfk_1` FOREIGN KEY (`PackageId`) REFERENCES `dueypackages` (`PackageId`) ON DELETE CASCADE;
@@ -21499,4 +21505,4 @@ ALTER TABLE `skills`
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@@ -21,12 +21,11 @@ services:
DB_HOST: "db" ## Remember if this is present it will OVERRIDE the host in the config.yaml, if you put here anything other than db, you'll need to change the config.yaml jdbc string to port 3307, and not port 3306
db:
image: mysql:8.0.23
image: mysql:8.4.0
environment:
MYSQL_RANDOM_ROOT_PASSWORD: "true"
MYSQL_DATABASE: "cosmic"
MYSQL_USER: "cosmic_server"
MYSQL_PASSWORD: "snailshell"
MYSQL_ROOT_PASSWORD: ""
MYSQL_ALLOW_EMPTY_PASSWORD: yes
ports:
- "3307:3306"
volumes:

74
pom.xml
View File

@@ -1,40 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cosmic-maplestory</groupId>
<artifactId>Cosmic</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Cosmic</name>
<description>Server emulator for Global MapleStory version 83</description>
<url>https://github.com/P0nk/Cosmic</url>
<inceptionYear>2021</inceptionYear>
<developers>
<developer>
<name>Ponk</name>
<email>ponkcode@gmail.com</email>
<url>https://github.com/P0nk</url>
<roles>
<role>maintainer</role>
<role>developer</role>
</roles>
<properties>
<discord>ponkcode</discord>
</properties>
</developer>
</developers>
<scm>
<connection>scm:git:https://github.com/P0nk/Cosmic.git</connection>
<developerConnection>scm:git:https://github.com/P0nk/Cosmic.git</developerConnection>
<url>https://github.com/P0nk/Cosmic</url>
</scm>
<issueManagement>
<system>GitHub Issues</system>
<url>https://github.com/P0nk/Cosmic/issues</url>
</issueManagement>
<ciManagement>
<system>GitHub Actions</system>
<url>https://github.com/P0nk/Cosmic/actions</url>
</ciManagement>
<properties>
<!-- Project -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<mainClass>net.server.Server</mainClass>
<!-- Maven plugins -->
<maven-surefire-plugin.version>3.0.0-M9</maven-surefire-plugin.version> <!-- For running unit tests -->
<maven-jar-plugin.version>3.3.0</maven-jar-plugin.version> <!-- Disabled. (for building thin jar) -->
<maven-assembly-plugin.version>3.5.0</maven-assembly-plugin.version> <!-- For packaging the executable fat jar -->
<maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version> <!-- For running unit tests -->
<maven-jar-plugin.version>3.4.1</maven-jar-plugin.version> <!-- Disabled. (for building thin jar) -->
<maven-assembly-plugin.version>3.7.1</maven-assembly-plugin.version> <!-- For packaging the executable fat jar -->
<!-- Dependencies -->
<slf4j-api.version>1.7.36</slf4j-api.version> <!-- Logging facade -->
<log4j.version>2.20.0</log4j.version> <!-- Slf4j implementation -->
<graalvm.version>22.3.1</graalvm.version> <!-- ScriptEngine implementation -->
<netty.version>4.1.89.Final</netty.version> <!-- Networking -->
<yamlbeans.version>1.15</yamlbeans.version> <!-- Config file -->
<slf4j-api.version>2.0.13</slf4j-api.version> <!-- Logging facade -->
<log4j.version>2.23.1</log4j.version> <!-- Slf4j implementation -->
<graalvm-js.version>23.0.4</graalvm-js.version>
<graalvm-js-scriptengine.version>24.0.1</graalvm-js-scriptengine.version> <!-- ScriptEngine implementation -->
<netty.version>4.1.109.Final</netty.version> <!-- Networking -->
<yamlbeans.version>1.17</yamlbeans.version> <!-- Config file -->
<jcip-annotations.version>1.0</jcip-annotations.version> <!-- Annotations for concurrency documentation -->
<HikariCP.version>5.0.1</HikariCP.version> <!-- Database connection pool -->
<mysql-connector-j.version>8.0.32</mysql-connector-j.version> <!-- MySQL JDBC driver -->
<jdbi-version>3.37.1</jdbi-version> <!-- Convenience wrapper around JDBC -->
<junit.version>5.9.2</junit.version> <!-- Unit test -->
<mockito.version>5.1.1</mockito.version> <!-- Unit test -->
<HikariCP.version>5.1.0</HikariCP.version> <!-- Database connection pool -->
<mysql-connector-j.version>8.4.0</mysql-connector-j.version> <!-- MySQL JDBC driver -->
<jdbi-version>3.45.1</jdbi-version> <!-- Convenience wrapper around JDBC -->
<junit.version>5.10.2</junit.version> <!-- Unit test -->
<mockito.version>5.11.0</mockito.version> <!-- Unit test -->
</properties>
<dependencies>
@@ -107,7 +139,7 @@
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
@@ -115,12 +147,12 @@
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>${graalvm.version}</version>
<version>${graalvm-js.version}</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>${graalvm.version}</version>
<version>${graalvm-js-scriptengine.version}</version>
</dependency>
<!-- Testing -->

View File

@@ -31,7 +31,7 @@ function action(mode, type, selection) {
}
if (status == 0) {
cm.sendSimple("Hey, you look like you need a breather. You should be enjoying the life, just like I am. Well, if you have a couple of items, I can trade you for an item you can play minigames with. Now... what can I do for you?#b\r\n#L0#Create a minigame item#l\r\n#L1#Explain to me what the minigames are about#l#k");
cm.sendSimple("Hey, you look like you need a breather from all that hunting. You should be enjoying the life, just like I am. Well, if you have a couple of items, I can trade you for an item you can play minigames with. Now... what can I do for you?#b\r\n#L0#Create a minigame item#l\r\n#L1#Explain to me what the minigames are about#l#k");
} else if (status == 1) {
if (selection == 0) {
@@ -57,6 +57,7 @@ function action(mode, type, selection) {
if (cm.haveItem(4030012, 15)) {
cm.gainItem(4030012, -15);
cm.gainItem(4080100, 1);
cm.dispose();
} else {
cm.sendNext("You want #bA set of Match Cards#k? Hmm...to make A set of Match Cards, you'll need some #bMonster Cards#k. Monster Card can be obtained by taking out the monsters all around the island. Collect 15 Monster Cards and you can make a set of A set of Match Cards."); //Lmfao a set of A set xD
cm.dispose();
@@ -81,7 +82,7 @@ function action(mode, type, selection) {
if (current == 1 || current == 2) {
cm.sendNextPrev("Enter the room, and when you're ready to play, click on #bReady#k.\r\nOnce the visitor clicks on #bReady#k, the room owner can press #bStart#k to begin the game. If an unwanted visitor walks in, and you don't want to play with that person, the room owner has the right to kick the visitor out of the room. There will be a square box with x written on the right of that person. Click on that for a cold goodbye, okay?"); //Oh yeah, because people WALK in Omok Rooms.
} else if (current == 3) {
if (cm.haveItem(omok1piece[selection], 99) && cm.haveItem(omok2piece[selection], 99) && cm.haveItem(4030009, 1)) {
if (cm.haveItem(omok1piece[selection], omokamount) && cm.haveItem(omok2piece[selection], omokamount) && cm.haveItem(4030009, 1)) {
cm.gainItem(omok1piece[selection], -omokamount);
cm.gainItem(omok2piece[selection], -omokamount);
cm.gainItem(4030009, -1);
@@ -95,7 +96,7 @@ function action(mode, type, selection) {
} else if (status == 5) {
if (current == 1) {
cm.sendNextPrev("When the first fame starts, #bthe room owner goes first#k. Beward that you'll be given a time limit, and you may lose your turn if you don't make your move on time. Normally, 3 x 3 is not allowed, but if there comes a point that it's absolutely necessary to put your piece there or face ending the game, then you can put it there. 3 x 3 is allowed as the last line of defense! Oh, and it won't count if it's #r6 or 7 straight#k. Only 5!");
cm.sendNextPrev("When the first game starts, #bthe room owner goes first#k. Beward that you'll be given a time limit, and you may lose your turn if you don't make your move on time. Normally, 3 x 3 is not allowed, but if there comes a point that it's absolutely necessary to put your piece there or face ending the game, then you can put it there. 3 x 3 is allowed as the last line of defense! Oh, and it won't count if it's #r6 or 7 straight#k. Only 5!");
} else if (current == 2) {
cm.sendNextPrev("Oh, and unlike Omok, when you create the game room for Match Cards, you'll need to set your game on the number of cards you'll use for the game. There are 3 modes avaliable, 3x4, 4x5, and 5x6, which will require 12, 20, and 30 cards respectively. Remember that you won't beable to change it up once the room is open, so if you really wish to change it up, you may have to close the room and open another one.");
}
@@ -110,12 +111,14 @@ function action(mode, type, selection) {
} else if (status == 7) {
if (current == 1) {
cm.sendPrev("When the next game starts, the loser will go first. Also, no one is allowed to leave in the middle of a game. If you do, you may need to request either a #bforfeit or tie#k. (Of course, if you request a forfeit, you'll lose the game.) And if you click on 'Leave' in the middle of the game and call to leave after the game, you'll leave the room right after the game is over. This will be a much more useful way to leave.");
cm.dispose();
} else if (current == 2) {
cm.sendNextPrev("If you and your opponent have the same number of matched pairs, then whoever had a longer streak of matched pairs will win. If you ever feel the need to go to the bathroom, or take an extended break, you can request a #btie#k. The game will end in a tie if the opponent accepts the request. Tip: this may be a good way to keep your friendships in tact.");
}
} else if (status == 8) {
if (current == 2) {
cm.sendPrev("When the next game starts, the loser will go first. Also, no one is allowed to leave in the middle of a game. If you do, you may need to request either a #bforfeit or tie#k. (Of course, if you request a forfeit, you'll lose the game.) And if you click on 'Leave' in the middle of the game and call to leave after the game, you'll leave the room right after the game is over. This will be a much more useful way to leave.");
cm.dispose();
}
}
}

View File

@@ -23,7 +23,12 @@ function action(mode, type, selection) {
}
if (!cm.isEventLeader()) {
cm.sendYesNo("I wish for your leader to talk to me. Alternatively, you may be wanting to quit. Are you going to abandon this campaign?");
// Player chose "No" or "End Chat"
if (mode <= 0) {
cm.dispose();
} else {
cm.sendYesNo("I wish for your leader to talk to me. Alternatively, you may be wanting to quit. Are you going to abandon this campaign?");
}
} else {
var eim = cm.getEventInstance();
if (eim == null) {

View File

@@ -59,6 +59,7 @@ import scripting.AbstractPlayerInteraction;
import scripting.event.EventInstanceManager;
import scripting.item.ItemScriptManager;
import server.*;
import server.ExpLogger.ExpLogRecord;
import server.ItemInformationProvider.ScriptedItem;
import server.events.Events;
import server.events.RescueGaga;
@@ -150,6 +151,7 @@ public class Character extends AbstractCharacterObject {
private final AtomicInteger gachaexp = new AtomicInteger();
private final AtomicInteger meso = new AtomicInteger();
private final AtomicInteger chair = new AtomicInteger(-1);
private long totalExpGained = 0;
private int merchantmeso;
private BuddyList buddylist;
private EventInstanceManager eventInstance = null;
@@ -3092,6 +3094,7 @@ public class Character extends AbstractCharacterObject {
leftover = nextExp - Integer.MAX_VALUE;
}
updateSingleStat(Stat.EXP, exp.addAndGet((int) total));
totalExpGained += total;
if (show) {
announceExpGain(gain, equip, party, inChat, white);
}
@@ -3108,6 +3111,20 @@ public class Character extends AbstractCharacterObject {
gainExpInternal(leftover, equip, party, false, inChat, white);
} else {
lastExpGainTime = System.currentTimeMillis();
if (YamlConfig.config.server.USE_EXP_GAIN_LOG) {
ExpLogRecord expLogRecord = new ExpLogger.ExpLogRecord(
getWorldServer().getExpRate(),
expCoupon,
totalExpGained,
exp.get(),
new Timestamp(lastExpGainTime),
id
);
ExpLogger.putExpLogRecord(expLogRecord);
}
totalExpGained = 0;
}
}
}

View File

@@ -47,19 +47,19 @@ public abstract class CharacterFactory {
return -1;
}
Character newchar = Character.getDefault(c);
newchar.setWorld(c.getWorld());
newchar.setSkinColor(SkinColor.getById(skin));
newchar.setGender(gender);
newchar.setName(name);
newchar.setHair(hair);
newchar.setFace(face);
Character newCharacter = Character.getDefault(c);
newCharacter.setWorld(c.getWorld());
newCharacter.setSkinColor(SkinColor.getById(skin));
newCharacter.setGender(gender);
newCharacter.setName(name);
newCharacter.setHair(hair);
newCharacter.setFace(face);
newchar.setLevel(recipe.getLevel());
newchar.setJob(recipe.getJob());
newchar.setMapId(recipe.getMap());
newCharacter.setLevel(recipe.getLevel());
newCharacter.setJob(recipe.getJob());
newCharacter.setMapId(recipe.getMap());
Inventory equipped = newchar.getInventory(InventoryType.EQUIPPED);
Inventory equipped = newCharacter.getInventory(InventoryType.EQUIPPED);
ItemInformationProvider ii = ItemInformationProvider.getInstance();
int top = recipe.getTop(), bottom = recipe.getBottom(), shoes = recipe.getShoes(), weapon = recipe.getWeapon();
@@ -88,12 +88,17 @@ public abstract class CharacterFactory {
equipped.addItemFromDB(eq_weapon.copy());
}
if (!newchar.insertNewChar(recipe)) {
if (!MakeCharInfoValidator.isNewCharacterValid(newCharacter)) {
log.warn("Owner from account {} tried to packet edit in character creation", c.getAccountName());
return -2;
}
c.sendPacket(PacketCreator.addNewCharEntry(newchar));
Server.getInstance().createCharacterEntry(newchar);
if (!newCharacter.insertNewChar(recipe)) {
return -2;
}
c.sendPacket(PacketCreator.addNewCharEntry(newCharacter));
Server.getInstance().createCharacterEntry(newCharacter);
Server.getInstance().broadcastGMMessage(c.getWorld(), PacketCreator.sendYellowTip("[New Char]: " + c.getAccountName() + " has created a new character with IGN " + name));
log.info("Account {} created chr with name {}", c.getAccountName(), name);

View File

@@ -0,0 +1,140 @@
package client.creator;
import client.Character;
import client.Job;
import client.inventory.InventoryType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import provider.Data;
import provider.DataTool;
import java.util.HashSet;
import java.util.Set;
public class MakeCharInfo {
private static final Logger log = LoggerFactory.getLogger(MakeCharInfo.class);
private static final String FACE_ID = "0";
private static final String HAIR_ID = "1";
private static final String HAIR_COLOR_ID = "2";
private static final String SKIN_ID = "3";
private static final String TOP_ID = "4";
private static final String BOTTOM_ID = "5";
private static final String SHOE_ID = "6";
private static final String WEAPON_ID = "7";
private final Set<Integer> charFaces = new HashSet<>();
private final Set<Integer> charHairs = new HashSet<>();
private final Set<Integer> charHairColors = new HashSet<>();
private final Set<Integer> charSkins = new HashSet<>();
private final Set<Integer> charTops = new HashSet<>();
private final Set<Integer> charBottoms = new HashSet<>();
private final Set<Integer> charShoes = new HashSet<>();
private final Set<Integer> charWeapons = new HashSet<>();
public MakeCharInfo(Data charInfoData) {
for (Data data : charInfoData.getChildren()) {
switch (data.getName()) {
case FACE_ID -> {
for (Data faceData : data) {
charFaces.add(DataTool.getInt(faceData));
}
}
case HAIR_ID -> {
for (Data hairData : data) {
charHairs.add(DataTool.getInt(hairData));
}
}
case HAIR_COLOR_ID -> {
for (Data hairColorData : data) {
charHairColors.add(DataTool.getInt(hairColorData));
}
}
case SKIN_ID -> {
for (Data skinData : data) {
charSkins.add(DataTool.getInt(skinData));
}
}
case TOP_ID -> {
for (Data topData : data) {
charTops.add(DataTool.getInt(topData));
}
}
case BOTTOM_ID -> {
for (Data bottomData : data) {
charBottoms.add(DataTool.getInt(bottomData));
}
}
case SHOE_ID -> {
for (Data shoeData : data) {
charShoes.add(DataTool.getInt(shoeData));
}
}
case WEAPON_ID -> {
for (Data weaponData : data) {
charWeapons.add(DataTool.getInt(weaponData));
}
}
default -> log.error("Unhandled node inside MakeCharInfo.img.xml: '" + data.getName() + "'");
}
}
}
public boolean verifyFaceId(int id) {
return this.charFaces.contains(id);
}
public boolean verifyHairId(int id) {
if (id % 10 != 0) {
return this.charHairs.contains(id - (id % 10));
}
return this.charHairs.contains(id);
}
public boolean verifyHairColorId(int id) {
return this.charHairColors.contains(id % 10);
}
public boolean verifySkinId(int id) {
return this.charSkins.contains(id);
}
public boolean verifyTopId(int id) {
return this.charTops.contains(id);
}
public boolean verifyBottomId(int id) {
return this.charBottoms.contains(id);
}
public boolean verifyShoeId(int id) {
return this.charShoes.contains(id);
}
public boolean verifyWeaponId(int id) {
return this.charWeapons.contains(id);
}
public boolean verifyCharacter(Character character) {
if (!verifyFaceId(character.getFace())) return false;
if (!verifyHairId(character.getHair())) return false;
if (!verifyHairColorId(character.getHair())) return false;
if (!verifySkinId(character.getSkinColor().getId())) return false;
// Here we only verify the equipment if the character that's being created is of type 'Beginner'
// This is because when the Maple Life A or Maple Life B items are used, the client does not send any data
// regarding what equipment the character should be wearing (as it's all handled server-side)
Job characterJob = character.getJob();
if (characterJob == Job.BEGINNER || characterJob == Job.NOBLESSE || characterJob == Job.LEGEND) {
if (!verifyTopId(character.getInventory(InventoryType.EQUIPPED).getItem((short) -5).getItemId()))
return false;
if (!verifyBottomId(character.getInventory(InventoryType.EQUIPPED).getItem((short) -6).getItemId()))
return false;
if (!verifyShoeId(character.getInventory(InventoryType.EQUIPPED).getItem((short) -7).getItemId()))
return false;
if (!verifyWeaponId(character.getInventory(InventoryType.EQUIPPED).getItem((short) -11).getItemId()))
return false;
}
return true;
}
}

View File

@@ -0,0 +1,41 @@
package client.creator;
import client.Character;
import provider.Data;
import provider.DataProviderFactory;
import provider.wz.WZFiles;
public class MakeCharInfoValidator {
private static final MakeCharInfo charFemale;
private static final MakeCharInfo charMale;
private static final MakeCharInfo orientCharFemale;
private static final MakeCharInfo orientCharMale;
private static final MakeCharInfo premiumCharFemale;
private static final MakeCharInfo premiumCharMale;
static {
Data data = DataProviderFactory.getDataProvider(WZFiles.ETC).getData("MakeCharInfo.img");
charFemale = new MakeCharInfo(data.getChildByPath("Info/CharFemale"));
charMale = new MakeCharInfo(data.getChildByPath("Info/CharMale"));
orientCharFemale = new MakeCharInfo(data.getChildByPath("OrientCharFemale"));
orientCharMale = new MakeCharInfo(data.getChildByPath("OrientCharMale"));
premiumCharFemale = new MakeCharInfo(data.getChildByPath("PremiumCharFemale"));
premiumCharMale = new MakeCharInfo(data.getChildByPath("PremiumCharMale"));
}
private static MakeCharInfo getMakeCharInfo(Character character) {
return switch (character.getJob()) {
case BEGINNER, WARRIOR, MAGICIAN, BOWMAN, THIEF, PIRATE -> character.isMale() ? charMale : charFemale;
case NOBLESSE -> character.isMale() ? premiumCharMale : premiumCharFemale;
case LEGEND -> character.isMale() ? orientCharMale : orientCharFemale;
default -> null;
};
}
public static boolean isNewCharacterValid(Character character) {
MakeCharInfo makeCharInfo = getMakeCharInfo(character);
if (makeCharInfo == null) return false;
return makeCharInfo.verifyCharacter(character);
}
}

View File

@@ -43,7 +43,6 @@ public class BeginnerCreator extends CharacterFactory {
}
public static int createCharacter(Client c, String name, int face, int hair, int skin, int top, int bottom, int shoes, int weapon, int gender) {
int status = createNewCharacter(c, name, face, hair, skin, gender, createRecipe(Job.BEGINNER, 1, MapId.MUSHROOM_TOWN, top, bottom, shoes, weapon));
return status;
return createNewCharacter(c, name, face, hair, skin, gender, createRecipe(Job.BEGINNER, 1, MapId.MUSHROOM_TOWN, top, bottom, shoes, weapon));
}
}

View File

@@ -43,7 +43,6 @@ public class LegendCreator extends CharacterFactory {
}
public static int createCharacter(Client c, String name, int face, int hair, int skin, int top, int bottom, int shoes, int weapon, int gender) {
int status = createNewCharacter(c, name, face, hair, skin, gender, createRecipe(Job.LEGEND, 1, MapId.ARAN_TUTORIAL_START, top, bottom, shoes, weapon));
return status;
return createNewCharacter(c, name, face, hair, skin, gender, createRecipe(Job.LEGEND, 1, MapId.ARAN_TUTORIAL_START, top, bottom, shoes, weapon));
}
}

View File

@@ -43,7 +43,6 @@ public class NoblesseCreator extends CharacterFactory {
}
public static int createCharacter(Client c, String name, int face, int hair, int skin, int top, int bottom, int shoes, int weapon, int gender) {
int status = createNewCharacter(c, name, face, hair, skin, gender, createRecipe(Job.NOBLESSE, 1, MapId.STARTING_MAP_NOBLESSE, top, bottom, shoes, weapon));
return status;
return createNewCharacter(c, name, face, hair, skin, gender, createRecipe(Job.NOBLESSE, 1, MapId.STARTING_MAP_NOBLESSE, top, bottom, shoes, weapon));
}
}

View File

@@ -82,6 +82,7 @@ public class ServerConfig {
public boolean USE_STARTING_AP_4;
public boolean USE_AUTOBAN;
public boolean USE_AUTOBAN_LOG;
public boolean USE_EXP_GAIN_LOG;
public boolean USE_AUTOSAVE;
public boolean USE_SERVER_AUTOASSIGNER;
public boolean USE_REFRESH_RANK_MOVE;
@@ -194,7 +195,6 @@ public class ServerConfig {
public boolean USE_PERFECT_SCROLLING;
public boolean USE_ENHANCED_CHSCROLL;
public boolean USE_ENHANCED_CRAFTING;
public boolean USE_ENHANCED_CLNSLATE;
public int SCROLL_CHANCE_ROLLS;
public int CHSCROLL_STAT_RATE;
public int CHSCROLL_STAT_RANGE;

View File

@@ -95,46 +95,6 @@ public class ItemId {
public static final int BEGINNERS_GUIDE = 4161001;
public static final int LEGENDS_GUIDE = 4161048;
public static final int NOBLESSE_GUIDE = 4161047;
public static final int SWORD = 1302000; // Weapon
public static final int HAND_AXE = 1312004;
public static final int WOODEN_CLUB = 1322005;
public static final int BASIC_POLEARM = 1442079;
public static final int WHITE_UNDERSHIRT = 1040002; // Top
public static final int UNDERSHIRT = 1040006;
public static final int GREY_TSHIRT = 1040010;
public static final int WHITE_TUBETOP = 1041002;
public static final int YELLOW_TSHIRT = 1041006;
public static final int GREEN_TSHIRT = 1041010;
public static final int RED_STRIPED_TOP = 1041011;
public static final int SIMPLE_WARRIOR_TOP = 1042167;
public static final int BLUE_JEAN_SHORTS = 1060002; // Bottom
public static final int BROWN_COTTON_SHORTS = 1060006;
public static final int RED_MINISKIRT = 1061002;
public static final int INDIGO_MINISKIRT = 1061008;
public static final int SIMPLE_WARRIOR_PANTS = 1062115;
public static final int RED_RUBBER_BOOTS = 1072001;
public static final int LEATHER_SANDALS = 1072005;
public static final int YELLOW_RUBBER_BOOTS = 1072037;
public static final int BLUE_RUBBER_BOOTS = 1072038;
public static final int AVERAGE_MUSASHI_SHOES = 1072383;
public static final int BLACK_TOBEN = 30000; // Hair
public static final int ZETA = 30010;
public static final int BLACK_REBEL = 30020;
public static final int BLACK_BUZZ = 30030;
public static final int BLACK_SAMMY = 31000;
public static final int BLACK_EDGY = 31040;
public static final int BLACK_CONNIE = 31050;
public static final int MOTIVATED_LOOK_M = 20000; // Face
public static final int PERPLEXED_STARE = 20001;
public static final int LEISURE_LOOK_M = 20002;
public static final int MOTIVATED_LOOK_F = 21000;
public static final int FEARFUL_STARE_M = 21001;
public static final int LEISURE_LOOK_F = 21002;
public static final int FEARFUL_STARE_F = 21201;
public static final int PERPLEXED_STARE_HAZEL = 20401;
public static final int LEISURE_LOOK_HAZEL = 20402;
public static final int MOTIVATED_LOOK_AMETHYST = 21700;
public static final int MOTIVATED_LOOK_BLUE = 20100;
// Warrior
public static final int RED_HWARANG_SHIRT = 1040021;

View File

@@ -22,6 +22,8 @@ public class ByteBufInPacket implements InPacket {
public byte readByte() {
return byteBuf.readByte();
}
@Override
public short readUnsignedByte() { return byteBuf.readUnsignedByte(); }
@Override
public short readShort() {

View File

@@ -4,6 +4,7 @@ import java.awt.*;
public interface InPacket extends Packet {
byte readByte();
short readUnsignedByte();
short readShort();
int readInt();
long readLong();

View File

@@ -59,7 +59,7 @@ public final class NPCMoreTalkHandler extends AbstractPacketHandler {
if (p.available() >= 4) {
selection = p.readInt();
} else if (p.available() > 0) {
selection = p.readByte();
selection = p.readUnsignedByte();
}
if (c.getQM() != null) {
if (c.getQM().isStart()) {

View File

@@ -25,8 +25,12 @@ import client.Character;
import client.Client;
import client.Skill;
import client.SkillFactory;
import client.inventory.*;
import client.inventory.Equip;
import client.inventory.Equip.ScrollResult;
import client.inventory.Inventory;
import client.inventory.InventoryType;
import client.inventory.Item;
import client.inventory.ModifyInventory;
import client.inventory.manipulator.InventoryManipulator;
import constants.id.ItemId;
import constants.inventory.ItemConstants;
@@ -37,7 +41,6 @@ import tools.PacketCreator;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author Matze
@@ -50,8 +53,8 @@ public final class ScrollHandler extends AbstractPacketHandler {
if (c.tryacquireClient()) {
try {
p.readInt(); // whatever...
short slot = p.readShort();
short dst = p.readShort();
short scrollSlot = p.readShort();
short equipSlot = p.readShort();
byte ws = (byte) p.readShort();
boolean whiteScroll = false; // white scroll being used?
boolean legendarySpirit = false; // legendary spirit skill
@@ -61,24 +64,21 @@ public final class ScrollHandler extends AbstractPacketHandler {
ItemInformationProvider ii = ItemInformationProvider.getInstance();
Character chr = c.getPlayer();
Equip toScroll = (Equip) chr.getInventory(InventoryType.EQUIPPED).getItem(dst);
Equip toScroll = (Equip) chr.getInventory(InventoryType.EQUIPPED).getItem(equipSlot);
Skill LegendarySpirit = SkillFactory.getSkill(1003);
if (chr.getSkillLevel(LegendarySpirit) > 0 && dst >= 0) {
if (chr.getSkillLevel(LegendarySpirit) > 0 && equipSlot >= 0) {
legendarySpirit = true;
toScroll = (Equip) chr.getInventory(InventoryType.EQUIP).getItem(dst);
toScroll = (Equip) chr.getInventory(InventoryType.EQUIP).getItem(equipSlot);
}
byte oldLevel = toScroll.getLevel();
byte oldSlots = toScroll.getUpgradeSlots();
Inventory useInventory = chr.getInventory(InventoryType.USE);
Item scroll = useInventory.getItem(slot);
Item scroll = useInventory.getItem(scrollSlot);
Item wscroll = null;
if (ItemConstants.isCleanSlate(scroll.getItemId())) {
Map<String, Integer> eqStats = ii.getEquipStats(toScroll.getItemId()); // clean slate issue found thanks to Masterrulax
if (eqStats == null || eqStats.get("tuc") == 0) {
announceCannotScroll(c, legendarySpirit);
return;
}
if (ItemConstants.isCleanSlate(scroll.getItemId()) && !ii.canUseCleanSlate(toScroll)) {
announceCannotScroll(c, legendarySpirit);
return;
} else if (!ItemConstants.isModifierScroll(scroll.getItemId()) && toScroll.getUpgradeSlots() < 1) {
announceCannotScroll(c, legendarySpirit); // thanks onechord for noticing zero upgrade slots freezing Legendary Scroll UI
return;
@@ -103,11 +103,6 @@ public final class ScrollHandler extends AbstractPacketHandler {
}
}
if (ItemConstants.isCleanSlate(scroll.getItemId()) && !ii.canUseCleanSlate(toScroll)) {
announceCannotScroll(c, legendarySpirit);
return;
}
Equip scrolled = (Equip) ii.scrollEquipWithId(toScroll, scroll.getItemId(), whiteScroll, 0, chr.isGM());
ScrollResult scrollSuccess = Equip.ScrollResult.FAIL; // fail
if (scrolled == null) {
@@ -141,7 +136,7 @@ public final class ScrollHandler extends AbstractPacketHandler {
if (scrollSuccess == Equip.ScrollResult.CURSE) {
if (!ItemId.isWeddingRing(toScroll.getItemId())) {
mods.add(new ModifyInventory(3, toScroll));
if (dst < 0) {
if (equipSlot < 0) {
Inventory inv = chr.getInventory(InventoryType.EQUIPPED);
inv.lockInventory();
@@ -174,7 +169,7 @@ public final class ScrollHandler extends AbstractPacketHandler {
}
c.sendPacket(PacketCreator.modifyInventory(true, mods));
chr.getMap().broadcastMessage(PacketCreator.getScrollEffect(chr.getId(), scrollSuccess, legendarySpirit, whiteScroll));
if (dst < 0 && (scrollSuccess == Equip.ScrollResult.SUCCESS || scrollSuccess == Equip.ScrollResult.CURSE)) {
if (equipSlot < 0 && (scrollSuccess == Equip.ScrollResult.SUCCESS || scrollSuccess == Equip.ScrollResult.CURSE)) {
chr.equipChanged();
}
} finally {

View File

@@ -228,10 +228,10 @@ public final class UseCashItemHandler extends AbstractPacketHandler {
return;
}
short flag = eq.getFlag();
flag |= ItemConstants.LOCK;
if (eq.getExpiration() > -1) {
if (eq.getExpiration() > -1 && (eq.getFlag() & ItemConstants.LOCK) != ItemConstants.LOCK) {
return; //No perma items pls
}
flag |= ItemConstants.LOCK;
eq.setFlag(flag);
long period = 0;
@@ -246,7 +246,8 @@ public final class UseCashItemHandler extends AbstractPacketHandler {
}
if (period > 0) {
eq.setExpiration(currentServerTime() + DAYS.toMillis(period));
long expiration = eq.getExpiration() > -1 ? eq.getExpiration() : currentServerTime();
eq.setExpiration(expiration + DAYS.toMillis(period));
}
// double-remove found thanks to BHB

View File

@@ -25,43 +25,14 @@ import client.Client;
import client.creator.novice.BeginnerCreator;
import client.creator.novice.LegendCreator;
import client.creator.novice.NoblesseCreator;
import constants.id.ItemId;
import net.AbstractPacketHandler;
import net.packet.InPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tools.PacketCreator;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public final class CreateCharHandler extends AbstractPacketHandler {
private static final Logger log = LoggerFactory.getLogger(CreateCharHandler.class);
private final static Set<Integer> IDs = new HashSet<>(Arrays.asList(
ItemId.SWORD, ItemId.HAND_AXE, ItemId.WOODEN_CLUB, ItemId.BASIC_POLEARM,// weapons
ItemId.WHITE_UNDERSHIRT, ItemId.UNDERSHIRT, ItemId.GREY_TSHIRT, ItemId.WHITE_TUBETOP, ItemId.YELLOW_TSHIRT,
ItemId.GREEN_TSHIRT, ItemId.RED_STRIPED_TOP, ItemId.SIMPLE_WARRIOR_TOP,// bottom
ItemId.BLUE_JEAN_SHORTS, ItemId.BROWN_COTTON_SHORTS, ItemId.RED_MINISKIRT, ItemId.INDIGO_MINISKIRT,
ItemId.SIMPLE_WARRIOR_PANTS, // top
ItemId.RED_RUBBER_BOOTS, ItemId.LEATHER_SANDALS, ItemId.YELLOW_RUBBER_BOOTS, ItemId.BLUE_RUBBER_BOOTS,
ItemId.AVERAGE_MUSASHI_SHOES,// shoes
ItemId.BLACK_TOBEN, ItemId.ZETA, ItemId.BLACK_REBEL, ItemId.BLACK_BUZZ, ItemId.BLACK_SAMMY,
ItemId.BLACK_EDGY, ItemId.BLACK_CONNIE,// hair
ItemId.MOTIVATED_LOOK_M, ItemId.PERPLEXED_STARE, ItemId.LEISURE_LOOK_M, ItemId.MOTIVATED_LOOK_F,
ItemId.FEARFUL_STARE_M, ItemId.LEISURE_LOOK_F, ItemId.FEARFUL_STARE_F, ItemId.PERPLEXED_STARE_HAZEL,
ItemId.LEISURE_LOOK_HAZEL, ItemId.MOTIVATED_LOOK_AMETHYST, ItemId.MOTIVATED_LOOK_BLUE //face
//#NeverTrustStevenCode
));
private static boolean isLegal(Integer toCompare) {
return IDs.contains(toCompare);
}
@Override
public final void handlePacket(InPacket p, Client c) {
public void handlePacket(InPacket p, Client c) {
String name = p.readString();
int job = p.readInt();
int face = p.readInt();
@@ -76,15 +47,6 @@ public final class CreateCharHandler extends AbstractPacketHandler {
int weapon = p.readInt();
int gender = p.readByte();
int[] items = new int[]{weapon, top, bottom, shoes, hair, face};
for (int item : items) {
if (!isLegal(item)) {
log.warn("Owner from account {} tried to packet edit in chr creation", c.getAccountName());
c.disconnect(true, false);
return;
}
}
int status;
switch (job) {
case 0: // Knights of Cygnus

View File

@@ -107,7 +107,7 @@ public class QuestScriptManager extends AbstractScriptManager {
public void end(Client c, short questid, int npc) {
Quest quest = Quest.getInstance(questid);
if (!c.getPlayer().getQuest(quest).getStatus().equals(QuestStatus.Status.STARTED) || !c.getPlayer().getMap().containsNPC(npc)) {
if (!c.getPlayer().getQuest(quest).getStatus().equals(QuestStatus.Status.STARTED) || (!c.getPlayer().getMap().containsNPC(npc) && !quest.isAutoComplete())) {
dispose(c);
return;
}

View File

@@ -0,0 +1,93 @@
package server;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.sql.Timestamp;
import static java.util.concurrent.TimeUnit.*;
import config.YamlConfig;
import tools.DatabaseConnection;
public class ExpLogger {
private static final LinkedBlockingQueue<ExpLogRecord> expLoggerQueue = new LinkedBlockingQueue<>();
private static final short EXP_LOGGER_THREAD_SLEEP_DURATION_SECONDS = 60;
private static final short EXP_LOGGER_THREAD_SHUTDOWN_WAIT_DURATION_MINUTES = 5;
public record ExpLogRecord(int worldExpRate, int expCoupon, long gainedExp, int currentExp,Timestamp expGainTime, int charid) {}
public static void putExpLogRecord(ExpLogRecord expLogRecord) {
try {
expLoggerQueue.put(expLogRecord);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static private ScheduledExecutorService schdExctr = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setPriority(Thread.MIN_PRIORITY);
return t;
}
});
private static Runnable saveExpLoggerToDBRunnable = new Runnable() {
@Override
public void run() {
try (Connection con = DatabaseConnection.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT INTO characterexplogs (world_exp_rate, exp_coupon, gained_exp, current_exp, exp_gain_time, charid) VALUES (?, ?, ?, ?, ?, ?)")) {
List<ExpLogRecord> drainedExpLogs = new ArrayList<>();
expLoggerQueue.drainTo(drainedExpLogs);
for (ExpLogRecord expLogRecord : drainedExpLogs) {
ps.setInt(1, expLogRecord.worldExpRate);
ps.setInt(2, expLogRecord.expCoupon);
ps.setLong(3, expLogRecord.gainedExp);
ps.setInt(4, expLogRecord.currentExp);
ps.setTimestamp(5, expLogRecord.expGainTime);
ps.setInt(6, expLogRecord.charid);
ps.addBatch();
}
ps.executeBatch();
} catch (SQLException sqle) {
sqle.printStackTrace();
}
}
};
private static void startExpLogger() {
schdExctr.scheduleWithFixedDelay(saveExpLoggerToDBRunnable, EXP_LOGGER_THREAD_SLEEP_DURATION_SECONDS, EXP_LOGGER_THREAD_SLEEP_DURATION_SECONDS, SECONDS);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
stopExpLogger();
}));
}
private static boolean stopExpLogger() {
schdExctr.shutdown();
try {
schdExctr.awaitTermination(EXP_LOGGER_THREAD_SHUTDOWN_WAIT_DURATION_MINUTES, MINUTES);
Thread runThreadBeforeShutdown = new Thread(saveExpLoggerToDBRunnable);
runThreadBeforeShutdown.setPriority(Thread.MIN_PRIORITY);
runThreadBeforeShutdown.start();
return true;
} catch (InterruptedException e) {
e.printStackTrace();
return false;
}
}
static {
if (YamlConfig.config.server.USE_EXP_GAIN_LOG) {
startExpLogger();
}
}
}

View File

@@ -22,9 +22,16 @@
package server;
import client.Character;
import client.*;
import client.Client;
import client.Job;
import client.Skill;
import client.SkillFactory;
import client.autoban.AutobanFactory;
import client.inventory.*;
import client.inventory.Equip;
import client.inventory.Inventory;
import client.inventory.InventoryType;
import client.inventory.Item;
import client.inventory.WeaponType;
import config.YamlConfig;
import constants.id.ItemId;
import constants.inventory.EquipSlot;
@@ -35,19 +42,36 @@ import constants.skills.NightWalker;
import net.server.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import provider.*;
import provider.Data;
import provider.DataDirectoryEntry;
import provider.DataFileEntry;
import provider.DataProvider;
import provider.DataProviderFactory;
import provider.DataTool;
import provider.wz.WZFiles;
import server.MakerItemFactory.MakerItemCreateEntry;
import server.life.LifeFactory;
import server.life.MonsterInformationProvider;
import tools.*;
import tools.DatabaseConnection;
import tools.PacketCreator;
import tools.Pair;
import tools.Randomizer;
import tools.StringUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* @author Matze
@@ -204,13 +228,13 @@ public class ItemInformationProvider {
} else if (itemId >= 1040000 && itemId < 1050000) {
theData = eqpStringData;
cat = "Eqp/Coat";
} else if (itemId >= 20000 && itemId < 22000) {
} else if (ItemConstants.isFace(itemId)) {
theData = eqpStringData;
cat = "Eqp/Face";
} else if (itemId >= 1080000 && itemId < 1090000) {
theData = eqpStringData;
cat = "Eqp/Glove";
} else if (itemId >= 30000 && itemId < 35000) {
} else if (ItemConstants.isHair(itemId)) {
theData = eqpStringData;
cat = "Eqp/Hair";
} else if (itemId >= 1050000 && itemId < 1060000) {
@@ -1025,9 +1049,16 @@ public class ItemInformationProvider {
Issue with clean slate found thanks to Masterrulax
Vicious added in the clean slate check thanks to Crypter (CrypterDEV)
*/
public boolean canUseCleanSlate(Equip nEquip) {
Map<String, Integer> eqstats = this.getEquipStats(nEquip.getItemId());
return YamlConfig.config.server.USE_ENHANCED_CLNSLATE || nEquip.getUpgradeSlots() < (byte) (eqstats.get("tuc") + nEquip.getVicious());
public boolean canUseCleanSlate(Equip equip) {
Map<String, Integer> eqStats = getEquipStats(equip.getItemId());
if (eqStats == null || eqStats.get("tuc") == 0 ) {
return false;
}
int totalUpgradeCount = eqStats.get("tuc");
int freeUpgradeCount = equip.getUpgradeSlots();
int viciousCount = equip.getVicious();
int appliedScrollCount = equip.getLevel();
return freeUpgradeCount + appliedScrollCount < totalUpgradeCount + viciousCount;
}
public Item scrollEquipWithId(Item equip, int scrollId, boolean usingWhiteScroll, int vegaItemId, boolean isGM) {

View File

@@ -190,6 +190,11 @@ public class MobSkill {
// TODO: avoid output argument banishPlayersOutput
public void applyEffect(Character player, Monster monster, boolean skill, List<Character> banishPlayersOutput) {
// See if the MobSkill is successful before doing anything
if (!makeChanceResult()) {
return;
}
Disease disease = null;
Map<MonsterStatus, Integer> stats = new EnumMap<>(MonsterStatus.class);
List<Integer> reflection = new ArrayList<>();
@@ -213,12 +218,12 @@ public class MobSkill {
case REVERSE_INPUT -> disease = Disease.CONFUSE;
case UNDEAD -> disease = Disease.ZOMBIFY;
case PHYSICAL_IMMUNE -> {
if (makeChanceResult() && !monster.isBuffed(MonsterStatus.MAGIC_IMMUNITY)) {
if (!monster.isBuffed(MonsterStatus.MAGIC_IMMUNITY)) {
stats.put(MonsterStatus.WEAPON_IMMUNITY, x);
}
}
case MAGIC_IMMUNE -> {
if (makeChanceResult() && !monster.isBuffed(MonsterStatus.WEAPON_IMMUNITY)) {
if (!monster.isBuffed(MonsterStatus.WEAPON_IMMUNITY)) {
stats.put(MonsterStatus.MAGIC_IMMUNITY, x);
}
}

View File

@@ -81,8 +81,9 @@ public class DatabaseConnection {
return true;
}
log.info("Initializing connection pool...");
final HikariConfig config = getConfig();
log.info("Initializing database connection pool. Connecting to:'{}' with user:'{}'", config.getJdbcUrl(),
config.getUsername());
Instant initStart = Instant.now();
try {
dataSource = new HikariDataSource(config);

View File

@@ -38,6 +38,36 @@ class ByteBufInPacketTest {
assertEquals(writtenByte, readByte);
}
@Test
void readUnsignedByte() {
final byte writtenByte = Byte.MAX_VALUE;
byteBuf.writeByte(writtenByte);
short readUnsignedByte = inPacket.readUnsignedByte();
assertEquals(writtenByte, readUnsignedByte);
}
@Test
void readUnsignedByte_shouldBeNonnegative() {
final byte writtenByte = Byte.MIN_VALUE;
byteBuf.writeByte(writtenByte);
short readUnsignedByte = inPacket.readUnsignedByte();
assertEquals((short)writtenByte + 256, readUnsignedByte);
}
@Test
void readUnsignedByte_shouldBeNonnegative2() {
final byte writtenByte = -1;
byteBuf.writeByte(writtenByte);
short readUnsignedByte = inPacket.readUnsignedByte();
assertEquals((short)writtenByte + 256, readUnsignedByte);
}
@Test
void readShort() {
final short writtenShort = 12_345;