Compare commits

...

59 Commits

Author SHA1 Message Date
Ponk
c9d551cd39 Merge pull request #173 from MatthewHinds/gm-security #minor
GM security changes to prevent item/mesos abuse
2023-05-29 15:50:03 +02:00
Matthew Hinds
95bf0473f3 Adjusted based on feedback 2023-05-29 14:32:01 +12:00
Matthew Hinds
a9d92b78a2 Meso drop restricted by GM level 2023-05-29 12:57:01 +12:00
Ponk
b8ebace039 Merge pull request #174 from Favouris/master #patch
Rename Monster Carnival portal scripts
2023-05-15 12:00:44 +02:00
Favouris
9223957931 Rename MCRevive6.js to MCrevive6.js 2023-05-14 19:17:38 +08:00
Favouris
5cddb7f2b6 Rename MCRevive5.js to MCrevive5.js 2023-05-14 19:17:25 +08:00
Favouris
08e7a3af16 Rename MCRevive4.js to MCrevive4.js 2023-05-14 19:17:15 +08:00
Favouris
1416cd432d Rename MCRevive3.js to MCrevive3.js 2023-05-14 19:17:03 +08:00
Favouris
2d6cf07a65 Rename MCRevive2.js to MCrevive2.js 2023-05-14 19:16:51 +08:00
Favouris
0b73d6112a Rename MCRevive1.js to MCrevive1.js 2023-05-14 19:16:40 +08:00
Ponk
b742ac0591 Merge pull request #172 from MatthewHinds/remove-levelup-messages #minor
Removed level up messages as it is non GMS like and a bit cringey
2023-05-12 08:57:51 +02:00
Matthew Hinds
4546fd44ff To prevent abuse, GMs should be permission restricted (via GM level) to trade with other non GM players, use their storage (prevent transferring to their other characters), send via Duey and to drop items. GM level is configurable. 2023-05-12 18:02:08 +12:00
Matthew Hinds
216fa9341b Removed level up messages as it is non GMS like and a bit cringey 2023-05-12 14:47:18 +12:00
Ponk
1d6d5dcc94 Merge pull request #171 from srcyscrt/docker-eclipse-temurin #patch
Use Eclipse Temurin images in the Dockerfile
2023-04-22 17:33:43 +02:00
srcyscrt
61f451694f Use Eclipse Temurin images in the Dockerfile 2023-04-22 18:43:23 +08:00
Ponk
c681f0bd82 Merge pull request #170 from Shahar6/master #patch
Fix dupe glitch with wedding
2023-04-21 19:49:17 +02:00
Shahar6
cbc0b2707e Fix dupe glitch with wedding
PoC for the glitch:
https://www.youtube.com/watch?v=EoVGQtMkJOA&ab_channel=ThirtyOneFifty
2023-04-21 20:27:59 +03:00
Ponk
36d0f8a2a0 Merge pull request #169 from FoxyYokai/Fix-Duey-Send-Item-Exploit #patch
Fix exploit with Duey Send Items
2023-04-16 08:30:05 +02:00
Sukishyou
301f65ce16 Add null check to duey packet edit check 2023-04-15 14:36:00 -05:00
Sukishyou
f1b95fe45e Fix exploit with Duey Send Items 2023-04-14 19:43:51 -05:00
Ponk
3091d747e6 Merge pull request #165 from P0nk/feat/upgrade-dependencies #patch
Upgrade dependencies
2023-03-02 18:35:59 +01:00
P0nk
f4062e5ebb Upgrade dependencies 2023-03-02 18:31:21 +01:00
Ponk
a8807f1ef0 Merge pull request #164 from P0nk/fix/custom-charset-string #patch
Fix writeString not fully respecting charset
2023-03-02 18:13:52 +01:00
P0nk
10945927c1 Fix writeString not fully respecting charset
The string would be cut short for charsets
with characters more than 1 byte.
2023-03-02 18:11:41 +01:00
Ponk
ab25f698da Merge pull request #160 from P0nk/bug/110/zenumist-portal #patch
Fix top portal in Zenumist Society (261000010) not working for GM
2023-02-17 00:48:33 +01:00
P0nk
b30e03ffb3 Fix portal in Zenumist society not working for GM chr 2023-02-17 00:42:33 +01:00
P0nk
82157c7bd1 Flatten ChangeMapHandler 2023-02-16 23:46:13 +01:00
Ponk
eb10f02ac3 Merge pull request #159 from P0nk/bug/158/id-command #patch
Fix !id command not working for certain types
2023-02-04 20:02:33 +01:00
P0nk
b1ef94ef60 Fix handbook file encoding
All handbook files are now UTF-8
2023-02-04 19:49:54 +01:00
P0nk
f89392b476 Refactor IdCommand 2023-02-04 19:46:09 +01:00
Ponk
32c4f2239d Merge pull request #155 from P0nk/feat/note-dao #minor
Refactor notes - add service and database layer
2022-12-27 16:49:55 +01:00
P0nk
404c00c2bf Merge branch 'master' into feat/note-dao 2022-12-27 15:09:38 +01:00
Ponk
9def444442 Fix version bump not obeying merge commit message (#157) #patch
* Fix version bump not obeying merge commit message

* Test the fix to bump-version

* Fix reference to specific tag of github-tag-action

* Dummy patch commit #patch

* Clean up testing of bump-version
2022-12-27 15:07:12 +01:00
P0nk
771b69d151 Merge branch 'master' into feat/note-dao 2022-12-27 14:21:43 +01:00
Ponk
cae6aa2305 Merge pull request #156 from P0nk/fix/saving-gm-report #patch
Fix saving in-game report
2022-12-27 14:12:47 +01:00
P0nk
14d80dc2f3 Fix saving "Illegal program claim" report
No chatlog is provided in the packet for this type
2022-12-27 14:08:20 +01:00
P0nk
cb38bcd270 Fix wrong timestamp format when saving report 2022-12-27 14:08:20 +01:00
P0nk
37a9a4121f Show confirmation after note is sent 2022-12-27 12:31:47 +01:00
P0nk
4731c0c60d Add tests for NoteService 2022-12-27 12:18:58 +01:00
P0nk
65111ae209 Create packet class for PacketCreator::showNotes 2022-12-27 12:18:36 +01:00
P0nk
2a460de911 Disable logging in tests 2022-12-27 12:05:51 +01:00
P0nk
cee82a08ba Remove ItemInformationProvider field in Character
This field prevented creation of Character mock in tests
2022-12-27 11:59:14 +01:00
P0nk
387437cada Workaround for Guild dependence on NoteDao 2022-12-27 11:05:00 +01:00
P0nk
af14da987e Replace FredrickProcessor dependence on NoteDao 2022-12-27 11:04:11 +01:00
P0nk
389b3ad2a4 Add NoteService to handle note operations
NoteService should be the only class with access to NoteDao;
nowhere else should NoteDao be accessed directly.

Channel dependencies are static in PacketProcessor, for now.
Ideally they would be injected in the constructor,
but since the constructor is private and I don't want to open
up that can of worms, I'll leave it like this.
At the very least, now we have a way of injecting services into
the handlers. This will make further restructuring way easier.
2022-12-27 10:34:55 +01:00
P0nk
5f1f5b7dcd Fix saving note 2022-12-27 10:24:47 +01:00
P0nk
7e3be4c45d Update README - explain hide 2022-12-26 18:09:09 +01:00
P0nk
c82881e6f2 Get rid of Character#sendNote, refactor usages of it 2022-12-26 18:09:09 +01:00
P0nk
6be1fabc55 Send wedding invitation note directly via dao 2022-12-26 18:07:41 +01:00
P0nk
4d480660b5 Rework sending notes to chr, get rid of the first method
Sending notes should not be the handled by Character
2022-12-26 18:07:41 +01:00
P0nk
1f4ce98998 Show notes using NoteDao 2022-12-26 18:07:41 +01:00
P0nk
605f2e212e Add NoteDao, used by NoteActionHandler to delete note when read 2022-12-26 18:07:41 +01:00
P0nk
188eb74a70 Add Note model object 2022-12-26 18:07:41 +01:00
P0nk
2d7d113458 Initial jdbi setup 2022-12-26 18:07:41 +01:00
Ponk
176ce6a3bd Merge pull request #150 from P0nk/bug-147/cwkpq-summons #patch
Fix error summoning master guardians on CWKPQ last stage
2022-10-16 12:32:51 +02:00
P0nk
f267b1fc0b Fix error summoning master guardians on CWKPQ last stage 2022-10-16 12:30:01 +02:00
Ponk
ea0bdb55af Merge pull request #149 from P0nk/tag-merge-commit
Tag the merge commit instead of last commit in the PR #patch
2022-10-16 11:56:24 +02:00
P0nk
4004b36bfa Tag the merge commit instead of last commit in the PR
The merge commit is more suitable since it provides some context
and groups up all the commits.
2022-10-16 11:53:29 +02:00
Ponk
d0a4c416e4 Merge pull request #148 from P0nk/versioning
Define versioning scheme
2022-10-16 10:06:55 +02:00
55 changed files with 1088 additions and 489 deletions

View File

@@ -1,10 +1,9 @@
# This workflow will tag the merge commit when merging a PR into the master branch.
# Add "#patch", "#minor", or "#major" at end of the merge commit subject to dictate the type of bump.
name: Bump version
on:
pull_request:
types:
- closed
push:
branches:
- master
@@ -18,7 +17,8 @@ jobs:
fetch-depth: '0'
- name: Bump version and push tag
uses: anothrNick/github-tag-action@v1
uses: anothrNick/github-tag-action@1.55.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WITH_V: true
WITH_V: true
BRANCH_HISTORY: last

View File

@@ -4,7 +4,7 @@
#
# Cosmic JAR creation stage
#
FROM maven:3.8.4-openjdk-17 AS jar
FROM maven:3.9.1-eclipse-temurin-17 AS jar
# Build in a separated location which won't have permissions issues.
WORKDIR /opt/cosmic
@@ -21,7 +21,7 @@ RUN mvn -f ./pom.xml clean package -Dmaven.test.skip -T 1C
#
# Server creation stage
#
FROM openjdk:17.0.2
FROM eclipse-temurin:17.0.6_10-jre
# Host the server in a location that won't have permissions issues.
WORKDIR /opt/server

View File

@@ -211,12 +211,17 @@ To launch the server, you may either:
---
### Getting into the game
If you ran the admin sql script, there already exists an account in your database with an admin character on it. You don't need to change its GM level. Log in using these credentials:
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:
* 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.
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.
After creating a character, experiment typing in all-chat "@commands".

View File

@@ -461,6 +461,12 @@ server:
#Event End Timestamp
EVENT_END_TIMESTAMP: 1428897600000
# GM Security Configuration
MINIMUM_GM_LEVEL_TO_TRADE: 4
MINIMUM_GM_LEVEL_TO_USE_STORAGE: 4
MINIMUM_GM_LEVEL_TO_USE_DUEY: 4
MINIMUM_GM_LEVEL_TO_DROP: 4
#Any NPC ids that should search for a js override script (useful if they already have wz entries since otherwise they're ignored).
NPCS_SCRIPTABLE:
#9200000: Talk to Cody # Cody

View File

@@ -264,7 +264,7 @@
5160006 - Sparkling Eyes - On the KeyConfig, configure this expression on a button of your choice. Press the button and watch your character display sparkling eyes.
5160007 - Flaming - On the KeyConfig, configure this expression on a button of your choice. Press the button and watch your character display the look of rage.
5160008 - Ray - On the KeyConfig, configure this expression on a button of your choice. Press the button and watch your character display beaming eyes.
5160009 - Goo Goo - On the KeyConfig, configure this expression on a button of your choice. Press the button and watch your character display the look of awesome!!!
5160009 - Goo Goo - On the KeyConfig, configure this expression on a button of your choice. Press the button and watch your character display the look of <EFBFBD>awesome!!!<EFBFBD>
5160010 - Whoa Whoa - On the KeyConfig, configure this expression on a button of your choice. Press the button and watch your character display the look of being flustered.
5160011 - Constant Sigh - On the KeyConfig, configure this expression on a button of your choice. Press the button and watch your character display the thinking look.
5160012 - Drool - On the KeyConfig, configure this expression on a button of your choice. Press the button and watch your character act like falling asleep.
@@ -479,9 +479,9 @@
5610001 - Vega's Spell(60%) - This winning spell from Vega enables a 90% success rate on a 60% scroll. Please check the scroll description to confirm that Vega's Spell is available for the scroll you choose.
5590000 - High-Five Stamp - Can equip items that are 5 levels above your current level.
5021026 - Gift Box Throwing Stars - A gift box that can be freely thrown around. Using the #cThrowing Star# will create an orbital effect.\n\nThis item cannot be deleted.
5010073 - Miss Popular - Well, lookie here. Someones certainly become popular with the guys. Turn this effect on and off by assigning it to a shortcut key from the keyboard settings menu.
5010073 - Miss Popular - Well, lookie here. Someone<EFBFBD>s certainly become popular with the guys. Turn this effect on and off by assigning it to a shortcut key from the keyboard settings menu.
5010074 - Mr. Popular - Well, lookie here. Someones certainly become popular with the girls. Turn this effect on and off by assigning it to a shortcut key from the keyboard settings menu.
5010074 - Mr. Popular - Well, lookie here. Someone<EFBFBD>s certainly become popular with the girls. Turn this effect on and off by assigning it to a shortcut key from the keyboard settings menu.
5240027 - Golden Drumstick - A drumstick that can be consumed only by #cBaby Tiger#. It recovers hunger and #cincreases Closeness by 100.#
5390005 - Cute Tiger Messenger - Shout to everyone in the world your character is on with this megaphone. Now available with your avatar on the top of everyone's screen! Comes with a tiger background for your avatar.

View File

@@ -1779,8 +1779,8 @@
4032103 - The Lost Treasure - The lost treasure that was stolen by the Master of Disguise who brazenly entered Ereve. Let's take it back to Irena.
4032104 - The Lost Treasure - The lost treasure that was stolen by the Master of Disguise who brazenly entered Ereve. Let's take it back to Eckhart.
4032105 - The Lost Treasure - The lost treasure that was stolen by the Master of Disguise who brazenly entered Ereve. Let's take it back to Hawkeye.
4000209 - Co-ke Slimes Bell - The bell of Co-ke Slime that has been taken off.
4000210 - Co-ke Pigs Ribbon - A piece of Co-ke Pigs ribbon.
4000209 - Co-ke Slime<EFBFBD>s Bell - The bell of Co-ke Slime that has been taken off.
4000210 - Co-ke Pig<EFBFBD>s Ribbon - A piece of Co-ke Pig<EFBFBD>s ribbon.
4000211 - Coca-Cola Cube - A cube with a drawing of a Coca-Cola bottle on it.
4000212 - CokePLAY Cube - A cube with the CokePLAY symbol.
4000213 - Coca-Cola Card - A card with a drawing of Coca-Cola.
@@ -1802,10 +1802,10 @@
4001151 - Happy Valley - Happy Valley
4001152 - Ariant - Ariant
4001153 - Magatia - Magatia
4031832 - Sophelias Portrait - A portrait of Sophelia.
4031832 - Sophelia<EFBFBD>s Portrait - A portrait of Sophelia.
4031833 - Pumpkin Juice - A juice made with pumpkin. It is very fragrant.
4031834 - Perfect Tool - The most perfect tool for making dolls.
4031835 - Lyudmillas Earring - The earrings that Lyudmilla lost. They shine with brilliance.
4031835 - Lyudmilla<EFBFBD>s Earring - The earrings that Lyudmilla lost. They shine with brilliance.
4031836 - Score - The score that Lyudmilla asked for. No one knows what kind of music is in it.
4031837 - Dumped Doll - A doll that Sophelia used to cherish a long time ago. She threw it away when she got a new doll.
4031838 - Piece of Cloth - A small piece of cloth. If you drag and drop it onto the rag doll, the doll will be completed bit by bit.
@@ -1893,26 +1893,26 @@
4032127 - Proof of Ability - The Proof of Ability that is required to learn a new skill. This proof signifies that you are ready to learn a new skill.
4032128 - Proof of Ability - The Proof of Ability that is required to learn a new skill. This proof signifies that you are ready to learn a new skill.
4032129 - Proof of Ability - The Proof of Ability that is required to learn a new skill. This proof signifies that you are ready to learn a new skill.
4032130 - Pig Doll - An adorable doll that resembles a pig. However, something doesn't seem right
4032131 - Pig Doll - An adorable doll that resembles a pig. However, something doesn't seem right
4032130 - Pig Doll - An adorable doll that resembles a pig. However, something doesn't seem right<EFBFBD>
4032131 - Pig Doll - An adorable doll that resembles a pig. However, something doesn't seem right<EFBFBD>
4032132 - Roca's Mission Report - A mission report written by Roca, the Agent for Henesys. This compiles a list of events that have taken place recently.
4032136 - Bubbling Doll - An adorable doll that was designed based on Bubbling, but something doesn't seem right. The way it smiles just seems a bit off, as if it has hidden agenda.
4032137 - Bubbling Doll - An adorable doll that was designed based on Bubbling, but something doesn't seem right. The way it smiles just seems a bit off, as if it has hidden agenda.
4032138 - Bubbling Doll - An adorable doll that was designed based on Bubbling, but something doesn't seem right. It's intears.
4032138 - Bubbling Doll - An adorable doll that was designed based on Bubbling, but something doesn't seem right. It's in<EFBFBD>tears.
4032139 - Old Key - An old, rusty key that Mr. Pickall has been looking for.
4032140 - Bubbling Doll - An adorable doll that was designed based on Bubbling, but something doesn't seem right. It's intears.
4032140 - Bubbling Doll - An adorable doll that was designed based on Bubbling, but something doesn't seem right. It's in<EFBFBD>tears.
4032141 - Mattias' Mission Report - A mission report written by Roca, the Agent for Kerning City. This compiles a list of events that have taken place recently.
4032142 - Clear Tree Sap - A clear tree sap that can only be found at the very top of the skyscraping trees. Accompanied by the freshest green scent.
4032143 - Fruit - A fruit picked from plants near Henesys.
4032144 - Hersha's Mission Report - A mission report written by Hersha, the Agent for Ellinia. This compiles a list of events that have taken place recently.
4032145 - Detector - A detector made to detect any forces of darkness. Take this to the 10 Boogies.
4032146 - Wooden Mask Doll - An adorable doll modeled after the Wooden Mask, but something doesn't seem right
4032147 - Rocky Mask Doll - An adorable doll modeled after the Stone Mask, but something doesn't seem right
4032146 - Wooden Mask Doll - An adorable doll modeled after the Wooden Mask, but something doesn't seem right<EFBFBD>
4032147 - Rocky Mask Doll - An adorable doll modeled after the Stone Mask, but something doesn't seem right<EFBFBD>
4032148 - Detector - A detector created with the sole purpose of detecting the Puppeteer's movements. We put it to good use, so let's return it to Neinheart.
4032149 - 10 Boogies' Mission Report - A mission report written by 10 Boogies, the Agents for Perion. This compiles a list of events that have taken place recently.
4032178 - Hack Attempt - A record that shows that Chief Grays tried to hack into the system.
4032179 - Ereve Investigation Permit - A permitt that allows one to investigate every part of Ereve. When the Red Alert is on, this permit is the only way you can roam freely around Ereve.
4032190 - Orange Mushroom Doll - An adorable doll modeled after the Orange Mushroom, but something doesn't seem right
4032190 - Orange Mushroom Doll - An adorable doll modeled after the Orange Mushroom, but something doesn't seem right<EFBFBD>
4032196 - Concentrated Formula: Step 1 - A formula concentrated with effective and potent ingredients. The first formula used to raise Mimiana and Mimio
4032197 - Concentrated Formula: Step 2 - A formula concentrated with effective and potent ingredients. The second formula used to raise Mimiana and Mimio.
4032198 - Concentrated Formula: Step 3 - A formula concentrated with effective and potent ingredients. The third formula used to raise Mimiana and Mimio.
@@ -2048,16 +2048,16 @@
4032270 - Glistening Sunlight - Glistening sunlight filled with the feeling of spring.
4032271 - Pure Small Sprout - A fragrant new sprout filled with the energies of spring. Used as an ingredient for Pure Perfume.
4032272 - Stationery of Longing - Pink stationary that's rumored to return a favorable response if used to send a letter to a secret crush.
4032273 - Pencil of Courage X 10 - A pencil that's used to write things you dont have the courage to say.
4032273 - Pencil of Courage X 10 - A pencil that's used to write things you don<EFBFBD>t have the courage to say.
4032275 - Pure Normal Sprout - A fragrant new sprout filled with the energies of spring. Used as an ingredient for Pure Perfume.
4032276 - Pure Sprout - A fragrant new sprout filled with the energies of spring. Used as an ingredient for Pure Perfume.
4032277 - Pure Large Sprout - A fragrant new sprout filled with the energies of spring. Used as an ingredient for Pure Perfume.
4032278 - Stationery of Deep Longing - Pink stationary that's rumored to return a favorable response if used to send a letter to a secret crush.
4032279 - Stationary of Hope and Longing - Pink stationary that's rumored to return a favorable response if used to send a letter to a secret crush.
4032280 - Letter of Love and Longing - Pink stationary that's rumored to return a favorable response if used to send a letter to a secret crush.
4032281 - Pencil of Courage X 100 - A pencil that's used to write things you dont have the courage to say.
4032282 - Pencil of Courage X 1000 - AA pencil that's used to write things you dont have the courage to say.
4032283 - Pencil of Courage X 10000 - A pencil that's used to write things you dont have the courage to say.
4032281 - Pencil of Courage X 100 - A pencil that's used to write things you don<EFBFBD>t have the courage to say.
4032282 - Pencil of Courage X 1000 - AA pencil that's used to write things you don<EFBFBD>t have the courage to say.
4032283 - Pencil of Courage X 10000 - A pencil that's used to write things you don<EFBFBD>t have the courage to say.
4032284 - Child's Broken Toy - A toy stolen from a child by a monster. It's broken and cannot be used.
4032285 - Child's Broken Toy - A toy stolen from a child by a monster. It's broken and cannot be used.
4032286 - Child's Broken Toy - A toy stolen from a child by a monster. It's broken and cannot be used.

View File

@@ -1565,9 +1565,9 @@ ossyria
200090057 - Empress' Road - To Ellinia
200090058 - Empress' Road - To Ereve
200090059 - Empress' Road - To Ellinia
200010303 - Hidden Street - Elizas Garden
211040102 - Hidden Street - Snow Souls Resting Place
209000100 - Happy Village - Cliffs Hut
200010303 - Hidden Street - Eliza<EFBFBD>s Garden
211040102 - Hidden Street - Snow Soul<EFBFBD>s Resting Place
209000100 - Happy Village - Cliff<EFBFBD>s Hut
219000000 - Hidden Street - Coke Town
219000001 - Hidden Street - Coke Town Sundry Goods Shop
219000002 - Hidden Street - House of Pouch
@@ -2863,7 +2863,7 @@ etc
980027000 - Goldrich's Maze - Maze of Erosion
980027100 - Goldrich's Maze - Maze of Erosion
980027200 - Goldrich's Maze - Maze of Erosion
980027300 - Goldrich's Maze - Beginning of the Maze
980027300 - Goldrich's Maze - Beginning of the Maze<EFBFBD>
980027400 - Goldrich's Maze - Monster's Maze
980027500 - Goldrich's Maze - Monster's Maze
980027600 - Goldrich's Maze - Monster's Maze
@@ -2944,7 +2944,7 @@ etc
980027001 - Goldrich's Maze - Maze of Erosion
980027101 - Goldrich's Maze - Maze of Erosion
980027201 - Goldrich's Maze - Maze of Erosion
980027301 - Goldrich's Maze - Beginning of the Maze
980027301 - Goldrich's Maze - Beginning of the Maze<EFBFBD>
980027401 - Goldrich's Maze - Monster's Maze
980027501 - Goldrich's Maze - Monster's Maze
980027601 - Goldrich's Maze - Monster's Maze
@@ -3025,7 +3025,7 @@ etc
980027002 - Goldrich's Maze - Maze of Erosion
980027102 - Goldrich's Maze - Maze of Erosion
980027202 - Goldrich's Maze - Maze of Erosion
980027302 - Goldrich's Maze - Beginning of the Maze
980027302 - Goldrich's Maze - Beginning of the Maze<EFBFBD>
980027402 - Goldrich's Maze - Monster's Maze
980027502 - Goldrich's Maze - Monster's Maze
980027602 - Goldrich's Maze - Monster's Maze
@@ -3349,7 +3349,7 @@ etc
913040007 - Opening - Cygnus Knights
913040008 - Opening - Cygnus Knights
913040009 - Opening - Cygnus Knights
920020000 - Hidden Street - Elizas Garden
920020000 - Hidden Street - Eliza<EFBFBD>s Garden
922020300 - Hidden Street - Origin of the Clock Tower
922220000 - Hidden Street - Gloomy Forest
922230000 - Hidden Street - Lunar World

View File

@@ -192,7 +192,7 @@
3010058 - WorldEnd - You will recover 50 HP every 10 seconds. Perhaps, as you recline, you will find the answer to many of life's questions.
3010057 - BloodyRose - You will recover 50 HP every 10 seconds. You will experience the might of a conqueror after recovery.
3010060 - Noblesse Chair - A chair makes you feel like you're sitting in the lap of luxury. Also recovers 50 HP every 10 seconds.
3010061 - Underneath the Maple Tree - A white chair created to celebrate Maple Story's 6th Anniversary. Sit on it to restore 35 HP and 10 MP every 10 seconds.
3010061 - Underneath the Maple Tree<EFBFBD> - A white chair created to celebrate Maple Story's 6th Anniversary. Sit on it to restore 35 HP and 10 MP every 10 seconds.
3010062 - Bamboo Chair - A chair that restores HP every 10 seconds when used. It's very strong since it was made from bamboo grown on Rien.
3010063 - Moon and Star Cushion - A pretty cushion shaped like a moon. Recovers 60 HP every 10 seconds.
3010064 - Male Desert Rabbit Cushion - 60 HP is restored every 10 seconds if you lean back on this cute Male Desert Rabbit Cushion.

33
pom.xml
View File

@@ -19,21 +19,22 @@
<mainClass>net.server.Server</mainClass>
<!-- Maven plugins -->
<maven-surefire-plugin.version>3.0.0-M7</maven-surefire-plugin.version> <!-- For running unit tests -->
<maven-jar-plugin.version>3.2.2</maven-jar-plugin.version> <!-- Disabled. (for building thin jar) -->
<maven-assembly-plugin.version>3.4.2</maven-assembly-plugin.version> <!-- For packaging the executable fat jar -->
<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 -->
<!-- Dependencies -->
<slf4j-api.version>1.7.36</slf4j-api.version> <!-- Logging facade -->
<log4j.version>2.18.0</log4j.version> <!-- Slf4j implementation -->
<graalvm.version>22.2.0</graalvm.version> <!-- ScriptEngine implementation -->
<netty.version>4.1.79.Final</netty.version> <!-- Networking -->
<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 -->
<jcip-annotations.version>1.0</jcip-annotations.version> <!-- Annotations for concurrency documentation -->
<HikariCP.version>5.0.1</HikariCP.version> <!-- Database connection pool -->
<mysql-connector-java.version>8.0.30</mysql-connector-java.version> <!-- MySQL JDBC driver -->
<junit.version>5.9.0</junit.version> <!-- Unit test -->
<mockito.version>4.7.0</mockito.version> <!-- Unit test -->
<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 -->
</properties>
<dependencies>
@@ -55,10 +56,16 @@
<version>${HikariCP.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql-connector-j.version}</version>
</dependency>
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-core</artifactId>
<version>${jdbi-version}</version>
</dependency>
<!-- Networking -->
<dependency>
@@ -197,4 +204,4 @@
</plugin>
</plugins>
</build>
</project>
</project>

View File

@@ -35,11 +35,13 @@ function action(mode, type, selection) {
cm.mapMessage(6, "Engarde! Master Guardians approach!");
for (var i = 0; i < 10; i++) {
var mob = eim.getMonster(9400594);
cm.getMap().spawnMonsterOnGroundBelow(mob, new java.awt.Point(-1337 + (Math.random() * 1337), 276));
const xPos = Math.floor(-1337 + (Math.random() * 1337))
cm.getMap().spawnMonsterOnGroundBelow(mob, new java.awt.Point(xPos, 276));
}
for (var i = 0; i < 20; i++) {
var mob = eim.getMonster(9400582);
cm.getMap().spawnMonsterOnGroundBelow(mob, new java.awt.Point(-1337 + (Math.random() * 1337), 276));
const xPos = Math.floor(-1337 + (Math.random() * 1337))
cm.getMap().spawnMonsterOnGroundBelow(mob, new java.awt.Point(xPos, 276));
}
eim.setIntProperty("glpq6", 1);
cm.dispose();

View File

@@ -97,7 +97,6 @@ import static java.util.concurrent.TimeUnit.*;
public class Character extends AbstractCharacterObject {
private static final Logger log = LoggerFactory.getLogger(Character.class);
private static final ItemInformationProvider ii = ItemInformationProvider.getInstance();
private static final String LEVEL_200 = "[Congrats] %s has reached Level %d! Congratulate %s on such an amazing achievement!";
private static final String[] BLOCKED_NAMES = {"admin", "owner", "moderator", "intern", "donor", "administrator", "FREDRICK", "help", "helper", "alert", "notice", "maplestory", "fuck", "wizet", "fucking", "negro", "fuk", "fuc", "penis", "pussy", "asshole", "gay",
"nigger", "homo", "suck", "cum", "shit", "shitty", "condom", "security", "official", "rape", "nigga", "sex", "tit", "boner", "orgy", "clit", "asshole", "fatass", "bitch", "support", "gamemaster", "cock", "gaay", "gm",
@@ -717,7 +716,7 @@ public class Character extends AbstractCharacterObject {
int maxbasedamage;
Item weapon_item = getInventory(InventoryType.EQUIPPED).getItem((short) -11);
if (weapon_item != null) {
maxbasedamage = calculateMaxBaseDamage(watk, ii.getWeaponType(weapon_item.getItemId()));
maxbasedamage = calculateMaxBaseDamage(watk, ItemInformationProvider.getInstance().getWeaponType(weapon_item.getItemId()));
} else {
if (job.isA(Job.PIRATE) || job.isA(Job.THUNDERBREAKER1)) {
double weapMulti = 3;
@@ -817,7 +816,7 @@ public class Character extends AbstractCharacterObject {
String medal = "";
final Item medalItem = getInventory(InventoryType.EQUIPPED).getItem((short) -49);
if (medalItem != null) {
medal = "<" + ii.getName(medalItem.getItemId()) + "> ";
medal = "<" + ItemInformationProvider.getInstance().getName(medalItem.getItemId()) + "> ";
}
return medal;
}
@@ -1873,6 +1872,7 @@ public class Character extends AbstractCharacterObject {
public boolean applyConsumeOnPickup(final int itemId) {
if (itemId / 1000000 == 2) {
ItemInformationProvider ii = ItemInformationProvider.getInstance();
if (ii.isConsumeOnPickup(itemId)) {
if (ItemConstants.isPartyItem(itemId)) {
List<Character> partyMembers = this.getPartyMembersOnSameMap();
@@ -1943,6 +1943,7 @@ public class Character extends AbstractCharacterObject {
Item mItem = mapitem.getItem();
boolean hasSpaceInventory = true;
ItemInformationProvider ii = ItemInformationProvider.getInstance();
if (ItemId.isNxCard(mapitem.getItemId()) || mapitem.getMeso() > 0 || ii.isConsumeOnPickup(mapitem.getItemId()) || (hasSpaceInventory = InventoryManipulator.checkSpace(client, mapitem.getItemId(), mItem.getQuantity(), mItem.getOwner()))) {
int mapId = this.getMapId();
@@ -2061,6 +2062,7 @@ public class Character extends AbstractCharacterObject {
}
public boolean canHoldUniques(List<Integer> itemids) {
ItemInformationProvider ii = ItemInformationProvider.getInstance();
for (Integer itemid : itemids) {
if (ii.isPickupRestricted(itemid) && this.haveItem(itemid)) {
return false;
@@ -3740,6 +3742,7 @@ public class Character extends AbstractCharacterObject {
}
public void cancelEffect(int itemId) {
ItemInformationProvider ii = ItemInformationProvider.getInstance();
cancelEffect(ii.getItemEffect(itemId), false, -1);
}
@@ -6479,7 +6482,6 @@ public class Character extends AbstractCharacterObject {
ThreadManager.getInstance().newTask(r);
}
levelUpMessages();
guildUpdate();
FamilyEntry familyEntry = getFamilyEntry();
@@ -6518,94 +6520,6 @@ public class Character extends AbstractCharacterObject {
return false;
}
}
private void levelUpMessages() {
if (level % 5 != 0) { //Performance FTW?
return;
}
if (level == 5) {
yellowMessage("Aww, you're level 5, how cute!");
} else if (level == 10) {
yellowMessage("Henesys Party Quest is now open to you! Head over to Henesys, find some friends, and try it out!");
} else if (level == 15) {
yellowMessage("Half-way to your 2nd job advancement, nice work!");
} else if (level == 20) {
yellowMessage("You can almost Kerning Party Quest!");
} else if (level == 25) {
yellowMessage("You seem to be improving, but you are still not ready to move on to the next step.");
} else if (level == 30) {
yellowMessage("You have finally reached level 30! Try job advancing, after that try the Mushroom Castle!");
} else if (level == 35) {
yellowMessage("Hey did you hear about this mall that opened in Kerning? Try visiting the Kerning Mall.");
} else if (level == 40) {
yellowMessage("Do @rates to see what all your rates are!");
} else if (level == 45) {
yellowMessage("I heard that a rock and roll artist died during the grand opening of the Kerning Mall. People are naming him the Spirit of Rock.");
} else if (level == 50) {
yellowMessage("You seem to be growing very fast, would you like to test your new found strength with the mighty Zakum?");
} else if (level == 55) {
yellowMessage("You can now try out the Ludibrium Maze Party Quest!");
} else if (level == 60) {
yellowMessage("Feels good to be near the end of 2nd job, doesn't it?");
} else if (level == 65) {
yellowMessage("You're only 5 more levels away from 3rd job, not bad!");
} else if (level == 70) {
yellowMessage("I see many people wearing a teddy bear helmet. I should ask someone where they got it from.");
} else if (level == 75) {
yellowMessage("You have reached level 3 quarters!");
} else if (level == 80) {
yellowMessage("You think you are powerful enough? Try facing horntail!");
} else if (level == 85) {
yellowMessage("Did you know? The majority of people who hit level 85 in Cosmic don't live to be 85 years old?");
} else if (level == 90) {
yellowMessage("Hey do you like the amusement park? I heard Spooky World is the best theme park around. I heard they sell cute teddy-bears.");
} else if (level == 95) {
yellowMessage("100% of people who hit level 95 in Cosmic don't live to be 95 years old.");
} else if (level == 100) {
yellowMessage("Mid-journey so far... You just reached level 100! Now THAT's such a feat, however to manage the 200 you will need even more passion and determination than ever! Good hunting!");
} else if (level == 105) {
yellowMessage("Have you ever been to leafre? I heard they have dragons!");
} else if (level == 110) {
yellowMessage("I see many people wearing a teddy bear helmet. I should ask someone where they got it from.");
} else if (level == 115) {
yellowMessage("I bet all you can think of is level 120, huh? Level 115 gets no love.");
} else if (level == 120) {
yellowMessage("Are you ready to learn from the masters? Head over to your job instructor!");
} else if (level == 125) {
yellowMessage("The struggle for mastery books has begun, huh?");
} else if (level == 130) {
yellowMessage("You should try Temple of Time. It should be pretty decent EXP.");
} else if (level == 135) {
yellowMessage("I hope you're still not struggling for mastery books!");
} else if (level == 140) {
yellowMessage("You're well into 4th job at this point, great work!");
} else if (level == 145) {
yellowMessage("Level 145 is serious business!");
} else if (level == 150) {
yellowMessage("You have becomed quite strong, but the journey is not yet over.");
} else if (level == 155) {
yellowMessage("At level 155, Zakum should be a joke to you. Nice job!");
} else if (level == 160) {
yellowMessage("Level 160 is pretty impressive. Try taking a picture and putting it on Instagram.");
} else if (level == 165) {
yellowMessage("At this level, you should start looking into doing some boss runs.");
} else if (level == 170) {
yellowMessage("Level 170, huh? You have the heart of a champion.");
} else if (level == 175) {
yellowMessage("You came a long way from level 1. Amazing job so far.");
} else if (level == 180) {
yellowMessage("Have you ever tried taking a boss on by yourself? It is quite difficult.");
} else if (level == 185) {
yellowMessage("Legend has it that you're a legend.");
} else if (level == 190) {
yellowMessage("You only have 10 more levels to go until you hit 200!");
} else if (level == 195) {
yellowMessage("Nothing is stopping you at this point, level 195!");
} else if (level == 200) {
yellowMessage("Very nicely done! You have reached the so-long dreamed LEVEL 200!!! You are truly a hero among men, cheers upon you!");
}
}
public void setPlayerRates() {
this.expRate *= GameConstants.getPlayerBonusExpRate(this.level / 20);
this.mesoRate *= GameConstants.getPlayerBonusMesoRate(this.level / 20);
@@ -6793,6 +6707,7 @@ public class Character extends AbstractCharacterObject {
return;
}
ItemInformationProvider ii = ItemInformationProvider.getInstance();
StatEffect mse = ii.getItemEffect(couponid);
mse.applyTo(this);
}
@@ -7825,6 +7740,7 @@ public class Character extends AbstractCharacterObject {
if (job.isA(Job.THIEF) || job.isA(Job.BOWMAN) || job.isA(Job.PIRATE) || job.isA(Job.NIGHTWALKER1) || job.isA(Job.WINDARCHER1)) {
Item weapon_item = getInventory(InventoryType.EQUIPPED).getItem((short) -11);
if (weapon_item != null) {
ItemInformationProvider ii = ItemInformationProvider.getInstance();
WeaponType weapon = ii.getWeaponType(weapon_item.getItemId());
boolean bow = weapon == WeaponType.BOW;
boolean crossbow = weapon == WeaponType.CROSSBOW;
@@ -8792,22 +8708,6 @@ public class Character extends AbstractCharacterObject {
return skillMacros;
}
public void sendNote(String to, String msg, byte fame) throws SQLException {
sendNote(to, this.getName(), msg, fame);
}
public static void sendNote(String to, String from, String msg, byte fame) throws SQLException {
try (Connection con = DatabaseConnection.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT INTO notes (`to`, `from`, `message`, `timestamp`, `fame`) VALUES (?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, to);
ps.setString(2, from);
ps.setString(3, msg);
ps.setLong(4, Server.getInstance().getCurrentTime());
ps.setByte(5, fame);
ps.executeUpdate();
}
}
public static void setAriantRoomLeader(int room, String charname) {
ariantroomleader[room] = charname;
}
@@ -9219,20 +9119,6 @@ public class Character extends AbstractCharacterObject {
}
}
public void changeName(String name) {
FredrickProcessor.removeFredrickReminders(this.getId());
this.name = name;
try (Connection con = DatabaseConnection.getConnection();
PreparedStatement ps = con.prepareStatement("UPDATE `characters` SET `name` = ? WHERE `id` = ?")) {
ps.setString(1, name);
ps.setInt(2, id);
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public int getDoorSlot() {
if (doorSlot != -1) {
return doorSlot;
@@ -9331,6 +9217,7 @@ public class Character extends AbstractCharacterObject {
return (-1);
}
ItemInformationProvider ii = ItemInformationProvider.getInstance();
return (sellAllItemsFromPosition(ii, type, it.getPosition()));
} finally {
inv.unlockInventory();
@@ -9413,6 +9300,7 @@ public class Character extends AbstractCharacterObject {
private List<Equip> getUpgradeableEquipped() {
List<Equip> list = new LinkedList<>();
ItemInformationProvider ii = ItemInformationProvider.getInstance();
for (Item item : getInventory(InventoryType.EQUIPPED)) {
if (ii.isUpgradeable(item.getItemId())) {
list.add((Equip) item);
@@ -9525,6 +9413,7 @@ public class Character extends AbstractCharacterObject {
private void standaloneMerge(Map<StatUpgrade, Float> statups, Client c, InventoryType type, short slot, Item item) {
short quantity;
ItemInformationProvider ii = ItemInformationProvider.getInstance();
if (item == null || (quantity = item.getQuantity()) < 1 || ii.isCash(item.getItemId()) || !ii.isUpgradeable(item.getItemId()) || hasMergeFlag(item)) {
return;
}
@@ -9619,7 +9508,7 @@ public class Character extends AbstractCharacterObject {
String medal = "";
Item medalItem = mapOwner.getInventory(InventoryType.EQUIPPED).getItem((short) -49);
if (medalItem != null) {
medal = "<" + ii.getName(medalItem.getItemId()) + "> ";
medal = "<" + ItemInformationProvider.getInstance().getName(medalItem.getItemId()) + "> ";
}
List<String> strLines = new LinkedList<>();
@@ -9640,21 +9529,6 @@ public class Character extends AbstractCharacterObject {
client.announceHint(msg, length);
}
public void showNote() {
try (Connection con = DatabaseConnection.getConnection();
PreparedStatement ps = con.prepareStatement("SELECT * FROM notes WHERE `to` = ? AND `deleted` = 0", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
ps.setString(1, this.getName());
try (ResultSet rs = ps.executeQuery()) {
rs.last();
int count = rs.getRow();
rs.first();
sendPacket(PacketCreator.showNotes(rs, count));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public void silentGiveBuffs(List<Pair<Long, PlayerBuffValueHolder>> buffs) {
for (Pair<Long, PlayerBuffValueHolder> mbsv : buffs) {
PlayerBuffValueHolder mbsvh = mbsv.getRight();
@@ -10357,6 +10231,7 @@ public class Character extends AbstractCharacterObject {
}
Collection<Item> eqpList = new LinkedHashSet<>();
ItemInformationProvider ii = ItemInformationProvider.getInstance();
for (Item it : fullList) {
if (!ii.isCash(it.getItemId())) {
eqpList.add(it);
@@ -10372,6 +10247,7 @@ public class Character extends AbstractCharacterObject {
expGain = Integer.MAX_VALUE;
}
ItemInformationProvider ii = ItemInformationProvider.getInstance();
for (Item item : getUpgradeableEquipList()) {
Equip nEquip = (Equip) item;
String itemName = ii.getName(nEquip.getItemId());
@@ -10387,6 +10263,7 @@ public class Character extends AbstractCharacterObject {
public void showAllEquipFeatures() {
String showMsg = "";
ItemInformationProvider ii = ItemInformationProvider.getInstance();
for (Item item : getInventory(InventoryType.EQUIPPED).list()) {
Equip nEquip = (Equip) item;
String itemName = ii.getName(nEquip.getItemId());

View File

@@ -3,68 +3,121 @@ package client.command.commands.gm2;
import client.Character;
import client.Client;
import client.command.Command;
import constants.game.NpcChat;
import constants.id.NpcId;
import server.ThreadManager;
import tools.exceptions.IdTypeNotSupportedException;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class IdCommand extends Command {
{
setDescription("Search in handbook.");
}
private final static int MAX_SEARCH_HITS = 100;
private final Map<String, String> handbookDirectory = typeFilePaths();
private final Map<String, HandbookFileItems> typeItems = new ConcurrentHashMap<>();
private final Map<String, String> handbookDirectory = new HashMap<>();
private final Map<String, HashMap<String, String>> itemMap = new HashMap<>();
private Map<String, String> typeFilePaths() {
return Map.ofEntries(
Map.entry("map", "handbook/Map.txt"),
Map.entry("etc", "handbook/Etc.txt"),
Map.entry("npc", "handbook/NPC.txt"),
Map.entry("use", "handbook/Use.txt"),
Map.entry("weapon", "handbook/Equip/Weapon.txt") // TODO add more into this
);
}
public IdCommand() {
handbookDirectory.put("map", "handbook/Map.txt");
handbookDirectory.put("etc", "handbook/Etc.txt");
handbookDirectory.put("npc", "handbook/NPC.txt");
handbookDirectory.put("use", "handbook/Use.txt");
handbookDirectory.put("weapon", "handbook/Equip/Weapon.txt"); // TODO add more into this
private static class HandbookFileItems {
private final List<HandbookItem> items;
public HandbookFileItems(List<String> fileLines) {
this.items = fileLines.stream()
.map(this::parseLine)
.filter(Predicate.not(Objects::isNull))
.toList();
}
private HandbookItem parseLine(String line) {
if (line == null) {
return null;
}
String[] splitLine = line.split(" - ", 2);
if (splitLine.length < 2 || splitLine[1].isBlank()) {
return null;
}
return new HandbookItem(splitLine[0], splitLine[1]);
}
public List<HandbookItem> search(String query) {
if (query == null || query.isBlank()) {
return Collections.emptyList();
}
return items.stream()
.filter(item -> item.matches(query))
.toList();
}
}
private record HandbookItem(String id, String name) {
public HandbookItem {
Objects.requireNonNull(id);
Objects.requireNonNull(name);
}
public boolean matches(String query) {
if (query == null) {
return false;
}
return this.name.toLowerCase().contains(query.toLowerCase());
}
}
@Override
public void execute(Client client, final String[] params) {
final Character player = client.getPlayer();
final Character chr = client.getPlayer();
if (params.length < 2) {
player.yellowMessage("Syntax: !id <type> <query>");
chr.yellowMessage("Syntax: !id <type> <query>");
return;
}
final String queryItem = joinStringArr(Arrays.copyOfRange(params, 1, params.length), " ");
player.yellowMessage("Querying for entry... May take some time... Please try to refine your search.");
final String type = params[0].toLowerCase();
final String[] queryItems = Arrays.copyOfRange(params, 1, params.length);
final String query = String.join(" ", queryItems);
chr.yellowMessage("Querying for entry... May take some time... Please try to refine your search.");
Runnable queryRunnable = () -> {
try {
populateIdMap(params[0].toLowerCase());
populateIdMap(type);
Map<String, String> resultList = fetchResults(itemMap.get(params[0]), queryItem);
StringBuilder sb = new StringBuilder();
final HandbookFileItems handbookFileItems = typeItems.get(type);
if (handbookFileItems == null) {
return;
}
final List<HandbookItem> searchHits = handbookFileItems.search(query);
if (resultList.size() > 0) {
int count = 0;
for (Map.Entry<String, String> entry : resultList.entrySet()) {
sb.append(String.format("Id for %s is: #b%s#k", entry.getKey(), entry.getValue()) + "\r\n");
if (++count > 100) {
break;
}
}
sb.append(String.format("Results found: #r%d#k | Returned: #b%d#k/100 | Refine search query to improve time.", resultList.size(), count) + "\r\n");
player.getAbstractPlayerInteraction().npcTalk(NpcId.MAPLE_ADMINISTRATOR, sb.toString());
if (!searchHits.isEmpty()) {
String searchHitsText = searchHits.stream()
.limit(MAX_SEARCH_HITS)
.map(item -> "Id for %s is: #b%s#k".formatted(item.name, item.id))
.collect(Collectors.joining(NpcChat.NEW_LINE));
int hitsCount = Math.min(searchHits.size(), MAX_SEARCH_HITS);
String summaryText = "Results found: #r%d#k | Returned: #b%d#k/100 | Refine search query to improve time.".formatted(searchHits.size(), hitsCount);
String fullText = searchHitsText + NpcChat.NEW_LINE + summaryText;
chr.getAbstractPlayerInteraction().npcTalk(NpcId.MAPLE_ADMINISTRATOR, fullText);
} else {
player.yellowMessage(String.format("Id not found for item: %s, of type: %s.", queryItem, params[0]));
chr.yellowMessage(String.format("Id not found for item: %s, of type: %s.", query, type));
}
} catch (IdTypeNotSupportedException e) {
player.yellowMessage("Your query type is not supported.");
chr.yellowMessage("Your query type is not supported.");
} catch (IOException e) {
player.yellowMessage("Error reading file, please contact your administrator.");
chr.yellowMessage("Error reading file, please contact your administrator.");
}
};
@@ -72,40 +125,15 @@ public class IdCommand extends Command {
}
private void populateIdMap(String type) throws IdTypeNotSupportedException, IOException {
if (!handbookDirectory.containsKey(type)) {
final String filePath = handbookDirectory.get(type);
if (filePath == null) {
throw new IdTypeNotSupportedException();
}
itemMap.put(type, new HashMap<>());
try (BufferedReader reader = Files.newBufferedReader(Path.of(handbookDirectory.get(type)))) {
String line;
while ((line = reader.readLine()) != null) {
String[] row = line.split(" - ", 2);
if (row.length == 2) {
itemMap.get(type).put(row[1].toLowerCase(), row[0]);
}
}
if (typeItems.containsKey(type)) {
return;
}
}
private String joinStringArr(String[] arr, String separator) {
if (null == arr || 0 == arr.length) {
return "";
}
StringBuilder sb = new StringBuilder(256);
sb.append(arr[0]);
for (int i = 1; i < arr.length; i++) {
sb.append(separator).append(arr[i]);
}
return sb.toString();
}
private Map<String, String> fetchResults(Map<String, String> queryMap, String queryItem) {
Map<String, String> results = new HashMap<>();
for (String item : queryMap.keySet()) {
if (item.indexOf(queryItem) != -1) {
results.put(item, queryMap.get(item));
}
}
return results;
final List<String> fileLines = Files.readAllLines(Path.of(filePath));
typeItems.put(type, new HandbookFileItems(fileLines));
}
}

View File

@@ -706,6 +706,12 @@ public class InventoryManipulator {
Inventory inv = chr.getInventory(type);
Item source = inv.getItem(src);
if (chr.isGM() && chr.gmLevel() < YamlConfig.config.server.MINIMUM_GM_LEVEL_TO_DROP) {
chr.message("You cannot drop items at your GM level.");
log.info("GM %s tried to drop item id %d", chr.getName(), source.getItemId());
return;
}
if (chr.getTrade() != null || chr.getMiniGame() != null || source == null) { //Only check needed would prob be merchants (to see if the player is in one)
return;
}

View File

@@ -284,7 +284,20 @@ public class DueyProcessor {
public static void dueySendItem(Client c, byte invTypeId, short itemPos, short amount, int sendMesos, String sendMessage, String recipient, boolean quick) {
if (c.tryacquireClient()) {
try {
if (c.getPlayer().isGM() && c.getPlayer().gmLevel() < YamlConfig.config.server.MINIMUM_GM_LEVEL_TO_USE_DUEY) {
c.getPlayer().message("You cannot use Duey to send items at your GM level.");
log.info(String.format("GM %s tried to send a package to %s", c.getPlayer().getName(), recipient));
c.sendPacket(PacketCreator.sendDueyMSG(DueyProcessor.Actions.TOCLIENT_SEND_INCORRECT_REQUEST.getCode()));
return;
}
int fee = Trade.getFee(sendMesos);
if (sendMessage != null && sendMessage.length() > 100) {
AutobanFactory.PACKET_EDIT.alert(c.getPlayer(), c.getPlayer().getName() + " tried to packet edit with Quick Delivery on duey.");
log.warn("Chr {} tried to use duey with too long of a text", c.getPlayer().getName());
c.disconnect(true, false);
return;
}
if (!quick) {
fee += 5000;
} else if (!c.getPlayer().haveItem(ItemId.QUICK_DELIVERY_TICKET)) {

View File

@@ -36,12 +36,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.ItemInformationProvider;
import server.maps.HiredMerchant;
import service.NoteService;
import tools.DatabaseConnection;
import tools.PacketCreator;
import tools.Pair;
import java.sql.*;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -54,6 +54,12 @@ public class FredrickProcessor {
private static final Logger log = LoggerFactory.getLogger(FredrickProcessor.class);
private static final int[] dailyReminders = new int[]{2, 5, 10, 15, 30, 60, 90, Integer.MAX_VALUE};
private final NoteService noteService;
public FredrickProcessor(NoteService noteService) {
this.noteService = noteService;
}
private static byte canRetrieveFromFredrick(Character chr, List<Pair<Item, InventoryType>> items) {
if (!Inventory.checkSpotsAndOwnership(chr, items)) {
List<Integer> itemids = new LinkedList<>();
@@ -127,10 +133,6 @@ public class FredrickProcessor {
}
}
public static void removeFredrickReminders(int cid) {
removeFredrickReminders(Collections.singletonList(new Pair<>(cid, 0)));
}
private static void removeFredrickReminders(List<Pair<Integer, Integer>> expiredCids) {
List<String> expiredCnames = new LinkedList<>();
for (Pair<Integer, Integer> id : expiredCids) {
@@ -153,7 +155,7 @@ public class FredrickProcessor {
}
}
public static void runFredrickSchedule() {
public void runFredrickSchedule() {
try (Connection con = DatabaseConnection.getConnection()) {
List<Pair<Integer, Integer>> expiredCids = new LinkedList<>();
List<Pair<Pair<Integer, String>, Integer>> notifCids = new LinkedList<>();
@@ -241,7 +243,7 @@ public class FredrickProcessor {
ps.addBatch();
String msg = fredrickReminderMessage(cid.getRight() - 1);
Character.sendNote(cid.getLeft().getRight(), "FREDRICK", msg, (byte) 0);
noteService.sendNormal(msg, "FREDRICK", cid.getLeft().getRight());
}
ps.executeBatch();
@@ -266,7 +268,7 @@ public class FredrickProcessor {
}
}
public static void fredrickRetrieveItems(Client c) { // thanks Gustav for pointing out the dupe on Fredrick handling
public void fredrickRetrieveItems(Client c) { // thanks Gustav for pointing out the dupe on Fredrick handling
if (c.tryacquireClient()) {
try {
Character chr = c.getPlayer();

View File

@@ -50,6 +50,8 @@ public class StorageProcessor {
ItemInformationProvider ii = ItemInformationProvider.getInstance();
Character chr = c.getPlayer();
Storage storage = chr.getStorage();
String gmBlockedStorageMessage = "You cannot use the storage as a GM of this level.";
byte mode = p.readByte();
if (chr.getLevel() < 15) {
@@ -61,7 +63,7 @@ public class StorageProcessor {
if (c.tryacquireClient()) {
try {
switch (mode) {
case 4: { // take out
case 4: { // Take out
byte type = p.readByte();
byte slot = p.readByte();
if (slot < 0 || slot > storage.getSlots()) { // removal starts at zero
@@ -70,8 +72,17 @@ public class StorageProcessor {
c.disconnect(true, false);
return;
}
slot = storage.getSlot(InventoryType.getByType(type), slot);
Item item = storage.getItem(slot);
if (hasGMRestrictions(chr)) {
chr.dropMessage(1, gmBlockedStorageMessage);
log.info(String.format("GM %s blocked from using storage", chr.getName()));
chr.sendPacket(PacketCreator.enableActions());
return;
}
if (item != null) {
if (ii.isPickupRestricted(item.getItemId()) && chr.haveItemWithId(item.getItemId(), true)) {
c.sendPacket(PacketCreator.getStorageError((byte) 0x0C));
@@ -107,7 +118,7 @@ public class StorageProcessor {
}
break;
}
case 5: { // store
case 5: { // Store
short slot = p.readShort();
int itemId = p.readInt();
short quantity = p.readShort();
@@ -120,6 +131,14 @@ public class StorageProcessor {
c.disconnect(true, false);
return;
}
if (hasGMRestrictions(chr)) {
chr.dropMessage(1, gmBlockedStorageMessage);
log.info(String.format("GM %s blocked from using storage", chr.getName()));
chr.sendPacket(PacketCreator.enableActions());
return;
}
if (quantity < 1) {
c.sendPacket(PacketCreator.enableActions());
return;
@@ -173,16 +192,24 @@ public class StorageProcessor {
}
break;
}
case 6: // arrange items
case 6: // Arrange items
if (YamlConfig.config.server.USE_STORAGE_ITEM_SORT) {
storage.arrangeItems(c);
}
c.sendPacket(PacketCreator.enableActions());
break;
case 7: { // meso
case 7: { // Mesos
int meso = p.readInt();
int storageMesos = storage.getMeso();
int playerMesos = chr.getMeso();
if (hasGMRestrictions(chr)) {
chr.dropMessage(1, gmBlockedStorageMessage);
log.info(String.format("GM %s blocked from using storage", chr.getName()));
chr.sendPacket(PacketCreator.enableActions());
return;
}
if ((meso > 0 && storageMesos >= meso) || (meso < 0 && playerMesos >= -meso)) {
if (meso < 0 && (storageMesos - meso) < 0) {
meso = Integer.MIN_VALUE + storageMesos;
@@ -208,7 +235,7 @@ public class StorageProcessor {
}
break;
}
case 8: // close... unless the player decides to enter cash shop!
case 8: // Close (unless the player decides to enter cash shop)
storage.close();
break;
}
@@ -217,4 +244,8 @@ public class StorageProcessor {
}
}
}
private static boolean hasGMRestrictions(Character character) {
return character.isGM() && character.gmLevel() < YamlConfig.config.server.MINIMUM_GM_LEVEL_TO_USE_STORAGE;
}
}

View File

@@ -309,6 +309,12 @@ public class ServerConfig {
//Event End Timestamp
public long EVENT_END_TIMESTAMP;
//GM Security Configuration
public int MINIMUM_GM_LEVEL_TO_TRADE;
public int MINIMUM_GM_LEVEL_TO_USE_STORAGE;
public int MINIMUM_GM_LEVEL_TO_USE_DUEY;
public int MINIMUM_GM_LEVEL_TO_DROP;
//Custom NPC overrides. List of NPC IDs.
public Map<String, String> NPCS_SCRIPTABLE = new HashMap<>();
}

View File

@@ -0,0 +1,8 @@
package constants.game;
public final class NpcChat {
public static final String NEW_LINE = "\r\n";
private NpcChat() {}
}

View File

@@ -0,0 +1,10 @@
package database;
import org.jdbi.v3.core.JdbiException;
public class DaoException extends JdbiException {
public DaoException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,89 @@
package database.note;
import database.DaoException;
import model.Note;
import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.JdbiException;
import tools.DatabaseConnection;
import java.util.List;
import java.util.Optional;
public class NoteDao {
public void save(Note note) {
try (Handle handle = DatabaseConnection.getHandle()) {
handle.createUpdate("""
INSERT INTO notes (`message`, `from`, `to`, `timestamp`, `fame`, `deleted`)
VALUES (?, ?, ?, ?, ?, ?)""")
.bind(0, note.message())
.bind(1, note.from())
.bind(2, note.to())
.bind(3, note.timestamp())
.bind(4, note.fame())
.bind(5, 0)
.execute();
} catch (JdbiException e) {
throw new DaoException("Failed to save note: %s".formatted(note.toString()), e);
}
}
public List<Note> findAllByTo(String to) {
try (Handle handle = DatabaseConnection.getHandle()) {
return handle.createQuery("""
SELECT *
FROM notes
WHERE `deleted` = 0
AND `to` = ?""")
.bind(0, to)
.mapTo(Note.class)
.list();
} catch (JdbiException e) {
throw new DaoException("Failed to find notes sent to: %s".formatted(to), e);
}
}
public Optional<Note> delete(int id) {
try (Handle handle = DatabaseConnection.getHandle()) {
Optional<Note> note = findById(handle, id);
if (note.isEmpty()) {
return Optional.empty();
}
deleteById(handle, id);
return note;
} catch (JdbiException e) {
throw new DaoException("Failed to delete note with id: %d".formatted(id), e);
}
}
private Optional<Note> findById(Handle handle, int id) {
final Optional<Note> note;
try {
note = handle.createQuery("""
SELECT *
FROM notes
WHERE `deleted` = 0
AND `id` = ?""")
.bind(0, id)
.mapTo(Note.class)
.findOne();
} catch (JdbiException e) {
throw new DaoException("Failed find note with id %s".formatted(id), e);
}
return note;
}
private void deleteById(Handle handle, int id) {
try {
handle.createUpdate("""
UPDATE notes
SET `deleted` = 1
WHERE `id` = ?""")
.bind(0, id)
.execute();
} catch (JdbiException e) {
throw new DaoException("Failed to delete note with id %d".formatted(id), e);
}
}
}

View File

@@ -0,0 +1,22 @@
package database.note;
import model.Note;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import java.sql.ResultSet;
import java.sql.SQLException;
public class NoteRowMapper implements RowMapper<Note> {
@Override
public Note map(ResultSet rs, StatementContext ctx) throws SQLException {
int id = rs.getInt("id");
String message = rs.getString("message");
String from = rs.getString("from");
String to = rs.getString("to");
long timestamp = rs.getLong("timestamp");
int fame = rs.getInt("fame");
return new Note(id, message, from, to, timestamp, fame);
}
}

View File

@@ -0,0 +1,21 @@
package model;
import java.util.Objects;
public record Note(int id, String message, String from, String to, long timestamp, int fame) {
private static final int PLACEHOLDER_ID = -1;
public Note {
Objects.requireNonNull(message);
Objects.requireNonNull(from);
Objects.requireNonNull(to);
}
public static Note createNormal(String message, String from, String to, long timestamp) {
return new Note(PLACEHOLDER_ID, message, from, to, timestamp, 0);
}
public static Note createGift(String message, String from, String to, long timestamp) {
return new Note(PLACEHOLDER_ID, message, from, to, timestamp, 1);
}
}

View File

@@ -0,0 +1,14 @@
package net;
import client.processor.npc.FredrickProcessor;
import service.NoteService;
import java.util.Objects;
public record ChannelDependencies(NoteService noteService, FredrickProcessor fredrickProcessor) {
public ChannelDependencies {
Objects.requireNonNull(noteService);
Objects.requireNonNull(fredrickProcessor);
}
}

View File

@@ -37,6 +37,9 @@ import java.util.Map;
public final class PacketProcessor {
private static final Logger log = LoggerFactory.getLogger(PacketProcessor.class);
private static final Map<String, PacketProcessor> instances = new LinkedHashMap<>();
private static ChannelDependencies channelDeps;
private PacketHandler[] handlers;
private PacketProcessor() {
@@ -49,11 +52,19 @@ public final class PacketProcessor {
handlers = new PacketHandler[maxRecvOp + 1];
}
public static void registerGameHandlerDependencies(ChannelDependencies channelDependencies) {
PacketProcessor.channelDeps = channelDependencies;
}
public static PacketProcessor getLoginServerProcessor() {
return getProcessor(LoginServer.WORLD_ID, LoginServer.CHANNEL_ID);
}
public static PacketProcessor getChannelServerProcessor(int world, int channel) {
if (channelDeps == null) {
throw new IllegalStateException("Unable to get channel server processor - dependencies are not registered");
}
return getProcessor(world, channel);
}
@@ -141,7 +152,7 @@ public final class PacketProcessor {
registerHandler(RecvOpcode.ITEM_SORT, new InventoryMergeHandler());
registerHandler(RecvOpcode.ITEM_MOVE, new ItemMoveHandler());
registerHandler(RecvOpcode.MESO_DROP, new MesoDropHandler());
registerHandler(RecvOpcode.PLAYER_LOGGEDIN, new PlayerLoggedinHandler());
registerHandler(RecvOpcode.PLAYER_LOGGEDIN, new PlayerLoggedinHandler(channelDeps.noteService()));
registerHandler(RecvOpcode.CHANGE_MAP, new ChangeMapHandler());
registerHandler(RecvOpcode.MOVE_LIFE, new MoveLifeHandler());
registerHandler(RecvOpcode.CLOSE_RANGE_ATTACK, new CloseRangeDamageHandler());
@@ -149,7 +160,7 @@ public final class PacketProcessor {
registerHandler(RecvOpcode.MAGIC_ATTACK, new MagicDamageHandler());
registerHandler(RecvOpcode.TAKE_DAMAGE, new TakeDamageHandler());
registerHandler(RecvOpcode.MOVE_PLAYER, new MovePlayerHandler());
registerHandler(RecvOpcode.USE_CASH_ITEM, new UseCashItemHandler());
registerHandler(RecvOpcode.USE_CASH_ITEM, new UseCashItemHandler(channelDeps.noteService()));
registerHandler(RecvOpcode.USE_ITEM, new UseItemHandler());
registerHandler(RecvOpcode.USE_RETURN_SCROLL, new UseItemHandler());
registerHandler(RecvOpcode.USE_UPGRADE_SCROLL, new ScrollHandler());
@@ -191,7 +202,7 @@ public final class PacketProcessor {
registerHandler(RecvOpcode.MESSENGER, new MessengerHandler());
registerHandler(RecvOpcode.NPC_ACTION, new NPCAnimationHandler());
registerHandler(RecvOpcode.CHECK_CASH, new TouchingCashShopHandler());
registerHandler(RecvOpcode.CASHSHOP_OPERATION, new CashOperationHandler());
registerHandler(RecvOpcode.CASHSHOP_OPERATION, new CashOperationHandler(channelDeps.noteService()));
registerHandler(RecvOpcode.COUPON_CODE, new CouponCodeHandler());
registerHandler(RecvOpcode.SPAWN_PET, new SpawnPetHandler());
registerHandler(RecvOpcode.MOVE_PET, new MovePetHandler());
@@ -204,11 +215,11 @@ public final class PacketProcessor {
registerHandler(RecvOpcode.CANCEL_DEBUFF, new CancelDebuffHandler());
registerHandler(RecvOpcode.USE_SKILL_BOOK, new SkillBookHandler());
registerHandler(RecvOpcode.SKILL_MACRO, new SkillMacroHandler());
registerHandler(RecvOpcode.NOTE_ACTION, new NoteActionHandler());
registerHandler(RecvOpcode.NOTE_ACTION, new NoteActionHandler(channelDeps.noteService()));
registerHandler(RecvOpcode.CLOSE_CHALKBOARD, new CloseChalkboardHandler());
registerHandler(RecvOpcode.USE_MOUNT_FOOD, new UseMountFoodHandler());
registerHandler(RecvOpcode.MTS_OPERATION, new MTSHandler());
registerHandler(RecvOpcode.RING_ACTION, new RingActionHandler());
registerHandler(RecvOpcode.RING_ACTION, new RingActionHandler(channelDeps.noteService()));
registerHandler(RecvOpcode.SPOUSE_CHAT, new SpouseChatHandler());
registerHandler(RecvOpcode.PET_AUTO_POT, new PetAutoPotHandler());
registerHandler(RecvOpcode.PET_EXCLUDE_ITEMS, new PetExcludeItemsHandler());
@@ -262,7 +273,7 @@ public final class PacketProcessor {
registerHandler(RecvOpcode.COCONUT, new CoconutHandler());
registerHandler(RecvOpcode.ARAN_COMBO_COUNTER, new AranComboHandler());
registerHandler(RecvOpcode.CLICK_GUIDE, new ClickGuideHandler());
registerHandler(RecvOpcode.FREDRICK_ACTION, new FredrickHandler());
registerHandler(RecvOpcode.FREDRICK_ACTION, new FredrickHandler(channelDeps.fredrickProcessor()));
registerHandler(RecvOpcode.MONSTER_CARNIVAL, new MonsterCarnivalHandler());
registerHandler(RecvOpcode.REMOTE_STORE, new RemoteStoreHandler());
registerHandler(RecvOpcode.WEDDING_ACTION, new WeddingHandler());

View File

@@ -71,8 +71,9 @@ public class ByteBufOutPacket implements OutPacket {
@Override
public void writeString(String value) {
writeShort((short) value.length());
writeBytes(value.getBytes(CharsetConstants.CHARSET));
byte[] bytes = value.getBytes(CharsetConstants.CHARSET);
writeShort(bytes.length);
writeBytes(bytes);
}
@Override

View File

@@ -0,0 +1,13 @@
package net.packet.out;
import net.opcodes.SendOpcode;
import net.packet.ByteBufOutPacket;
public final class SendNoteSuccessPacket extends ByteBufOutPacket {
public SendNoteSuccessPacket() {
super(SendOpcode.MEMO_RESULT);
writeByte(4);
}
}

View File

@@ -0,0 +1,30 @@
package net.packet.out;
import model.Note;
import net.opcodes.SendOpcode;
import net.packet.ByteBufOutPacket;
import java.util.List;
import java.util.Objects;
import static tools.PacketCreator.getTime;
public final class ShowNotesPacket extends ByteBufOutPacket {
public ShowNotesPacket(List<Note> notes) {
super(SendOpcode.MEMO_RESULT);
Objects.requireNonNull(notes);
writeByte(3);
writeByte(notes.size());
notes.forEach(this::writeNote);
}
private void writeNote(Note note) {
writeInt(note.id());
writeString(note.from() + " "); //Stupid nexon forgot space lol
writeString(note.message());
writeLong(getTime(note.timestamp()));
writeByte(note.fame());
}
}

View File

@@ -30,11 +30,15 @@ import client.inventory.Item;
import client.inventory.ItemFactory;
import client.inventory.manipulator.CashIdGenerator;
import client.newyear.NewYearCardRecord;
import client.processor.npc.FredrickProcessor;
import config.YamlConfig;
import constants.game.GameConstants;
import constants.inventory.ItemConstants;
import constants.net.OpcodeConstants;
import constants.net.ServerConstants;
import database.note.NoteDao;
import net.ChannelDependencies;
import net.PacketProcessor;
import net.netty.LoginServer;
import net.packet.Packet;
import net.server.channel.Channel;
@@ -55,6 +59,7 @@ import server.TimerManager;
import server.expeditions.ExpeditionBossLog;
import server.life.PlayerNPCFactory;
import server.quest.Quest;
import service.NoteService;
import tools.DatabaseConnection;
import tools.Pair;
@@ -91,6 +96,7 @@ public class Server {
private static final Set<Integer> activeFly = new HashSet<>();
private static final Map<Integer, Integer> couponRates = new HashMap<>(30);
private static final List<Integer> activeCoupons = new LinkedList<>();
private static ChannelDependencies channelDependencies;
private LoginServer loginServer;
private final List<Map<Integer, String>> channels = new LinkedList<>();
@@ -838,6 +844,8 @@ public class Server {
throw new IllegalStateException("Failed to initiate a connection to the database");
}
channelDependencies = registerChannelDependencies();
final ExecutorService initExecutor = Executors.newFixedThreadPool(10);
// Run slow operations asynchronously to make startup faster
final List<Future<?>> futures = new ArrayList<>();
@@ -866,7 +874,7 @@ public class Server {
}
ThreadManager.getInstance().start();
initializeTimelyTasks(); // aggregated method for timely tasks thanks to lxconan
initializeTimelyTasks(channelDependencies); // aggregated method for timely tasks thanks to lxconan
try {
int worldCount = Math.min(GameConstants.WORLD_NAMES.length, YamlConfig.config.server.WORLDS);
@@ -914,6 +922,16 @@ public class Server {
}
}
private ChannelDependencies registerChannelDependencies() {
NoteService noteService = new NoteService(new NoteDao());
FredrickProcessor fredrickProcessor = new FredrickProcessor(noteService);
ChannelDependencies channelDependencies = new ChannelDependencies(noteService, fredrickProcessor);
PacketProcessor.registerGameHandlerDependencies(channelDependencies);
return channelDependencies;
}
private LoginServer initLoginServer(int port) {
LoginServer loginServer = new LoginServer(port);
loginServer.start();
@@ -932,7 +950,7 @@ public class Server {
}
}
private void initializeTimelyTasks() {
private void initializeTimelyTasks(ChannelDependencies channelDependencies) {
TimerManager tMan = TimerManager.getInstance();
tMan.start();
tMan.register(tMan.purge(), YamlConfig.config.server.PURGING_INTERVAL);//Purging ftw...
@@ -946,7 +964,7 @@ public class Server {
tMan.register(new LoginCoordinatorTask(), HOURS.toMillis(1), timeLeft);
tMan.register(new EventRecallCoordinatorTask(), HOURS.toMillis(1), timeLeft);
tMan.register(new LoginStorageTask(), MINUTES.toMillis(2), MINUTES.toMillis(2));
tMan.register(new DueyFredrickTask(), HOURS.toMillis(1), timeLeft);
tMan.register(new DueyFredrickTask(channelDependencies.fredrickProcessor()), HOURS.toMillis(1), timeLeft);
tMan.register(new InvitationTask(), SECONDS.toMillis(30), SECONDS.toMillis(30));
tMan.register(new RespawnTask(), YamlConfig.config.server.RESPAWN_INTERVAL, YamlConfig.config.server.RESPAWN_INTERVAL);
@@ -1164,7 +1182,7 @@ public class Server {
public void expelMember(GuildCharacter initiator, String name, int cid) {
Guild g = guilds.get(initiator.getGuildId());
if (g != null) {
g.expelMember(initiator, name, cid);
g.expelMember(initiator, name, cid, channelDependencies.noteService());
}
}

View File

@@ -41,10 +41,10 @@ import server.CashShop;
import server.CashShop.CashItem;
import server.CashShop.CashItemFactory;
import server.ItemInformationProvider;
import service.NoteService;
import tools.PacketCreator;
import tools.Pair;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
@@ -54,6 +54,12 @@ import static java.util.concurrent.TimeUnit.DAYS;
public final class CashOperationHandler extends AbstractPacketHandler {
private static final Logger log = LoggerFactory.getLogger(CashOperationHandler.class);
private final NoteService noteService;
public CashOperationHandler(NoteService noteService) {
this.noteService = noteService;
}
@Override
public void handlePacket(InPacket p, Client c) {
Character chr = c.getPlayer();
@@ -128,14 +134,13 @@ public final class CashOperationHandler extends AbstractPacketHandler {
cs.gift(Integer.parseInt(recipient.get("id")), chr.getName(), message, cItem.getSN());
c.sendPacket(PacketCreator.showGiftSucceed(recipient.get("name"), cItem));
c.sendPacket(PacketCreator.showCash(chr));
try {
chr.sendNote(recipient.get("name"), chr.getName() + " has sent you a gift! Go check out the Cash Shop.", (byte) 0); //fame or not
} catch (SQLException ex) {
ex.printStackTrace();
}
String noteMessage = chr.getName() + " has sent you a gift! Go check out the Cash Shop.";
noteService.sendNormal(noteMessage, chr.getName(), recipient.get("name"));
Character receiver = c.getChannelServer().getPlayerStorage().getCharacterByName(recipient.get("name"));
if (receiver != null) {
receiver.showNote();
noteService.show(receiver);
}
} else if (action == 0x05) { // Modify wish list
cs.clearWishList();
@@ -330,12 +335,8 @@ public final class CashOperationHandler extends AbstractPacketHandler {
cs.gainCash(toCharge, itemRing, chr.getWorld());
cs.gift(partner.getId(), chr.getName(), text, eqp.getSN(), rings.getRight());
chr.addCrushRing(Ring.loadFromDb(rings.getLeft()));
try {
chr.sendNote(partner.getName(), text, (byte) 1);
} catch (SQLException ex) {
ex.printStackTrace();
}
partner.showNote();
noteService.sendWithFame(text, chr.getName(), partner.getName());
noteService.show(partner);
}
}
} else {
@@ -393,12 +394,8 @@ public final class CashOperationHandler extends AbstractPacketHandler {
cs.gainCash(payment, -itemRing.getPrice());
cs.gift(partner.getId(), chr.getName(), text, eqp.getSN(), rings.getRight());
chr.addFriendshipRing(Ring.loadFromDb(rings.getLeft()));
try {
chr.sendNote(partner.getName(), text, (byte) 1);
} catch (SQLException ex) {
ex.printStackTrace();
}
partner.showNote();
noteService.sendWithFame(text, chr.getName(), partner.getName());
noteService.show(partner);
}
}
} else {

View File

@@ -58,126 +58,137 @@ public final class ChangeMapHandler extends AbstractPacketHandler {
if (chr.getTrade() != null) {
Trade.cancelTrade(chr, Trade.TradeResult.UNSUCCESSFUL_ANOTHER_MAP);
}
if (p.available() == 0) { //Cash Shop :)
if (!chr.getCashShop().isOpened()) {
c.disconnect(false, false);
return;
boolean enteringMapFromCashShop = p.available() == 0;
if (enteringMapFromCashShop) {
enterFromCashShop(c);
return;
}
if (chr.getCashShop().isOpened()) {
c.disconnect(false, false);
return;
}
try {
p.readByte(); // 1 = from dying 0 = regular portals
int targetMapId = p.readInt();
String portalName = p.readString();
Portal portal = chr.getMap().getPortal(portalName);
p.readByte();
boolean wheel = p.readByte() > 0;
boolean chasing = p.readByte() == 1 && chr.isGM() && p.available() == 2 * Integer.BYTES;
if (chasing) {
chr.setChasing(true);
chr.setPosition(new Point(p.readInt(), p.readInt()));
}
String[] socket = c.getChannelServer().getIP().split(":");
chr.getCashShop().open(false);
chr.setSessionTransitionState();
try {
c.sendPacket(PacketCreator.getChannelChange(InetAddress.getByName(socket[0]), Integer.parseInt(socket[1])));
} catch (UnknownHostException ex) {
ex.printStackTrace();
}
} else {
if (chr.getCashShop().isOpened()) {
c.disconnect(false, false);
return;
}
try {
p.readByte(); // 1 = from dying 0 = regular portals
int targetid = p.readInt();
String startwp = p.readString();
Portal portal = chr.getMap().getPortal(startwp);
p.readByte();
boolean wheel = p.readByte() > 0;
if (targetMapId != -1) {
if (!chr.isAlive()) {
MapleMap map = chr.getMap();
if (wheel && chr.haveItemWithId(ItemId.WHEEL_OF_FORTUNE, false)) {
// thanks lucasziron (lziron) for showing revivePlayer() triggering by Wheel
boolean chasing = p.readByte() == 1 && chr.isGM();
if (chasing) {
chr.setChasing(true);
chr.setPosition(new Point(p.readInt(), p.readInt()));
}
InventoryManipulator.removeById(c, InventoryType.CASH, ItemId.WHEEL_OF_FORTUNE, 1, true, false);
chr.sendPacket(PacketCreator.showWheelsLeft(chr.getItemQuantity(ItemId.WHEEL_OF_FORTUNE, false)));
if (targetid != -1) {
if (!chr.isAlive()) {
MapleMap map = chr.getMap();
if (wheel && chr.haveItemWithId(ItemId.WHEEL_OF_FORTUNE, false)) {
// thanks lucasziron (lziron) for showing revivePlayer() triggering by Wheel
InventoryManipulator.removeById(c, InventoryType.CASH, ItemId.WHEEL_OF_FORTUNE, 1, true, false);
chr.sendPacket(PacketCreator.showWheelsLeft(chr.getItemQuantity(ItemId.WHEEL_OF_FORTUNE, false)));
chr.updateHp(50);
chr.changeMap(map, map.findClosestPlayerSpawnpoint(chr.getPosition()));
} else {
boolean executeStandardPath = true;
if (chr.getEventInstance() != null) {
executeStandardPath = chr.getEventInstance().revivePlayer(chr);
chr.updateHp(50);
chr.changeMap(map, map.findClosestPlayerSpawnpoint(chr.getPosition()));
} else {
boolean executeStandardPath = true;
if (chr.getEventInstance() != null) {
executeStandardPath = chr.getEventInstance().revivePlayer(chr);
}
if (executeStandardPath) {
chr.respawn(map.getReturnMapId());
}
}
} else {
if (chr.isGM()) {
MapleMap to = chr.getWarpMap(targetMapId);
chr.changeMap(to, to.getPortal(0));
} else {
final int divi = chr.getMapId() / 100;
boolean warp = false;
if (divi == 0) {
if (targetMapId == 10000) {
warp = true;
}
if (executeStandardPath) {
chr.respawn(map.getReturnMapId());
} else if (divi == 20100) {
if (targetMapId == MapId.LITH_HARBOUR) {
c.sendPacket(PacketCreator.lockUI(false));
c.sendPacket(PacketCreator.disableUI(false));
warp = true;
}
} else if (divi == 9130401) { // Only allow warp if player is already in Intro map, or else = hack
if (targetMapId == MapId.EREVE || targetMapId / 100 == 9130401) { // Cygnus introduction
warp = true;
}
} else if (divi == 9140900) { // Aran Introduction
if (targetMapId == MapId.ARAN_TUTO_2 || targetMapId == MapId.ARAN_TUTO_3 || targetMapId == MapId.ARAN_TUTO_4 || targetMapId == MapId.ARAN_INTRO) {
warp = true;
}
} else if (divi / 10 == 1020) { // Adventurer movie clip Intro
if (targetMapId == 1020000) {
warp = true;
}
} else if (divi / 10 >= 980040 && divi / 10 <= 980045) {
if (targetMapId == MapId.WITCH_TOWER_ENTRANCE) {
warp = true;
}
}
} else {
if (chr.isGM()) {
MapleMap to = chr.getWarpMap(targetid);
if (warp) {
final MapleMap to = chr.getWarpMap(targetMapId);
chr.changeMap(to, to.getPortal(0));
} else {
final int divi = chr.getMapId() / 100;
boolean warp = false;
if (divi == 0) {
if (targetid == 10000) {
warp = true;
}
} else if (divi == 20100) {
if (targetid == MapId.LITH_HARBOUR) {
c.sendPacket(PacketCreator.lockUI(false));
c.sendPacket(PacketCreator.disableUI(false));
warp = true;
}
} else if (divi == 9130401) { // Only allow warp if player is already in Intro map, or else = hack
if (targetid == MapId.EREVE || targetid / 100 == 9130401) { // Cygnus introduction
warp = true;
}
} else if (divi == 9140900) { // Aran Introduction
if (targetid == MapId.ARAN_TUTO_2 || targetid == MapId.ARAN_TUTO_3 || targetid == MapId.ARAN_TUTO_4 || targetid == MapId.ARAN_INTRO) {
warp = true;
}
} else if (divi / 10 == 1020) { // Adventurer movie clip Intro
if (targetid == 1020000) {
warp = true;
}
} else if (divi / 10 >= 980040 && divi / 10 <= 980045) {
if (targetid == MapId.WITCH_TOWER_ENTRANCE) {
warp = true;
}
}
if (warp) {
final MapleMap to = chr.getWarpMap(targetid);
chr.changeMap(to, to.getPortal(0));
}
}
}
}
}
if (portal != null && !portal.getPortalStatus()) {
c.sendPacket(PacketCreator.blockedMessage(1));
if (portal != null && !portal.getPortalStatus()) {
c.sendPacket(PacketCreator.blockedMessage(1));
c.sendPacket(PacketCreator.enableActions());
return;
}
if (chr.getMapId() == MapId.FITNESS_EVENT_LAST) {
chr.getFitness().resetTimes();
} else if (chr.getMapId() == MapId.OLA_EVENT_LAST_1 || chr.getMapId() == MapId.OLA_EVENT_LAST_2) {
chr.getOla().resetTimes();
}
if (portal != null) {
if (portal.getPosition().distanceSq(chr.getPosition()) > 400000) {
c.sendPacket(PacketCreator.enableActions());
return;
}
if (chr.getMapId() == MapId.FITNESS_EVENT_LAST) {
chr.getFitness().resetTimes();
} else if (chr.getMapId() == MapId.OLA_EVENT_LAST_1 || chr.getMapId() == MapId.OLA_EVENT_LAST_2) {
chr.getOla().resetTimes();
}
if (portal != null) {
if (portal.getPosition().distanceSq(chr.getPosition()) > 400000) {
c.sendPacket(PacketCreator.enableActions());
return;
}
portal.enterPortal(c);
} else {
c.sendPacket(PacketCreator.enableActions());
}
} catch (Exception e) {
e.printStackTrace();
portal.enterPortal(c);
} else {
c.sendPacket(PacketCreator.enableActions());
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void enterFromCashShop(Client c) {
final Character chr = c.getPlayer();
if (!chr.getCashShop().isOpened()) {
c.disconnect(false, false);
return;
}
String[] socket = c.getChannelServer().getIP().split(":");
chr.getCashShop().open(false);
chr.setSessionTransitionState();
try {
c.sendPacket(PacketCreator.getChannelChange(InetAddress.getByName(socket[0]), Integer.parseInt(socket[1])));
} catch (UnknownHostException ex) {
ex.printStackTrace();
}
}
}

View File

@@ -31,6 +31,11 @@ import net.packet.InPacket;
* @author kevintjuh93
*/
public class FredrickHandler extends AbstractPacketHandler {
private final FredrickProcessor fredrickProcessor;
public FredrickHandler(FredrickProcessor fredrickProcessor) {
this.fredrickProcessor = fredrickProcessor;
}
@Override
public void handlePacket(InPacket p, Client c) {
@@ -42,7 +47,7 @@ public class FredrickHandler extends AbstractPacketHandler {
//c.sendPacket(PacketCreator.getFredrick((byte) 0x24));
break;
case 0x1A:
FredrickProcessor.fredrickRetrieveItems(c);
fredrickProcessor.fredrickRetrieveItems(c);
break;
case 0x1C: //Exit
break;

View File

@@ -23,6 +23,7 @@ package net.server.channel.handlers;
import client.Character;
import client.Client;
import config.YamlConfig;
import net.AbstractPacketHandler;
import net.packet.InPacket;
import tools.PacketCreator;
@@ -42,6 +43,11 @@ public final class MesoDropHandler extends AbstractPacketHandler {
p.skip(4);
int meso = p.readInt();
if (player.isGM() && player.gmLevel() < YamlConfig.config.server.MINIMUM_GM_LEVEL_TO_DROP) {
player.message("You cannot drop mesos at your GM level.");
return;
}
if (c.tryacquireClient()) { // thanks imbee for noticing players not being able to throw mesos too fast
try {
if (meso <= player.getMeso() && meso > 9 && meso < 50001) {

View File

@@ -22,34 +22,40 @@
package net.server.channel.handlers;
import client.Client;
import model.Note;
import net.AbstractPacketHandler;
import net.packet.InPacket;
import tools.DatabaseConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import service.NoteService;
import tools.PacketCreator;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
public final class NoteActionHandler extends AbstractPacketHandler {
private static final Logger log = LoggerFactory.getLogger(NoteActionHandler.class);
private final NoteService noteService;
public NoteActionHandler(NoteService noteService) {
this.noteService = noteService;
}
@Override
public final void handlePacket(InPacket p, Client c) {
public void handlePacket(InPacket p, Client c) {
int action = p.readByte();
if (action == 0 && c.getPlayer().getCashShop().getAvailableNotes() > 0) {
if (action == 0 && c.getPlayer().getCashShop().getAvailableNotes() > 0) { // Reply to gift in cash shop
String charname = p.readString();
String message = p.readString();
try {
if (c.getPlayer().getCashShop().isOpened()) {
c.sendPacket(PacketCreator.showCashInventory(c));
}
c.getPlayer().sendNote(charname, message, (byte) 1);
c.getPlayer().getCashShop().decreaseNotes();
} catch (SQLException e) {
e.printStackTrace();
if (c.getPlayer().getCashShop().isOpened()) {
c.sendPacket(PacketCreator.showCashInventory(c));
}
} else if (action == 1) {
boolean sendNoteSuccess = noteService.sendWithFame(message, c.getPlayer().getName(), charname);
if (sendNoteSuccess) {
c.getPlayer().getCashShop().decreaseNotes();
}
} else if (action == 1) { // Discard notes in game
int num = p.readByte();
p.readByte();
p.readByte();
@@ -58,24 +64,13 @@ public final class NoteActionHandler extends AbstractPacketHandler {
int id = p.readInt();
p.readByte(); //Fame, but we read it from the database :)
try (Connection con = DatabaseConnection.getConnection()) {
try (PreparedStatement ps = con.prepareStatement("SELECT `fame` FROM notes WHERE id=? AND deleted=0")) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
fame += rs.getInt("fame");
}
}
}
try (PreparedStatement ps = con.prepareStatement("UPDATE notes SET `deleted` = 1 WHERE id = ?")) {
ps.setInt(1, id);
ps.executeUpdate();
}
} catch (SQLException e) {
e.printStackTrace();
Optional<Note> discardedNote = noteService.delete(id);
if (discardedNote.isEmpty()) {
log.warn("Note with id {} not able to be discarded. Already discarded?", id);
continue;
}
fame += discardedNote.get().fame();
}
if (fame > 0) {
c.getPlayer().gainFame(fame);

View File

@@ -46,6 +46,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import scripting.event.EventInstanceManager;
import server.life.MobSkill;
import service.NoteService;
import tools.DatabaseConnection;
import tools.PacketCreator;
import tools.Pair;
@@ -63,6 +64,12 @@ public final class PlayerLoggedinHandler extends AbstractPacketHandler {
private static final Logger log = LoggerFactory.getLogger(PlayerLoggedinHandler.class);
private static final Set<Integer> attemptingLoginAccounts = new HashSet<>();
private final NoteService noteService;
public PlayerLoggedinHandler(NoteService noteService) {
this.noteService = noteService;
}
private boolean tryAcquireAccount(int accId) {
synchronized (attemptingLoginAccounts) {
if (attemptingLoginAccounts.contains(accId)) {
@@ -302,7 +309,8 @@ public final class PlayerLoggedinHandler extends AbstractPacketHandler {
}
}
player.showNote();
noteService.show(player);
if (player.getParty() != null) {
PartyCharacter pchar = player.getMPC();

View File

@@ -32,7 +32,8 @@ import tools.PacketCreator;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.OffsetDateTime;
import java.sql.Timestamp;
import java.time.Instant;
/*
*
@@ -40,7 +41,7 @@ import java.time.OffsetDateTime;
*/
public final class ReportHandler extends AbstractPacketHandler {
public final void handlePacket(InPacket p, Client c) {
int type = p.readByte(); //01 = Conversation claim 00 = illegal program
int type = p.readByte(); //00 = Illegal program claim, 01 = Conversation claim
String victim = p.readString();
int reason = p.readByte();
String description = p.readString();
@@ -58,7 +59,7 @@ public final class ReportHandler extends AbstractPacketHandler {
return;
}
Server.getInstance().broadcastGMMessage(c.getWorld(), PacketCreator.serverNotice(6, victim + " was reported for: " + description));
addReport(c.getPlayer().getId(), Character.getIdByName(victim), 0, description, null);
addReport(c.getPlayer().getId(), Character.getIdByName(victim), 0, description, "");
} else if (type == 1) {
String chatlog = p.readString();
if (chatlog == null) {
@@ -80,17 +81,16 @@ public final class ReportHandler extends AbstractPacketHandler {
}
}
public void addReport(int reporterid, int victimid, int reason, String description, String chatlog) {
private void addReport(int reporterid, int victimid, int reason, String description, String chatlog) {
try (Connection con = DatabaseConnection.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT INTO reports (`reporttime`, `reporterid`, `victimid`, `reason`, `chatlog`, `description`) VALUES (?, ?, ?, ?, ?, ?)")) {
ps.setString(1, OffsetDateTime.now().toString());
ps.setTimestamp(1, Timestamp.from(Instant.now()));
ps.setInt(2, reporterid);
ps.setInt(3, victimid);
ps.setInt(4, reason);
ps.setString(5, chatlog);
ps.setString(6, description);
ps.addBatch();
ps.executeBatch();
ps.executeUpdate();
} catch (SQLException ex) {
ex.printStackTrace();
}

View File

@@ -38,6 +38,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import scripting.event.EventInstanceManager;
import server.ItemInformationProvider;
import service.NoteService;
import tools.DatabaseConnection;
import tools.PacketCreator;
import tools.Pair;
@@ -56,6 +57,12 @@ import java.sql.SQLException;
public final class RingActionHandler extends AbstractPacketHandler {
private static final Logger log = LoggerFactory.getLogger(RingActionHandler.class);
private final NoteService noteService;
public RingActionHandler(NoteService noteService) {
this.noteService = noteService;
}
private static int getEngagementBoxId(int useItemId) {
return switch (useItemId) {
case ItemId.ENGAGEMENT_BOX_MOONSTONE -> ItemId.EMPTY_ENGAGEMENT_BOX_MOONSTONE;
@@ -396,7 +403,8 @@ public final class RingActionHandler extends AbstractPacketHandler {
return;
}
String groom = c.getPlayer().getName(), bride = Character.getNameById(c.getPlayer().getPartnerId());
String groom = c.getPlayer().getName();
String bride = Character.getNameById(c.getPlayer().getPartnerId());
int guest = Character.getIdByName(name);
if (groom == null || bride == null || groom.equals("") || bride.equals("") || guest <= 0) {
c.getPlayer().dropMessage(5, "Unable to find " + name + "!");
@@ -417,14 +425,16 @@ public final class RingActionHandler extends AbstractPacketHandler {
if (resStatus > 0) {
long expiration = cserv.getWeddingTicketExpireTime(resStatus + 1);
String baseMessage = "You've been invited to %s and %s's Wedding!".formatted(groom, bride);
Character guestChr = c.getWorldServer().getPlayerStorage().getCharacterById(guest);
if (guestChr != null && InventoryManipulator.checkSpace(guestChr.getClient(), newItemId, 1, "") && InventoryManipulator.addById(guestChr.getClient(), newItemId, (short) 1, expiration)) {
guestChr.dropMessage(6, "[Wedding] You've been invited to " + groom + " and " + bride + "'s Wedding!");
guestChr.dropMessage(6, "[Wedding] %s".formatted(baseMessage));
} else {
String dueyMessage = baseMessage + " Receive your invitation from Duey!";
if (guestChr != null && guestChr.isLoggedinWorld()) {
guestChr.dropMessage(6, "[Wedding] You've been invited to " + groom + " and " + bride + "'s Wedding! Receive your invitation from Duey!");
guestChr.dropMessage(6, "[Wedding] %s".formatted(dueyMessage));
} else {
c.getPlayer().sendNote(name, "You've been invited to " + groom + " and " + bride + "'s Wedding! Receive your invitation from Duey!", (byte) 0);
noteService.sendNormal(dueyMessage, groom, name);
}
Item weddingTicket = new Item(newItemId, (short) 0, (short) 1);
@@ -442,7 +452,7 @@ public final class RingActionHandler extends AbstractPacketHandler {
c.getPlayer().dropMessage(5, "Invitation was not sent to '" + name + "'. Either the time for your marriage reservation already came or it was not found.");
}
} catch (SQLException ex) {
} catch (Exception ex) {
ex.printStackTrace();
return;
}

View File

@@ -38,6 +38,7 @@ import constants.id.MapId;
import constants.inventory.ItemConstants;
import net.AbstractPacketHandler;
import net.packet.InPacket;
import net.packet.out.SendNoteSuccessPacket;
import net.server.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,10 +47,10 @@ import server.Shop;
import server.ShopFactory;
import server.TimerManager;
import server.maps.*;
import service.NoteService;
import tools.PacketCreator;
import tools.Pair;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -60,6 +61,12 @@ import static java.util.concurrent.TimeUnit.SECONDS;
public final class UseCashItemHandler extends AbstractPacketHandler {
private static final Logger log = LoggerFactory.getLogger(UseCashItemHandler.class);
private final NoteService noteService;
public UseCashItemHandler(NoteService noteService) {
this.noteService = noteService;
}
@Override
public void handlePacket(InPacket p, Client c) {
final Character player = c.getPlayer();
@@ -358,12 +365,11 @@ public final class UseCashItemHandler extends AbstractPacketHandler {
} else if (itemType == 509) {
String sendTo = p.readString();
String msg = p.readString();
try {
player.sendNote(sendTo, msg, (byte) 0);
} catch (SQLException e) {
e.printStackTrace();
boolean sendSuccess = noteService.sendNormal(msg, player.getName(), sendTo);
if (sendSuccess) {
remove(c, position, itemId);
c.sendPacket(new SendNoteSuccessPacket());
}
remove(c, position, itemId);
} else if (itemType == 510) {
player.getMap().broadcastMessage(PacketCreator.musicChange("Jukebox/Congratulation"));
remove(c, position, itemId);

View File

@@ -69,7 +69,7 @@ public final class WeddingHandler extends AbstractPacketHandler {
if (!item.isUntradeable()) {
if (itemid == item.getItemId() && quantity <= item.getQuantity()) {
newItem = item.copy();
newItem.setQuantity(quantity);
marriage.addGiftItem(groomWishlist, newItem);
InventoryManipulator.removeFromSlot(c, type, slot, quantity, false, false);
@@ -161,4 +161,4 @@ public final class WeddingHandler extends AbstractPacketHandler {
}
}
}
}
}

View File

@@ -34,6 +34,7 @@ import net.server.coordinator.world.InviteCoordinator.InviteResult;
import net.server.coordinator.world.InviteCoordinator.InviteType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import service.NoteService;
import tools.DatabaseConnection;
import tools.PacketCreator;
@@ -497,7 +498,7 @@ public class Guild {
}
}
public void expelMember(GuildCharacter initiator, String name, int cid) {
public void expelMember(GuildCharacter initiator, String name, int cid, NoteService noteService) {
membersLock.lock();
try {
java.util.Iterator<GuildCharacter> itr = members.iterator();
@@ -512,16 +513,7 @@ public class Guild {
if (mgc.isOnline()) {
Server.getInstance().getWorld(mgc.getWorld()).setGuildAndRank(cid, 0, 5);
} else {
try (Connection con = DatabaseConnection.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT INTO notes (`to`, `from`, `message`, `timestamp`) VALUES (?, ?, ?, ?)")) {
ps.setString(1, mgc.getName());
ps.setString(2, initiator.getName());
ps.setString(3, "You have been expelled from the guild.");
ps.setLong(4, System.currentTimeMillis());
ps.executeUpdate();
} catch (SQLException e) {
log.error("expelMember - Guild", e);
}
noteService.sendNormal("You have been expelled from the guild.", initiator.getName(), mgc.getName());
Server.getInstance().getWorld(mgc.getWorld()).setOfflineGuildStatus((short) 0, (byte) 5, cid);
}
} catch (Exception re) {

View File

@@ -26,10 +26,15 @@ import client.processor.npc.FredrickProcessor;
* @author Ronan
*/
public class DueyFredrickTask implements Runnable {
private final FredrickProcessor fredrickProcessor;
public DueyFredrickTask(FredrickProcessor fredrickProcessor) {
this.fredrickProcessor = fredrickProcessor;
}
@Override
public void run() {
FredrickProcessor.runFredrickSchedule();
fredrickProcessor.runFredrickSchedule();
DueyProcessor.runDueyExpireSchedule();
}
}

View File

@@ -448,6 +448,20 @@ public class Trade {
}
public static void inviteTrade(Character c1, Character c2) {
if ((c1.isGM() && !c2.isGM()) && c1.gmLevel() < YamlConfig.config.server.MINIMUM_GM_LEVEL_TO_TRADE) {
c1.message("You cannot trade with non-GM characters.");
log.info(String.format("GM %s blocked from trading with %s due to GM level.", c1.getName(), c2.getName()));
cancelTrade(c1, TradeResult.NO_RESPONSE);
return;
}
if ((!c1.isGM() && c2.isGM()) && c2.gmLevel() < YamlConfig.config.server.MINIMUM_GM_LEVEL_TO_TRADE) {
c1.message("You cannot trade with this GM character.");
cancelTrade(c1, TradeResult.NO_RESPONSE);
return;
}
if (InviteCoordinator.hasInvite(InviteType.TRADE, c1.getId())) {
if (hasTradeInviteBack(c1, c2)) {
c1.message("You are already managing this player's trade invitation.");

View File

@@ -0,0 +1,110 @@
package service;
import client.Character;
import database.DaoException;
import database.note.NoteDao;
import model.Note;
import net.packet.out.ShowNotesPacket;
import net.server.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class NoteService {
private static final Logger log = LoggerFactory.getLogger(NoteService.class);
private final NoteDao noteDao;
public NoteService(NoteDao noteDao) {
this.noteDao = noteDao;
}
/**
* Send normal note from one character to another
*
* @return Send success
*/
public boolean sendNormal(String message, String senderName, String receiverName) {
Note normalNote = Note.createNormal(message, senderName, receiverName, Server.getInstance().getCurrentTime());
return send(normalNote);
}
/**
* Send note which will increase the receiver's fame by one.
*
* @return Send success
*/
public boolean sendWithFame(String message, String senderName, String receiverName) {
Note noteWithFame = Note.createGift(message, senderName, receiverName, Server.getInstance().getCurrentTime());
return send(noteWithFame);
}
private boolean send(Note note) {
// TODO: handle the following cases (originally listed at PacketCreator#noteError)
/*
* 0 = Player online, use whisper
* 1 = Check player's name
* 2 = Receiver inbox full
*/
try {
noteDao.save(note);
return true;
} catch (DaoException e) {
log.error("Failed to send note {}", note, e);
return false;
}
}
/**
* Show unread notes
*
* @param chr Note recipient
*/
public void show(Character chr) {
if (chr == null) {
throw new IllegalArgumentException("Unable to show notes - chr is null");
}
List<Note> notes = getNotes(chr.getName());
if (notes.isEmpty()) {
return;
}
chr.sendPacket(new ShowNotesPacket(notes));
}
private List<Note> getNotes(String to) {
final List<Note> notes;
try {
notes = noteDao.findAllByTo(to);
} catch (DaoException e) {
log.error("Failed to find notes sent to chr name {}", to, e);
return Collections.emptyList();
}
if (notes == null || notes.isEmpty()) {
return Collections.emptyList();
}
return notes;
}
/**
* Delete a read note
*
* @param noteId Id of note to discard
* @return Discarded note. Empty optional if failed to discard.
*/
public Optional<Note> delete(int noteId) {
try {
return noteDao.delete(noteId);
} catch (DaoException e) {
log.error("Failed to discard note with id {}", noteId, e);
return Optional.empty();
}
}
}

View File

@@ -3,9 +3,13 @@ package tools;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import config.YamlConfig;
import database.note.NoteRowMapper;
import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.Jdbi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.Duration;
@@ -21,15 +25,24 @@ import static java.util.concurrent.TimeUnit.SECONDS;
public class DatabaseConnection {
private static final Logger log = LoggerFactory.getLogger(DatabaseConnection.class);
private static HikariDataSource dataSource;
private static Jdbi jdbi;
public static Connection getConnection() throws SQLException {
if (dataSource == null) {
throw new IllegalStateException("Unable to get connection from uninitialized connection pool");
throw new IllegalStateException("Unable to get connection - connection pool is uninitialized");
}
return dataSource.getConnection();
}
public static Handle getHandle() {
if (jdbi == null) {
throw new IllegalStateException("Unable to get handle - connection pool is uninitialized");
}
return jdbi.open();
}
private static String getDbUrl() {
// Environment variables override what's defined in the config file
// This feature is used for the Docker support
@@ -73,6 +86,7 @@ public class DatabaseConnection {
Instant initStart = Instant.now();
try {
dataSource = new HikariDataSource(config);
initializeJdbi(dataSource);
long initDuration = Duration.between(initStart, Instant.now()).toMillis();
log.info("Connection pool initialized in {} ms", initDuration);
return true;
@@ -84,4 +98,9 @@ public class DatabaseConnection {
// Timed out - failed to initialize
return false;
}
private static void initializeJdbi(DataSource dataSource) {
jdbi = Jdbi.create(dataSource)
.registerRowMapper(new NoteRowMapper());
}
}

View File

@@ -71,7 +71,6 @@ import server.movement.LifeMovementFragment;
import java.awt.*;
import java.net.InetAddress;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.*;
@@ -5404,12 +5403,6 @@ public class PacketCreator {
return p;
}
public static Packet noteSendMsg() {
OutPacket p = OutPacket.create(SendOpcode.MEMO_RESULT);
p.writeByte(4);
return p;
}
/*
* 0 = Player online, use whisper
* 1 = Check player's name
@@ -5422,21 +5415,6 @@ public class PacketCreator {
return p;
}
public static Packet showNotes(ResultSet notes, int count) throws SQLException {
final OutPacket p = OutPacket.create(SendOpcode.MEMO_RESULT);
p.writeByte(3);
p.writeByte(count);
for (int i = 0; i < count; i++) {
p.writeInt(notes.getInt("id"));
p.writeString(notes.getString("from") + " ");//Stupid nexon forgot space lol
p.writeString(notes.getString("message"));
p.writeLong(getTime(notes.getLong("timestamp")));
p.writeByte(notes.getByte("fame"));//FAME :D
notes.next();
}
return p;
}
public static Packet useChalkboard(Character chr, boolean close) {
OutPacket p = OutPacket.create(SendOpcode.CHALKBOARD);
p.writeInt(chr.getId());

View File

@@ -0,0 +1,28 @@
package model;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class NoteTest {
@Test
void requireNonNullMessage() {
assertThrows(NullPointerException.class, () -> new Note(1, null, "from", "to", System.currentTimeMillis(), 0));
}
@Test
void requireNonNullFrom() {
assertThrows(NullPointerException.class, () -> new Note(2, "message", null, "to", System.currentTimeMillis(), 0));
}
@Test
void requireNonNullTo() {
assertThrows(NullPointerException.class, () -> new Note(3, "message", "from", null, System.currentTimeMillis(), 0));
}
@Test
void createNew() {
assertDoesNotThrow(() -> new Note(4, "message", "from", "to", System.currentTimeMillis(), 5));
}
}

View File

@@ -0,0 +1,142 @@
package service;
import database.note.NoteDao;
import model.Note;
import net.packet.out.ShowNotesPacket;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import testutil.Mocks;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static testutil.AnyValues.daoException;
import static testutil.AnyValues.string;
class NoteServiceTest {
@Mock
private NoteDao noteDao;
private NoteService noteService;
@BeforeEach
void reset() {
MockitoAnnotations.openMocks(this);
this.noteService = new NoteService(noteDao);
}
@Test
void sendNormalSuccess() {
String message = "message";
String from = "from";
String to = "to";
boolean success = noteService.sendNormal(message, from, to);
assertTrue(success);
var noteCaptor = ArgumentCaptor.forClass(Note.class);
verify(noteDao).save(noteCaptor.capture());
var note = noteCaptor.getValue();
assertEquals(message, note.message());
assertEquals(from, note.from());
assertEquals(to, note.to());
assertEquals(0, note.fame());
}
@Test
void sendWithFameSuccess() {
String message = "fameMessage";
String from = "fameFrom";
String to = "fameTo";
boolean success = noteService.sendWithFame(message, from, to);
assertTrue(success);
var noteCaptor = ArgumentCaptor.forClass(Note.class);
verify(noteDao).save(noteCaptor.capture());
var note = noteCaptor.getValue();
assertEquals(message, note.message());
assertEquals(from, note.from());
assertEquals(to, note.to());
assertEquals(1, note.fame());
}
@Test
void sendFailure() {
doThrow(daoException()).when(noteDao).save(any());
boolean success = noteService.sendNormal(string(), string(), string());
assertFalse(success);
verify(noteDao).save(any());
}
@Test
void showRejectsNull() {
assertThrows(IllegalArgumentException.class, () -> noteService.show(null));
}
@Test
void showOneNote() {
String chrName = "showMeNotes";
var chr = Mocks.chr(chrName);
when(noteDao.findAllByTo(chrName)).thenReturn(List.of(anyNote()));
noteService.show(chr);
verify(chr).sendPacket(any(ShowNotesPacket.class));
}
private Note anyNote() {
return new Note(1, "message", "from", "to", 100200300400L, 0);
}
@Test
void showZeroNotes_shouldNotSendPacket() {
var chr = Mocks.chr("mockChr");
when(noteDao.findAllByTo(any())).thenReturn(Collections.emptyList());
noteService.show(chr);
verify(chr, never()).sendPacket(any());
}
@Test
void showNotesFailure_shouldNotSendPacket() {
var chr = Mocks.chr("mockChr");
when(noteDao.findAllByTo(any())).thenThrow(daoException());
noteService.show(chr);
verify(chr, never()).sendPacket(any());
}
@Test
void deleteNoteSuccess() {
int noteId = 1056;
var note = anyNote();
when(noteDao.delete(noteId)).thenReturn(Optional.of(note));
Optional<Note> deletedNote = noteDao.delete(noteId);
assertTrue(deletedNote.isPresent());
assertEquals(note, deletedNote.get());
}
@Test
void deleteNoteFailure() {
when(noteDao.delete(anyInt())).thenThrow(daoException());
Optional<Note> deletedNote = noteService.delete(4382);
assertTrue(deletedNote.isEmpty());
}
}

View File

@@ -0,0 +1,14 @@
package testutil;
import database.DaoException;
public class AnyValues {
public static String string() {
return "string";
}
public static DaoException daoException() {
return new DaoException(string(), new RuntimeException());
}
}

View File

@@ -0,0 +1,19 @@
package testutil;
import client.Character;
import org.mockito.Mockito;
import static org.mockito.Mockito.when;
public class Mocks {
public static Character chr() {
return Mockito.mock(Character.class);
}
public static Character chr(String name) {
var chr = chr();
when(chr.getName()).thenReturn(name);
return chr;
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" name="Cosmic" shutdownHook="disable">
<Properties>
<Property name="standard-pattern">%d{HH:mm:ss.SSS} [%t] %-5level %logger{2} - %msg%n</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout>
<Pattern>${standard-pattern}</Pattern>
</PatternLayout>
</Console>
</Appenders>
<Loggers>
<Root level="off">
<AppenderRef ref="Console" level="info"/>
</Root>
</Loggers>
</Configuration>