Vanilla L2J Mobius CT_0 Interlude @ 9be7adbe5a (2026-08-25): java tree, build.xml, libs, and the datapack files SPP later modifies

This commit is contained in:
2026-09-03 19:04:54 +02:00
commit f520c66e85
1631 changed files with 298118 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
* -text
+21
View File
@@ -0,0 +1,21 @@
# --- deployment / binaries (not source) ---
server/
build/
*.zip
*.rar
*.jar.pre-*
ziAuQ8He
ziVsvreY
# obsolete copies (superseded by src_mobius)
patches/
src/
l2j-lisvus/
# downloaded crest pack (result lives in spp_crest_seed.sql)
crests/
# local client config
l2.ini
l2.ini.decrypted.txt
# tooling
/.tmp/
*.log
__pycache__/
@@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<project name="L2J_Mobius_CT_0_Interlude" default="cleanup" basedir=".">
<description>
This file is part of the L2J Mobius project.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see [http://www.gnu.org/licenses/].
</description>
<property name="build" location="../build" />
<property name="build.bin" location="${build}/bin" />
<property name="build.dist" location="${build}/dist" />
<property name="build.dist.libs" location="${build.dist}/libs" />
<property name="build.dist.databaseinstaller" location="${build.dist}/db_installer" />
<property name="datapack" location="dist" />
<property name="libs" location="${datapack}/libs" />
<property name="src" location="java" />
<path id="classpath">
<fileset dir="${libs}">
<include name="*.jar" />
<exclude name="**/*-sources.jar" />
</fileset>
</path>
<pathconvert property="manifest.libs" pathsep=" ">
<path refid="classpath" />
<mapper>
<chainedmapper>
<flattenmapper />
<globmapper from="*.jar" to="../libs/*.jar" />
</chainedmapper>
</mapper>
</pathconvert>
<target name="checkRequirements" description="Check Requirements.">
<fail message="Ant 1.8.2 is required. But your version is ${ant.version} and if you are using Eclipse probably is outdated.">
<condition>
<not>
<antversion atleast="1.8.2" />
</not>
</condition>
</fail>
<available classname="java.util.stream.Stream" property="JDK25.present" />
<fail unless="JDK25.present" message="Java 25 is required. But your version is Java ${ant.java.version} and probably JDK is not installed." />
</target>
<target name="init" depends="checkRequirements" description="Create the output directories.">
<delete dir="${build.bin}" quiet="true" />
<mkdir dir="${build.bin}" />
</target>
<target name="compile" depends="init" description="Compile the source.">
<javac srcdir="${src}" classpathref="classpath" destdir="${build.bin}" compiler="modern" debug="true" debuglevel="lines,vars,source" includeantruntime="false" source="25" target="25" encoding="UTF-8" />
</target>
<target name="jar" depends="compile" description="Create the jar files.">
<tstamp>
<format property="time.stamp" pattern="yyyy-MM-dd HH:mm:ss z" />
</tstamp>
<jar destfile="${build.dist.libs}/LoginServer.jar" level="9">
<fileset dir="${build.bin}">
<exclude name="**/gameserver/**" />
<exclude name="**/tools/DatabaseInstaller**" />
<exclude name="**/tools/Search**" />
</fileset>
<manifest>
<attribute name="Build-By" value="${user.name}" />
<attribute name="Build-Date" value="${time.stamp}" />
<attribute name="Implementation-URL" value="http://www.l2jmobius.org/" />
<attribute name="Class-Path" value="${manifest.libs}" />
<attribute name="Main-Class" value="org.l2jmobius.loginserver.LoginServer" />
</manifest>
</jar>
<jar destfile="${build.dist.libs}/GameServer.jar" level="9">
<fileset dir="${build.bin}">
<exclude name="**/loginserver/**" />
<exclude name="**/tools/AccountManager**" />
<exclude name="**/tools/DatabaseInstaller**" />
<exclude name="**/tools/GameServerRegister**" />
</fileset>
<manifest>
<attribute name="Build-By" value="${user.name}" />
<attribute name="Build-Date" value="${time.stamp}" />
<attribute name="Implementation-URL" value="http://www.l2jmobius.org/" />
<attribute name="Class-Path" value="${manifest.libs}" />
<attribute name="Main-Class" value="org.l2jmobius.gameserver.GameServer" />
</manifest>
</jar>
<jar destfile="${build.dist.databaseinstaller}/DatabaseInstaller.jar" level="9">
<fileset dir="${build.bin}">
<include name="**/commons/config/DatabaseConfig**" />
<include name="**/commons/config/InterfaceConfig**" />
<include name="**/commons/database/DatabaseFactory**" />
<include name="**/commons/ui/**" />
<include name="**/commons/util/ConfigReader**" />
<include name="**/tools/DatabaseInstaller**" />
</fileset>
<manifest>
<attribute name="Build-By" value="${user.name}" />
<attribute name="Build-Date" value="${time.stamp}" />
<attribute name="Class-Path" value="${manifest.libs}" />
<attribute name="Implementation-URL" value="http://www.l2jmobius.org/" />
<attribute name="Main-Class" value="org.l2jmobius.tools.DatabaseInstaller" />
</manifest>
</jar>
</target>
<target name="adding-core" depends="jar" description="Adding the compiled jars to the Zip file.">
<zip destfile="${build}/L2J_Mobius_CT_0_Interlude.zip" basedir="${build.dist}" level="9" />
</target>
<target name="adding-datapack" depends="adding-core" description="Updating the Zip file with datapack content.">
<zip destfile="${build}/L2J_Mobius_CT_0_Interlude.zip" basedir="${datapack}" excludes="**/*-sources.jar" update="true" level="9" />
</target>
<target name="adding-readme" depends="adding-datapack" description="Adding readme.txt to the Zip file.">
<zip destfile="${build}/L2J_Mobius_CT_0_Interlude.zip" basedir="." includes="readme.txt" update="true" level="9" />
</target>
<target name="cleanup" depends="adding-readme" description="Cleaning the build folder.">
<delete dir="${build.dist}" />
</target>
</project>
@@ -0,0 +1,38 @@
# ---------------------------------------------------------------------------
# Fake players
# ---------------------------------------------------------------------------
# Our fake player system uses the existing NPC system, allowing fake players
# to function seamlessly like NPCs with minimal impact on server performance.
# Enable fake players.
EnableFakePlayers = False
# Enable chatting with fake players.
FakePlayerChat = True
# Enable shots usage for fake players.
FakePlayerUseShots = True
# Reward PvP kills by killing fake players.
FakePlayerKillsRewardPvP = True
# Fake player kills apply karma rules.
FakePlayerUnflaggedKillsKarma = True
# Fake players can be attacked without PvP flagging.
FakePlayerAutoAttackable = False
# Aggressive AI fake players attack nearby monsters.
FakePlayerAggroMonsters = True
# Aggressive AI fake players attack nearby players.
FakePlayerAggroPlayers = False
# Aggressive AI fake players attack nearby fake players.
FakePlayerAggroFPC = False
# Fake players can drop items when killing monsters.
FakePlayerCanDropItems = True
# Fake players can pickup dropped items.
FakePlayerCanPickup = True
@@ -0,0 +1,666 @@
# ---------------------------------------------------------------------------
# General Server Settings
# ---------------------------------------------------------------------------
# The defaults are set to be retail-like. If you modify any of these settings your server will deviate from being retail-like.
# Warning:
# Please take extreme caution when changing anything. Also please understand what you are changing before you do so on a live server.
# ---------------------------------------------------------------------------
# Administrator
# ---------------------------------------------------------------------------
# If this option is set to True every newly created character will have access level 127. This means that every character created will have Administrator Privileges.
# Default: False
EverybodyHasAdminRights = False
# If True, only accounts with GM access can enter the server.
# Default: False
ServerGMOnly = False
# Enable GMs to have the glowing aura of a Hero character on login.
# Notes:
# GMs can do "///hero" on themselves and get this aura voluntarily.
# It's advised to keep this off due to graphic lag.
# Default: False
GMHeroAura = False
# Whether GM logins in builder hide mode by default.
# Default: True
GMStartupBuilderHide = True
# Auto set invulnerable status to a GM on login.
# Default: False
GMStartupInvulnerable = True
# Auto set invisible status to a GM on login.
# Default: False
GMStartupInvisible = True
# Auto block private messages to a GM on login.
# Default: False
GMStartupSilence = False
# Auto list GMs in GM list (/gmlist) on login.
# Default: False
GMStartupAutoList = False
# Auto set diet mode on to a GM on login (affects your weight penalty).
# Default: False
GMStartupDietMode = False
# Item restrictions apply to GMs as well? (True = restricted usage)
# Default: True
GMItemRestriction = True
# Skill restrictions apply to GMs as well? (True = restricted usage)
# Default: True
GMSkillRestriction = True
# Allow GMs to drop/trade non-tradable and quest(drop only) items
# Default: False
GMTradeRestrictedItems = False
# Allow GMs to restart/exit while is fighting stance
# Default: True
GMRestartFighting = True
# Show the GM's name behind an announcement made by him
# example: "Announce: hi (HanWik)"
GMShowAnnouncerName = False
# Show the GM's name before an announcement made by him
# example: "Nyaran: hi"
GMShowCritAnnouncerName = False
# Give special skills for every GM
# 7029,7041-7064,7088-7096,23238-23249 (Master's Blessing)
# Default: False
GMGiveSpecialSkills = False
# Give special aura skills for every GM
# 7029,23238-23249,23253-23296 (Master's Blessing)
# Default: False
GMGiveSpecialAuraSkills = False
# Debug html paths for GM characters.
# Default: True
GMDebugHtmlPaths = True
# In case you are not satisfied with the retail-like implementation of //gmspeed",
# with this config you can rollback it to the old custom L2J version of the GM Speed.
# Default: False
UseSuperHasteAsGMSpeed = False
# ---------------------------------------------------------------------------
# Server Security
# ---------------------------------------------------------------------------
# Logging settings. Enabling these settings will significantly increase the amount of log data written to disk.
# This can lead to increased disk usage and depending on server load and player activity, may impact performance.
# Use these options carefully based on your logging needs and server capacity.
# Enable logging of player chat messages. Set to True if you need a record of all chat interactions.
# Default: False
LogChat = False
# Enable logging of item transactions (e.g., pickups, trades, sales).
# This setting can be useful for tracking item movement but may lead to extensive logging on busy servers.
# Default: False
LogItems = False
# If LogItems is enabled, set this to True to only log important items, specifically Adena (in-game currency) and equippable items.
# This helps to reduce log volume by excluding common, low-value items.
# Default: False
LogItemsSmallLog = False
# If LogItems is enabled, set this to True to log only specific item IDs rather than all items.
# This is helpful if you only need logs for certain items, such as rare or high-value items.
# Default: False
LogItemsIdsOnly = False
# Specifies the item IDs to log when LogItemsIdsOnly is enabled.
# Enter item IDs separated by commas to track specific items, for example, rare items or in-game currency.
# Default: 4356 (Gold Einhasad)
LogItemsIdsList = 4356
# Enable logging for all actions involving item enchantments, such as success or failure of upgrades.
# This can help with tracking suspicious behavior but may create large log files if enchantment is frequently used.
# Default: False
LogItemEnchants = False
# Enable logging for all actions related to skill enchantments, including upgrades and modifications.
# Useful for monitoring skill progression but can lead to extensive logging on active servers.
# Default: False
LogSkillEnchants = False
# Enable audit logging for actions performed by Game Masters (GMs).
# This helps in tracking GM activities to ensure administrative actions are recorded for accountability.
# Default: False
GMAudit = False
# Check players for non-allowed skills
# Default: False
SkillCheckEnable = True
# If true, remove invalid skills from player and database.
# Report only, if false.
# Default: False
SkillCheckRemove = True
# Check also GM characters (only if SkillCheckEnable = True)
# Default: True
SkillCheckGM = False
# ---------------------------------------------------------------------------
# Optimization
# ---------------------------------------------------------------------------
# Items on ground management.
# Allow players to drop items on the ground.
# Default: True
AllowDiscardItem = True
# Delete dropped reward items from world after a specified amount of seconds. Disabled = 0.
# Default: 600
AutoDestroyDroppedItemAfter = 600
# Time in seconds after which dropped herb will be auto-destroyed
# Default: 60
AutoDestroyHerbTime = 60
# List of item id that will not be destroyed (separated by "," like 57,5575,6673).
# Notes:
# Make sure the lists do NOT CONTAIN trailing spaces or spaces between the numbers!
# Items on this list will be protected regardless of the following options.
# Default: 0
ListOfProtectedItems = 0
# This is the interval (in minutes), that the gameserver will update a players information such as location.
# The higher you set this number, there will be less character information saving so you will have less accessing of the database and your hard drive(s).
# The lower you set this number, there will be more frequent character information saving so you will have more access to the database and your hard drive(s).
# A value of 0 disables periodic saving.
# Independent of this setting the character is always saved after leaving the world.
# Default: 15
CharacterDataStoreInterval = 15
# This enables the server to only update items when saving the character.
# Enabling this greatly reduces DB usage and improves performance.
# WARNING: This option causes item loss during crashes.
# Default: False
LazyItemsUpdate = False
# When enabled, this forces (even if using lazy item updates) the items owned by the character to be updated into DB when saving its character.
# Default: True
UpdateItemsOnCharStore = True
# Also delete from world misc. items dropped by players (all except equip-able items).
# Notes:
# Works only if AutoDestroyDroppedItemAfter is greater than 0.
# Default: False
DestroyPlayerDroppedItem = False
# Destroy dropped equippable items (armor, weapon, jewelry).
# Notes:
# Works only if DestroyPlayerDroppedItem = True
# Default: False
DestroyEquipableItem = False
# Make all items destroyable.
# If enabled players can destroy all items!!!
DestroyAllItems = False
# Save dropped items into the database for restoring after restart.
# Default: False
SaveDroppedItem = False
# Enable/Disable the emptying of the stored dropped items table after items are loaded into memory (safety setting).
# If the server crashed before saving items, on next start old items will be restored and players may already have picked up some of them so this will prevent duplicates.
# Default: False
EmptyDroppedItemTableAfterLoad = False
# Time interval in minutes to save in DB items on ground. Disabled = 0.
# Notes:
# If SaveDroppedItemInterval is disabled, items will be saved into the database only at server shutdown.
# Default: 60
SaveDroppedItemInterval = 60
# Delete all saved items from the database on next restart?
# Notes:
# Works only if SaveDroppedItem = False.
# Default: False
ClearDroppedItemTable = False
# Delete invalid quest from players.
# Default: False
AutoDeleteInvalidQuestData = False
# Allow creating multiple non-stackable items at one time?
# Default: True
MultipleItemDrop = True
# Enable/Disable html caching.
# True = Load all html's into cache on server startup.
# False = Load html's into cache only on first time html is requested.
# Recommended for live servers: True
# Recommended for development: False
HtmCache = False
# Check if html files contain non ASCII characters.
# Default = True
CheckHtmlEncoding = True
# Minimum and maximum variables in seconds for NPC animation delay.
# You must keep MinNpcAnimation lower or equal to MaxNpcAnimation.
# Set values to 0 for disabling random animations.
# Default: 5
MinNpcAnimation = 5
# Default: 60
MaxNpcAnimation = 60
# Default: 5
MinMonsterAnimation = 5
# Default: 60
MaxMonsterAnimation = 60
# Grid options: Grids can turn themselves on and off. This also affects the loading and processing of all AI tasks and (in the future) geodata within this grid.
# Turn on for a grid with a person in it is immediate, but it then turns on the 8 neighboring grids based on the specified number of seconds.
# Turn off for a grid and neighbors occurs after the specified number of seconds have passed during which a grid has had no players in or in any of its neighbors.
# The always on option allows to ignore all this and let all grids be active at all times (not suggested).
# Default: False
GridsAlwaysOn = False
# Default: 1
GridNeighborTurnOnTime = 1
# Default: 90
GridNeighborTurnOffTime = 90
# Correct buylist and multisell prices when lower than sell price.
# Default: True
CorrectPrices = True
# Item limit on multisell transaction.
# Max client allowed 999999
MultisellAmountLimit = 10000
# ---------------------------------------------------------------------------
# Falling Damage
# ---------------------------------------------------------------------------
# Allow characters to receive damage from falling.
# Default: True
EnableFallingDamage = True
# ---------------------------------------------------------------------------
# Skills & Effects
# ---------------------------------------------------------------------------
# Affect debuff time by the character resistances.
# If the option is false, the debuff time will be the one set in the skill xml.
DebuffDurationUsesResists = False
# ---------------------------------------------------------------------------
# Features
# ---------------------------------------------------------------------------
# Peace Zone Modes:
# 0 = Peace All the Time
# 1 = PVP During Siege for siege participants
# 2 = PVP All the Time
# Default: 0
PeaceZoneMode = 0
# Global Chat.
# Available Options: ON, OFF, GM, GLOBAL
# Default: ON
GlobalChat = ON
# Trade Chat.
# Available Options: ON, OFF, GM, GLOBAL
# Default: ON
TradeChat = ON
# Minimum level for chat, 0 = disable.
MinimumChatLevel = 0
# If you are experiencing problems with Warehouse transactions, feel free to disable them here.
# Default: True
AllowWarehouse = True
# Default: True
AllowRefund = True
# If True player can try on weapon and armor in shop.
# Default: True
AllowWear = True
# Default: 5
WearDelay = 5
#Adena cost to try on an item.
# Default: 10
WearPrice = 10
# Disable additional adena rewards for starter villages repeatable quests based on turning in items
# True = additional reward for 10+ items not given on quest turn in, False = get additional reward for 10+ items on quest turn in
# ATTENTION: enabling this option greatly decrease adena income capabilities on low levels.
# Default: False
AltVillagesRepQuestReward = False
# ---------------------------------------------------------------------------
# Instances
# ---------------------------------------------------------------------------
# Restores the player to their previous instance (ie. an instanced area/dungeon) on EnterWorld.
# Default: False
RestorePlayerInstance = True
# Set whether summon skills can be used to summon players inside an instance.
# When enabled individual instances can have summoning disabled in instance xml's.
# DEFAULT NEEDS TO BE VERIFIED, MUST BE CHANGED HERE AND IN CONFIG.JAVA IF NOT CORRECT
# Default: False
AllowSummonInInstance = False
# When a player dies, is removed from instance after a fixed period of time.
# Time in seconds.
# Default: 60
EjectDeadPlayerTime = 60
# When is instance finished, is set time to destruction currency instance.
# Time in seconds.
# Default: 300
DefaultFinishTime = 300
# ---------------------------------------------------------------------------
# Misc Settings
# ---------------------------------------------------------------------------
# Default: True
AllowRace = True
# Default: True
AllowWater = True
# Default: True
AllowFishing = True
# Default: True
AllowBoat = True
# Boat broadcast radius.
# If players getting annoyed by boat shouts then radius can be decreased.
# Default: 20000
BoatBroadcastRadius = 20000
# Default: True
AllowCursedWeapons = True
# If false, always block party on event.
# If true, allows party if both are in the same event.
AllowPartyInSameEvent = True
# Show "data/html/servnews.htm" when a character enters world.
# Default: False
ShowServerNews = False
# Enable the Community Board.
# Default: True
EnableCommunityBoard = True
# Default Community Board page.
# Default: _bbshome
BBSDefault = _bbshome
# Enable chat filter
# Default = False
UseChatFilter = False
# Replace filter words with following chars
ChatFilterChars = ^_^
# Banchat for channels, split ";"
# GENERAL (white)
# SHOUT (!)
# WHISPER (")
# PARTY (#)
# CLAN (@)
# GM (//gmchat)
# PETITION_PLAYER (*)
# PETITION_GM (*)
# TRADE (+)
# ALLIANCE ($)
# ANNOUNCEMENT
# BOAT
# FRIEND
# MSNCHAT
# PARTYMATCH_ROOM
# PARTYROOM_COMMANDER (Yellow)
# PARTYROOM_ALL (Red)
# HERO_VOICE (%)
# CRITICAL_ANNOUNCE
# SCREEN_ANNOUNCE
# BATTLEFIELD
# MPCC_ROOM
# NPC_GENERAL
# NPC_SHOUT
# Default: GENERAL;SHOUT;TRADE;HERO_VOICE;WHISPER
BanChatChannels = GENERAL;SHOUT;TRADE;HERO_VOICE;WHISPER
# ---------------------------------------------------------------------------
# Manor
# ---------------------------------------------------------------------------
# Default: True
AllowManor = True
# Manor refresh time in military hours.
# Default: 20 (8pm)
AltManorRefreshTime = 20
# Manor refresh time (minutes).
# Default: 00 (start of the hour)
AltManorRefreshMin = 00
# Manor period approve time in military hours.
# Default: 6 (6am)
AltManorApproveTime = 6
# Manor period approve time (minutes).
# Default: 0
AltManorApproveMin = 0
# Manor maintenance time (minutes).
# Default: 6
AltManorMaintenanceMin = 6
# Manor Save Type.
# True = Save data into the database after every action
# Default: False
AltManorSaveAllActions = False
# Manor Save Period (used only if AltManorSaveAllActions = False)
# Default: 2 (hour)
AltManorSavePeriodRate = 2
# ---------------------------------------------------------------------------
# Lottery
# ---------------------------------------------------------------------------
# Default: True
AllowLottery = True
# Initial Lottery prize.
# Default: 50000
AltLotteryPrize = 50000
# Lottery Ticket Price
# Default: 2000
AltLotteryTicketPrice = 2000
# What part of jackpot amount should receive characters who pick 5 wining numbers
# Default: 0.6
AltLottery5NumberRate = 0.6
# What part of jackpot amount should receive characters who pick 4 wining numbers
# Default: 0.2
AltLottery4NumberRate = 0.2
# What part of jackpot amount should receive characters who pick 3 wining numbers
# Default: 0.2
AltLottery3NumberRate = 0.2
# How much Adena receive characters who pick two or less of the winning number
# Default: 200
AltLottery2and1NumberPrize = 200
# ---------------------------------------------------------------------------
# Fishing Tournament
# ---------------------------------------------------------------------------
# Enable or disable the Fishing Tournament system
AltFishChampionshipEnabled = True
# Item Id used as reward
AltFishChampionshipRewardItemId = 57
# Item count used as reward (for the 5 first winners)
AltFishChampionshipReward1 = 800000
AltFishChampionshipReward2 = 500000
AltFishChampionshipReward3 = 300000
AltFishChampionshipReward4 = 200000
AltFishChampionshipReward5 = 100000
# ---------------------------------------------------------------------------
# Item Auction
# ---------------------------------------------------------------------------
#
AltItemAuctionEnabled = True
# Number of days before auction cleared from database with all bids.
# Default: 14
AltItemAuctionExpiredAfter = 14
# Auction extends to specified amount of seconds if one or more new bids added.
# By default auction extends only two times, by 5 and 3 minutes, this custom value used after it.
# Values higher than 60s is not recommended.
# Default: 0
AltItemAuctionTimeExtendsOnBid = 0
# ---------------------------------------------------------------------------
# Dimension Rift
# ---------------------------------------------------------------------------
# Minimum party size to enter rift. Min = 2, Max = 9.
# If while inside the rift, the party becomes smaller, all members will be teleported back.
# Default: 2
RiftMinPartySize = 2
# Number of maximum jumps between rooms allowed, after this time party will be teleported back
# Default: 4
MaxRiftJumps = 4
# Time in ms the party has to wait until the mobs spawn when entering a room. C4 retail: 10s
# Default: 10000
RiftSpawnDelay = 10000
# Time between automatic jumps in seconds
# Default: 480
AutoJumpsDelayMin = 480
# Default: 600
AutoJumpsDelayMax = 600
# Time Multiplier for stay in the boss room
# Default: 1.5
BossRoomTimeMultiply = 1.5
# Cost in dimension fragments to enter the rift, each party member must own this amount
# Default: 18
RecruitCost = 18
# Default: 21
SoldierCost = 21
# Default: 24
OfficerCost = 24
# Default: 27
CaptainCost = 27
# Default: 30
CommanderCost = 30
# Default: 33
HeroCost = 33
# ---------------------------------------------------------------------------
# Punishment
# ---------------------------------------------------------------------------
# Player punishment for illegal actions:
# BROADCAST - broadcast warning to GMs only
# KICK - kick player (default)
# KICKBAN - kick and ban player
# JAIL - jail player
DefaultPunish = KICK
# This setting typically specifies the duration of the above punishment.
# Default: 0 (automatically sets to 100 years)
DefaultPunishParam = 0
# Apply default punish if player buy items for zero Adena.
# Default: True
OnlyGMItemsFree = True
# Jail is a PvP zone.
# Default: False
JailIsPvp = False
# Disable all chat in jail (except normal one)
# Default: True
JailDisableChat = True
# Disable all transaction in jail
# Trade/Store/Drop
# Default: False
JailDisableTransaction = False
# Enchant Skill Details Settings
# Default: 1,5
NormalEnchantCostMultipiler = 1
SafeEnchantCostMultipiler = 5
# ---------------------------------------------------------------------------
# Custom Components
# ---------------------------------------------------------------------------
# Default: False
CustomNpcData = True
# Default: False
CustomTeleportTable = True
# Default: False
CustomSkillsLoad = True
# Default: False
CustomItemsLoad = True
# Default: False
CustomMultisellLoad = True
# Default: False
CustomBuyListLoad = True
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Use lowercase for searchText and answers. -->
<!-- You can use specific fpcName or ALL to use with all fpcs. -->
<list xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="xsd/FakePlayerChatData.xsd">
<fakePlayerChat fpcName="ALL" searchMethod="EQUALS" searchText="hi" answers="hello;hi;hi there;hello there" />
<fakePlayerChat fpcName="ALL" searchMethod="EQUALS" searchText="hey" answers="hey hey;hey;hey there" />
<fakePlayerChat fpcName="ALL" searchMethod="EQUALS" searchText="hello" answers="hello;hi;hi there;hello there" />
<fakePlayerChat fpcName="Evi" searchMethod="EQUALS" searchText="here?" answers="yes;busy;i look for something" />
<fakePlayerChat fpcName="Evi" searchMethod="EQUALS" searchText="whats up?" answers="good;busy;i look for something" />
<fakePlayerChat fpcName="Evi" searchMethod="EQUALS" searchText="what?" answers="something :P;something for me" />
<fakePlayerChat fpcName="Evi" searchMethod="EQUALS" searchText="why?" answers="because;i don't know;what?" />
<fakePlayerChat fpcName="Evi" searchMethod="EQUALS" searchText="really" answers="really;yes;of course" />
<fakePlayerChat fpcName="Evi" searchMethod="EQUALS" searchText="thanks" answers=":);:D;:*" />
<fakePlayerChat fpcName="Evi" searchMethod="EQUALS" searchText="thank you" answers=":);:D;:*" />
<fakePlayerChat fpcName="Evi" searchMethod="STARTS_WITH" searchText="how are you" answers="fine;good;busy" />
<fakePlayerChat fpcName="Evi" searchMethod="STARTS_WITH" searchText="do you know" answers="nope;no sorry;nope, i don't" />
<fakePlayerChat fpcName="Evi" searchMethod="STARTS_WITH" searchText="where can i" answers="i don't know;no clue;ask someone else :P" />
<fakePlayerChat fpcName="Evi" searchMethod="STARTS_WITH" searchText="can i ask you" answers="yes;what?;tell me" />
<fakePlayerChat fpcName="Evi" searchMethod="CONTAINS" searchText="server;ha;problem" answers="it's good;i don't know;i don't think so..." />
<fakePlayerChat fpcName="Evi" searchMethod="CONTAINS" searchText="server;ha;bug" answers="it's good;i don't know;i don't think so..." />
<fakePlayerChat fpcName="Evi" searchMethod="CONTAINS" searchText="is th;server;good" answers="it's good :D;i like it :P;yes it is :)" />
<fakePlayerChat fpcName="Evi" searchMethod="CONTAINS" searchText="where;you;go;?" answers="i look for something;checking stuff;looking for curius people :P" />
<fakePlayerChat fpcName="Evi" searchMethod="CONTAINS" searchText="are;you;kidding" answers="^^;:D;:P" />
</list>
@@ -0,0 +1,130 @@
/*
* This file is part of the L2J Mobius project.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package handlers.chat.channels;
import java.util.StringTokenizer;
import org.l2jmobius.gameserver.config.GeneralConfig;
import org.l2jmobius.gameserver.config.custom.FactionSystemConfig;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.holders.player.BlockList;
import org.l2jmobius.gameserver.handler.IChatHandler;
import org.l2jmobius.gameserver.handler.IVoicedCommandHandler;
import org.l2jmobius.gameserver.handler.VoicedCommandHandler;
import org.l2jmobius.gameserver.network.SystemMessageId;
import org.l2jmobius.gameserver.network.enums.ChatType;
import org.l2jmobius.gameserver.network.serverpackets.CreatureSay;
/**
* General Chat Handler.
* @author durgus
*/
public class ChatGeneral implements IChatHandler
{
private static final ChatType[] CHAT_TYPES =
{
ChatType.GENERAL,
};
@Override
public void onChat(ChatType type, Player activeChar, String paramsValue, String text)
{
boolean vcdUsed = false;
if (text.startsWith("."))
{
final StringTokenizer st = new StringTokenizer(text);
final IVoicedCommandHandler vch;
String command = "";
String params = paramsValue;
if (st.countTokens() > 1)
{
command = st.nextToken().substring(1);
params = text.substring(command.length() + 2);
}
else
{
command = text.substring(1);
}
vch = VoicedCommandHandler.getInstance().getHandler(command);
if (vch != null)
{
vch.onCommand(command, activeChar, params);
vcdUsed = true;
}
else
{
vcdUsed = false;
}
}
if (!vcdUsed)
{
if (activeChar.isChatBanned() && GeneralConfig.BAN_CHAT_CHANNELS.contains(type))
{
activeChar.sendPacket(SystemMessageId.CHATTING_IS_CURRENTLY_PROHIBITED_IF_YOU_TRY_TO_CHAT_BEFORE_THE_PROHIBITION_IS_REMOVED_THE_PROHIBITION_TIME_WILL_BECOME_EVEN_LONGER);
return;
}
if ((activeChar.getLevel() < GeneralConfig.MINIMUM_CHAT_LEVEL) && !activeChar.isGM())
{
activeChar.sendMessage("Players can use general chat after Lv. " + GeneralConfig.MINIMUM_CHAT_LEVEL + ".");
return;
}
final CreatureSay cs = new CreatureSay(activeChar, type, activeChar.getAppearance().getVisibleName(), text);
final CreatureSay csRandom = new CreatureSay(activeChar, type, activeChar.getAppearance().getVisibleName(), ChatRandomizer.randomize(text));
World.forEachVisibleObjectInRange(activeChar, Player.class, 1250, player ->
{
if ((player != null) && !BlockList.isBlocked(player, activeChar))
{
if (FactionSystemConfig.FACTION_SYSTEM_ENABLED)
{
if (FactionSystemConfig.FACTION_SPECIFIC_CHAT)
{
if ((activeChar.isGood() && player.isEvil()) || (activeChar.isEvil() && player.isGood()))
{
player.sendPacket(csRandom);
}
else
{
player.sendPacket(cs);
}
}
else
{
player.sendPacket(cs);
}
}
else
{
player.sendPacket(cs);
}
}
});
activeChar.sendPacket(cs);
}
}
@Override
public ChatType[] getChatTypeList()
{
return CHAT_TYPES;
}
}
@@ -0,0 +1,141 @@
/*
* This file is part of the L2J Mobius project.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package handlers.chat.channels;
import org.l2jmobius.gameserver.config.GeneralConfig;
import org.l2jmobius.gameserver.config.PlayerConfig;
import org.l2jmobius.gameserver.config.custom.FactionSystemConfig;
import org.l2jmobius.gameserver.config.custom.FakePlayersConfig;
import org.l2jmobius.gameserver.data.xml.FakePlayerData;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.holders.player.BlockList;
import org.l2jmobius.gameserver.handler.IChatHandler;
import org.l2jmobius.gameserver.managers.FakePlayerChatManager;
import org.l2jmobius.gameserver.network.SystemMessageId;
import org.l2jmobius.gameserver.network.enums.ChatType;
import org.l2jmobius.gameserver.network.serverpackets.CreatureSay;
/**
* Tell Chat Handler.
* @author durgus
*/
public class ChatWhisper implements IChatHandler
{
private static final ChatType[] CHAT_TYPES =
{
ChatType.WHISPER
};
@Override
public void onChat(ChatType type, Player activeChar, String target, String text)
{
if (activeChar.isChatBanned() && GeneralConfig.BAN_CHAT_CHANNELS.contains(type))
{
activeChar.sendPacket(SystemMessageId.CHATTING_IS_CURRENTLY_PROHIBITED_IF_YOU_TRY_TO_CHAT_BEFORE_THE_PROHIBITION_IS_REMOVED_THE_PROHIBITION_TIME_WILL_BECOME_EVEN_LONGER);
return;
}
if (GeneralConfig.JAIL_DISABLE_CHAT && activeChar.isJailed() && !activeChar.isGM())
{
activeChar.sendPacket(SystemMessageId.CHATTING_IS_CURRENTLY_PROHIBITED);
return;
}
// Return if no target is set.
if (target == null)
{
return;
}
if (FakePlayersConfig.FAKE_PLAYERS_ENABLED && (FakePlayerData.getInstance().getProperName(target) != null))
{
if (FakePlayerData.getInstance().isTalkable(target))
{
if (FakePlayersConfig.FAKE_PLAYER_CHAT)
{
final String name = FakePlayerData.getInstance().getProperName(target);
activeChar.sendPacket(new CreatureSay(activeChar, type, "->" + name, text));
FakePlayerChatManager.getInstance().manageChat(activeChar, name, text);
}
else
{
activeChar.sendPacket(SystemMessageId.THAT_PERSON_IS_IN_MESSAGE_REFUSAL_MODE);
}
}
else
{
activeChar.sendPacket(SystemMessageId.THAT_PLAYER_IS_NOT_ONLINE);
}
return;
}
final Player receiver = World.getPlayer(target);
if ((receiver != null) && !receiver.isSilenceMode(activeChar.getObjectId()))
{
if (GeneralConfig.JAIL_DISABLE_CHAT && receiver.isJailed() && !activeChar.isGM())
{
activeChar.sendMessage("Player is in jail.");
return;
}
if (receiver.isChatBanned())
{
activeChar.sendPacket(SystemMessageId.THAT_PERSON_IS_IN_MESSAGE_REFUSAL_MODE);
return;
}
if ((receiver.getClient() == null) || receiver.getClient().isDetached())
{
activeChar.sendMessage("Player is in offline mode.");
return;
}
if (FactionSystemConfig.FACTION_SYSTEM_ENABLED && FactionSystemConfig.FACTION_SPECIFIC_CHAT && ((activeChar.isGood() && receiver.isEvil()) || (activeChar.isEvil() && receiver.isGood())))
{
activeChar.sendMessage("Player belongs to the opposing faction.");
return;
}
if (!BlockList.isBlocked(receiver, activeChar))
{
// Allow receiver to send PMs to this char, which is in silence mode.
if (PlayerConfig.SILENCE_MODE_EXCLUDE && activeChar.isSilenceMode())
{
activeChar.addSilenceModeExcluded(receiver.getObjectId());
}
receiver.sendPacket(new CreatureSay(activeChar, type, activeChar.getName(), text));
activeChar.sendPacket(new CreatureSay(activeChar, type, "->" + receiver.getName(), text));
}
else
{
activeChar.sendPacket(SystemMessageId.THAT_PERSON_IS_IN_MESSAGE_REFUSAL_MODE);
}
}
else
{
activeChar.sendPacket(SystemMessageId.THAT_PLAYER_IS_NOT_ONLINE);
}
}
@Override
public ChatType[] getChatTypeList()
{
return CHAT_TYPES;
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<list enabled="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../xsd/spawns.xsd">
<spawn name="FakePlayers">
<npc id="80000" x="83485" y="147998" z="-3407" heading="23509" respawnDelay="60" /> <!-- Evi -->
</spawn>
</list>
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.config;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the database related configurations.
* @author Mobius
*/
public class DatabaseConfig
{
// File
private static final String DATABASE_CONFIG_FILE = "./config/Database.ini";
// Constants
public static String DATABASE_DRIVER;
public static String DATABASE_URL;
public static String DATABASE_LOGIN;
public static String DATABASE_PASSWORD;
public static int DATABASE_MAX_CONNECTIONS;
public static boolean DATABASE_TEST_CONNECTIONS;
public static boolean BACKUP_DATABASE;
public static String MYSQL_BIN_PATH;
public static String BACKUP_PATH;
public static int BACKUP_DAYS;
public static void load()
{
final ConfigReader config = new ConfigReader(DATABASE_CONFIG_FILE);
DATABASE_DRIVER = config.getString("Driver", "com.mysql.cj.jdbc.Driver");
DATABASE_URL = config.getString("URL", "jdbc:mysql://localhost/l2jmobius");
DATABASE_LOGIN = config.getString("Login", "root");
DATABASE_PASSWORD = config.getString("Password", "");
DATABASE_MAX_CONNECTIONS = config.getInt("MaximumDatabaseConnections", 10);
DATABASE_TEST_CONNECTIONS = config.getBoolean("TestDatabaseConnections", false);
BACKUP_DATABASE = config.getBoolean("BackupDatabase", false);
MYSQL_BIN_PATH = config.getString("MySqlBinLocation", "C:/xampp/mysql/bin/");
BACKUP_PATH = config.getString("BackupPath", "../backup/");
BACKUP_DAYS = config.getInt("BackupDays", 30);
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.config;
import java.awt.GraphicsEnvironment;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the interface related configurations.
* @author Mobius
*/
public class InterfaceConfig
{
// File
private static final String INTERFACE_CONFIG_FILE = "./config/Interface.ini";
// Constants
public static boolean ENABLE_GUI;
public static boolean DARK_THEME;
public static void load()
{
final ConfigReader config = new ConfigReader(INTERFACE_CONFIG_FILE);
ENABLE_GUI = config.getBoolean("EnableGUI", true) && !GraphicsEnvironment.isHeadless();
if (ENABLE_GUI)
{
DARK_THEME = config.getBoolean("DarkTheme", true);
}
}
}
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.config;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the threadpool related configurations.
* @author Mobius
*/
public class ThreadConfig
{
// File
private static final String THREADS_CONFIG_FILE = "./config/Threads.ini";
// Constants
public static int SCHEDULED_THREAD_POOL_SIZE;
public static int HIGH_PRIORITY_SCHEDULED_THREAD_POOL_SIZE;
public static int INSTANT_THREAD_POOL_SIZE;
public static boolean THREADS_FOR_LOADING;
public static void load()
{
final ConfigReader config = new ConfigReader(THREADS_CONFIG_FILE);
SCHEDULED_THREAD_POOL_SIZE = config.getInt("ScheduledThreadPoolSize", -1);
if (SCHEDULED_THREAD_POOL_SIZE == -1)
{
SCHEDULED_THREAD_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 4;
}
INSTANT_THREAD_POOL_SIZE = config.getInt("InstantThreadPoolSize", -1);
if (INSTANT_THREAD_POOL_SIZE == -1)
{
INSTANT_THREAD_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2;
}
if ((SCHEDULED_THREAD_POOL_SIZE > 2) && (INSTANT_THREAD_POOL_SIZE > 2))
{
HIGH_PRIORITY_SCHEDULED_THREAD_POOL_SIZE = Math.max(2, SCHEDULED_THREAD_POOL_SIZE / 4);
}
else
{
HIGH_PRIORITY_SCHEDULED_THREAD_POOL_SIZE = 0;
}
if (config.containsKey("ThreadsForLoading"))
{
THREADS_FOR_LOADING = config.getBoolean("ThreadsForLoading", false);
}
else
{
THREADS_FOR_LOADING = false;
}
}
}
@@ -0,0 +1,242 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.crypt;
import java.nio.charset.StandardCharsets;
/**
* Blowfish cipher implementation with ECB processing for L2J packet encryption.<br>
* Provides checksum validation and XOR encryption for secure server communication.
* <ul>
* <li>Packet checksum verification and generation for integrity validation.</li>
* <li>XOR pass encryption for initial login server to game client handshake.</li>
* <li>Blowfish ECB encryption/decryption for ongoing packet security.</li>
* </ul>
*/
public class NewCrypt
{
// Constants.
private static final int BYTES_PER_BLOCK = 4;
private static final int CHECKSUM_SIZE = 4;
private static final int BLOWFISH_BLOCK_SIZE = 8;
private static final int XOR_KEY_OFFSET = 4;
private static final int XOR_FINAL_OFFSET = 8;
private static final int BYTE_MASK = 0xFF;
private static final int SHIFT_8_BITS = 8;
private static final int SHIFT_16_BITS = 16;
private static final int SHIFT_24_BITS = 24;
private static final long MASK_16_BITS = 0xFF00;
private static final long MASK_24_BITS = 0xFF0000;
private static final long MASK_32_BITS = 0xFF000000;
// Encryption Engine.
private final BlowfishEngine _blowfishCipher;
/**
* Creates new crypt instance with blowfish key bytes.
* @param blowfishKey
*/
public NewCrypt(byte[] blowfishKey)
{
_blowfishCipher = new BlowfishEngine();
_blowfishCipher.init(blowfishKey);
}
/**
* Creates new crypt instance with string key converted to bytes.
* @param key
*/
public NewCrypt(String key)
{
this(key.getBytes(StandardCharsets.UTF_8));
}
/**
* Verifies packet checksum for entire data array.
* @param rawData data array to be verified
* @return true when the checksum of the data is valid, false otherwise
*/
public static boolean verifyChecksum(byte[] rawData)
{
return verifyChecksum(rawData, 0, rawData.length);
}
/**
* Verifies packet checksum for data integrity validation.<br>
* Used for login server to game client and game server communication.
* @param rawData data array to be verified
* @param offset at which offset to start verifying
* @param size number of bytes to verify
* @return true if the checksum of the data is valid, false otherwise
*/
public static boolean verifyChecksum(byte[] rawData, int offset, int size)
{
// Check if size is multiple of 4 and if there is more than only the checksum.
if (((size & 3) != 0) || (size <= CHECKSUM_SIZE))
{
return false;
}
long calculatedChecksum = 0;
final int dataLength = size - CHECKSUM_SIZE;
long currentBlock = -1;
int position;
for (position = offset; position < dataLength; position += BYTES_PER_BLOCK)
{
currentBlock = rawData[position] & BYTE_MASK;
currentBlock |= (rawData[position + 1] << SHIFT_8_BITS) & MASK_16_BITS;
currentBlock |= (rawData[position + 2] << SHIFT_16_BITS) & MASK_24_BITS;
currentBlock |= (rawData[position + 3] << SHIFT_24_BITS) & MASK_32_BITS;
calculatedChecksum ^= currentBlock;
}
currentBlock = rawData[position] & BYTE_MASK;
currentBlock |= (rawData[position + 1] << SHIFT_8_BITS) & MASK_16_BITS;
currentBlock |= (rawData[position + 2] << SHIFT_16_BITS) & MASK_24_BITS;
currentBlock |= (rawData[position + 3] << SHIFT_24_BITS) & MASK_32_BITS;
return currentBlock == calculatedChecksum;
}
/**
* Appends packet checksum to entire data array.
* @param rawData data array to compute the checksum from
*/
public static void appendChecksum(byte[] rawData)
{
appendChecksum(rawData, 0, rawData.length);
}
/**
* Computes and appends packet checksum at the end of the packet.
* @param rawData data array to compute the checksum from
* @param offset offset where to start in the data array
* @param size number of bytes to compute the checksum from
*/
public static void appendChecksum(byte[] rawData, int offset, int size)
{
long calculatedChecksum = 0;
final int dataLength = size - CHECKSUM_SIZE;
long currentBlock;
int position;
for (position = offset; position < dataLength; position += BYTES_PER_BLOCK)
{
currentBlock = rawData[position] & BYTE_MASK;
currentBlock |= (rawData[position + 1] << SHIFT_8_BITS) & MASK_16_BITS;
currentBlock |= (rawData[position + 2] << SHIFT_16_BITS) & MASK_24_BITS;
currentBlock |= (rawData[position + 3] << SHIFT_24_BITS) & MASK_32_BITS;
calculatedChecksum ^= currentBlock;
}
currentBlock = rawData[position] & BYTE_MASK;
currentBlock |= (rawData[position + 1] << SHIFT_8_BITS) & MASK_16_BITS;
currentBlock |= (rawData[position + 2] << SHIFT_16_BITS) & MASK_24_BITS;
currentBlock |= (rawData[position + 3] << SHIFT_24_BITS) & MASK_32_BITS;
rawData[position] = (byte) (calculatedChecksum & BYTE_MASK);
rawData[position + 1] = (byte) ((calculatedChecksum >> SHIFT_8_BITS) & BYTE_MASK);
rawData[position + 2] = (byte) ((calculatedChecksum >> SHIFT_16_BITS) & BYTE_MASK);
rawData[position + 3] = (byte) ((calculatedChecksum >> SHIFT_24_BITS) & BYTE_MASK);
}
/**
* Encrypts packet with XOR encoding and appends the XOR key to entire data array.<br>
* Assumes sufficient room exists for the key without overwriting data.
* @param rawData The raw bytes to be encrypted
* @param xorKey The 4 bytes (int) XOR key
*/
public static void encXORPass(byte[] rawData, int xorKey)
{
encXORPass(rawData, 0, rawData.length, xorKey);
}
/**
* Encrypts packet with XOR encoding and appends the XOR key to the data.<br>
* Assumes sufficient room exists for the key without overwriting data.
* @param rawData The raw bytes to be encrypted
* @param offset The beginning of the data to be encrypted
* @param size Length of the data to be encrypted
* @param xorKey The 4 bytes (int) XOR key
*/
public static void encXORPass(byte[] rawData, int offset, int size, int xorKey)
{
final int endPosition = size - XOR_FINAL_OFFSET;
int currentPosition = XOR_KEY_OFFSET + offset;
int dataBlock;
int encryptionKey = xorKey; // Initial xor key.
while (currentPosition < endPosition)
{
dataBlock = rawData[currentPosition] & BYTE_MASK;
dataBlock |= (rawData[currentPosition + 1] & BYTE_MASK) << SHIFT_8_BITS;
dataBlock |= (rawData[currentPosition + 2] & BYTE_MASK) << SHIFT_16_BITS;
dataBlock |= (rawData[currentPosition + 3] & BYTE_MASK) << SHIFT_24_BITS;
encryptionKey += dataBlock;
dataBlock ^= encryptionKey;
rawData[currentPosition++] = (byte) (dataBlock & BYTE_MASK);
rawData[currentPosition++] = (byte) ((dataBlock >> SHIFT_8_BITS) & BYTE_MASK);
rawData[currentPosition++] = (byte) ((dataBlock >> SHIFT_16_BITS) & BYTE_MASK);
rawData[currentPosition++] = (byte) ((dataBlock >> SHIFT_24_BITS) & BYTE_MASK);
}
rawData[currentPosition++] = (byte) (encryptionKey & BYTE_MASK);
rawData[currentPosition++] = (byte) ((encryptionKey >> SHIFT_8_BITS) & BYTE_MASK);
rawData[currentPosition++] = (byte) ((encryptionKey >> SHIFT_16_BITS) & BYTE_MASK);
rawData[currentPosition++] = (byte) ((encryptionKey >> SHIFT_24_BITS) & BYTE_MASK);
}
/**
* Decrypts data using Blowfish cipher in ECB mode.<br>
* Results are placed directly inside the raw array without error checking.
* @param rawData the data array to be decrypted
* @param offset the offset at which to start decrypting
* @param size the number of bytes to be decrypted
*/
public void decrypt(byte[] rawData, int offset, int size)
{
for (int i = offset; i < (offset + size); i += BLOWFISH_BLOCK_SIZE)
{
_blowfishCipher.decryptBlock(rawData, i);
}
}
/**
* Encrypts data using Blowfish cipher in ECB mode.<br>
* Results are placed directly inside the raw array without error checking.
* @param rawData the data array to be encrypted
* @param offset the offset at which to start encrypting
* @param size the number of bytes to be encrypted
*/
public void crypt(byte[] rawData, int offset, int size)
{
for (int i = offset; i < (offset + size); i += BLOWFISH_BLOCK_SIZE)
{
_blowfishCipher.encryptBlock(rawData, i);
}
}
}
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.database;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
import org.l2jmobius.commons.config.DatabaseConfig;
/**
* @author Mobius
*/
public class DatabaseBackup
{
private static final DateTimeFormatter BACKUP_FORMAT = DateTimeFormatter.ofPattern("_yyyy_MM_dd_HH_mm'.sql'");
public static void performBackup(String description)
{
// Delete old files.
if (DatabaseConfig.BACKUP_DAYS > 0)
{
final long cut = LocalDateTime.now().minusDays(DatabaseConfig.BACKUP_DAYS).toEpochSecond(ZoneOffset.UTC);
final Path path = Paths.get(DatabaseConfig.BACKUP_PATH);
try
{
Files.list(path).filter(n ->
{
try
{
return Files.getLastModifiedTime(n).to(TimeUnit.SECONDS) < cut;
}
catch (Exception ex)
{
return false;
}
}).forEach(n ->
{
try
{
Files.delete(n);
}
catch (Exception ex)
{
// Ignore.
}
});
}
catch (Exception e)
{
// Ignore.
}
}
// Dump to file.
final String mysqldumpPath = System.getProperty("os.name").toLowerCase().contains("win") ? DatabaseConfig.MYSQL_BIN_PATH : "";
try
{
// Java 17
// final Process process = Runtime.getRuntime().exec(mysqldumpPath + "mysqldump -u " + Config.DATABASE_LOGIN + (Config.DATABASE_PASSWORD.trim().isEmpty() ? "" : " -p" + Config.DATABASE_PASSWORD) + " "
// + Config.DATABASE_URL.replace("jdbc:mysql://", "").replaceAll(".*\\/|\\?.*", "") + " -r " + Config.BACKUP_PATH + description + BACKUP_FORMAT.format(LocalDateTime.now()));
// Java 18
final String backupFileName = DatabaseConfig.BACKUP_PATH + description + BACKUP_FORMAT.format(LocalDateTime.now());
final String databaseName = DatabaseConfig.DATABASE_URL.replace("jdbc:mysql://", "").replaceAll(".*\\/|\\?.*", "");
final String[] command =
{
mysqldumpPath + "mysqldump",
"-u",
DatabaseConfig.DATABASE_LOGIN,
DatabaseConfig.DATABASE_PASSWORD.trim().isEmpty() ? "" : "-p" + DatabaseConfig.DATABASE_PASSWORD,
databaseName,
"-r",
backupFileName
};
final Process process = Runtime.getRuntime().exec(command);
process.waitFor();
}
catch (Exception e)
{
// Ignore.
}
}
}
@@ -0,0 +1,290 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.database;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.l2jmobius.commons.config.DatabaseConfig;
/**
* DatabaseFactory class using HikariCP for connection pooling.<br>
* Configured for high-load environments with 2000-3000 players.<br>
* Singleton implementation to ensure a single pool instance.
* @author Mobius
* @since November 10th 2018
* @version October 16th 2024
*/
public class DatabaseFactory
{
private static final Logger LOGGER = Logger.getLogger(DatabaseFactory.class.getName());
private static HikariDataSource DATABASE_POOL;
private DatabaseFactory()
{
}
/**
* Initializes the HikariCP connection pool with optimized settings.<br>
* Ensures that the pool is initialized only once.
*/
public static synchronized void init()
{
if ((DATABASE_POOL != null) && !DATABASE_POOL.isClosed())
{
LOGGER.warning("Database: Connection pool is already initialized.");
return;
}
// Load configurations.
DatabaseConfig.load();
try
{
final HikariConfig config = new HikariConfig();
config.setDriverClassName(DatabaseConfig.DATABASE_DRIVER);
config.setJdbcUrl(DatabaseConfig.DATABASE_URL);
config.setUsername(DatabaseConfig.DATABASE_LOGIN);
config.setPassword(DatabaseConfig.DATABASE_PASSWORD);
// Pool Size Configuration.
config.setMaximumPoolSize(determineMaxPoolSize(DatabaseConfig.DATABASE_MAX_CONNECTIONS)); // 100
config.setMinimumIdle(determineMinimumIdle(DatabaseConfig.DATABASE_MAX_CONNECTIONS)); // e.g., 20
// Timeout Settings.
config.setConnectionTimeout(60000); // 1 minute.
config.setIdleTimeout(300000); // 5 minutes.
config.setMaxLifetime(600000); // 10 minutes.
// Leak Detection.
config.setLeakDetectionThreshold(600000); // 10 minutes.
// Pool Name for Identification.
config.setPoolName("L2JMobiusPool");
// Register MBeans for Monitoring.
config.setRegisterMbeans(true);
// Additional Optimizations.
config.setInitializationFailTimeout(-1);
config.setValidationTimeout(5000); // 5 seconds.
// Initialize HikariDataSource.
DATABASE_POOL = new HikariDataSource(config);
LOGGER.info("Database: HikariCP pool initialized successfully.");
if (DatabaseConfig.DATABASE_TEST_CONNECTIONS)
{
testDatabaseConnections();
}
else
{
testSingleConnection();
}
}
catch (Exception e)
{
LOGGER.log(Level.SEVERE, "Database: Failed to initialize HikariCP pool.", e);
}
}
/**
* Determines the appropriate maximum pool size based on configuration and server capacity.
* @param configuredMax The configured maximum pool size from Config.
* @return Adjusted maximum pool size.
*/
private static int determineMaxPoolSize(int configuredMax)
{
return Math.clamp(configuredMax, 4, 1000);
}
/**
* Determines the appropriate minimum idle connections based on configuration.
* @param configuredMax The configured maximum pool size from Config.
* @return Adjusted minimum idle connections.
*/
private static int determineMinimumIdle(int configuredMax)
{
return Math.max(determineMaxPoolSize(configuredMax) / 10, 2);
}
/**
* Tests the database connections by attempting to open the maximum number of connections.<br>
* Adjusts the pool size if necessary based on successful connections.
*/
private static void testDatabaseConnections()
{
final List<Connection> connections = new ArrayList<>();
int successfulConnections = 0;
try
{
LOGGER.info("Database: Testing database connections...");
for (int i = 0; i < DATABASE_POOL.getMaximumPoolSize(); i++)
{
Connection connection = null;
try
{
connection = DATABASE_POOL.getConnection();
connections.add(connection);
successfulConnections++;
LOGGER.info("Database: Successfully opened connection " + connection.toString() + ".");
}
catch (SQLException e)
{
LOGGER.log(Level.SEVERE, "Database: Failed to open connection " + (i + 1) + "!", e);
break;
}
}
if (successfulConnections == DATABASE_POOL.getMaximumPoolSize())
{
LOGGER.info("Database: Initialized with a total of " + successfulConnections + " connections.");
}
else
{
LOGGER.warning("Database: Only " + successfulConnections + " out of " + DATABASE_POOL.getMaximumPoolSize() + " connections were successful.");
adjustPoolSize(successfulConnections);
}
}
finally // Close all opened connections.
{
for (Connection connection : connections)
{
if (connection != null)
{
try
{
connection.close();
}
catch (SQLException e)
{
LOGGER.log(Level.SEVERE, "Database: Error closing connection.", e);
}
}
}
}
}
/**
* Adjusts the pool size based on the number of successful connections.
* @param successfulConnections Number of connections that were successfully opened.
*/
private static void adjustPoolSize(int successfulConnections)
{
LOGGER.warning("Database: Adjusting pool size based on successful connections.");
// Calculate new pool size, reducing in steps to find a stable number.
int newConnectionCount = successfulConnections;
if (successfulConnections > 100)
{
newConnectionCount = (successfulConnections / 100) * 100;
}
else if (successfulConnections > 50)
{
newConnectionCount = (successfulConnections / 50) * 50;
}
// Ensure a minimum pool size of 20.
newConnectionCount = Math.max(newConnectionCount, 20);
// Update pool configuration.
try
{
DATABASE_POOL.setMaximumPoolSize(newConnectionCount);
DATABASE_POOL.setMinimumIdle(determineMinimumIdle(newConnectionCount));
LOGGER.info("Database: Reinitialized pool size to " + newConnectionCount + ".");
}
catch (Exception e)
{
LOGGER.log(Level.SEVERE, "Database: Failed to adjust pool size.", e);
}
}
/**
* Tests a single connection to verify database connectivity.
*/
private static void testSingleConnection()
{
try (Connection connection = DATABASE_POOL.getConnection())
{
if (connection.isValid(5))
{
LOGGER.info("Database: Initialized with a valid connection.");
}
else
{
LOGGER.warning("Database: Connection is not valid.");
}
}
catch (SQLException e)
{
LOGGER.log(Level.SEVERE, "Database: Problem initializing connection pool.", e);
}
}
/**
* Retrieves a connection from the pool.
* @return A valid database connection.
*/
public static Connection getConnection()
{
try
{
return DATABASE_POOL.getConnection();
}
catch (SQLException e)
{
LOGGER.log(Level.SEVERE, "Database: Could not get a connection.", e);
throw new RuntimeException("Unable to obtain a database connection.", e);
}
}
/**
* Closes the HikariCP connection pool gracefully.
*/
public static synchronized void close()
{
if ((DATABASE_POOL != null) && !DATABASE_POOL.isClosed())
{
try
{
DATABASE_POOL.close();
LOGGER.info("Database: HikariCP pool closed successfully.");
}
catch (Exception e)
{
LOGGER.log(Level.SEVERE, "Database: There was a problem closing the data source.", e);
}
}
}
}
@@ -0,0 +1,465 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network;
import java.util.Collection;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.l2jmobius.commons.network.buffer.ReadBuffer;
import org.l2jmobius.commons.network.buffer.WriteBuffer;
import org.l2jmobius.commons.network.handler.ReadHandler;
import org.l2jmobius.commons.network.handler.WriteHandler;
import org.l2jmobius.commons.network.packet.WritablePacket;
import org.l2jmobius.commons.network.pool.ResourcePool;
/**
* Abstract client entity that owns a {@link Connection} and manages packet I/O.<br>
* <br>
* <b>Fair-send mechanism:</b> a single global {@link ConcurrentLinkedQueue} ({@code PENDING_CLIENTS}) is used across all active clients. Whenever a client wants to send it atomically acquires the {@code _writing} flag, then adds itself to the pending queue.<br>
* The client polled from the front of the queue sends its next packet. This round-robin approach ensures no single client can monopolise the I/O threads when many clients are active simultaneously.<br>
* <br>
* <b>Packet dropping:</b> when {@link ConnectionConfig#dropPackets} is enabled, packets that return {@code true} from {@link WritablePacket#canBeDropped} are silently discarded once the estimated outbound queue exceeds {@link ConnectionConfig#dropPacketThreshold}.
* @param <T> the concrete {@link Connection} subtype bound to this client
* @author JoeAlisson, Mobius
*/
public abstract class Client<T extends Connection<?>>
{
/**
* Global fair-send queue shared across all client instances.<br>
* Unbounded; clients add themselves when they start sending and are polled by the completing write to find the next sender.
*/
private static final ConcurrentLinkedQueue<Client<?>> PENDING_CLIENTS = new ConcurrentLinkedQueue<>();
private final T _connection;
private final Queue<WritablePacket<? extends Client<T>>> _packetsToWrite = new ConcurrentLinkedQueue<>();
private final AtomicBoolean _writing = new AtomicBoolean();
private final AtomicBoolean _disconnecting = new AtomicBoolean();
private final AtomicBoolean _closing = new AtomicBoolean();
private final AtomicInteger _estimateQueueSize = new AtomicInteger();
private final AtomicInteger _dataSentSize = new AtomicInteger();
// Read-side state (accessed only on the single async-read thread for this client).
private boolean _readingPayload;
private int _expectedReadSize;
/**
* Constructs a client bound to the given connection.
* @param connection the open connection; must not be {@code null} or closed
* @throws IllegalArgumentException if {@code connection} is {@code null} or already closed
*/
protected Client(T connection)
{
if ((connection == null) || !connection.isOpen())
{
throw new IllegalArgumentException("The connection is null or closed.");
}
_connection = connection;
}
/**
* Queues {@code packet} for transmission and triggers the fair-send loop if needed.
* @param packet the packet to send; ignored if {@code null}
*/
protected void writePacket(WritablePacket<? extends Client<T>> packet)
{
if (!isConnected() || (packet == null) || packetCanBeDropped(packet))
{
return;
}
_estimateQueueSize.incrementAndGet();
_packetsToWrite.add(packet);
writeFairPacket();
}
/**
* Queues multiple packets and triggers the fair-send loop if needed.
* @param packets the collection of packets to send; ignored if {@code null} or empty
*/
protected void writePackets(Collection<WritablePacket<? extends Client<T>>> packets)
{
if (!isConnected() || (packets == null) || packets.isEmpty())
{
return;
}
_estimateQueueSize.addAndGet(packets.size());
_packetsToWrite.addAll(packets);
writeFairPacket();
}
/**
* Determines if a packet can be dropped based on the connection's drop packet settings.
* @param packet The packet to check.
* @return True if the packet can be dropped, false otherwise.
*/
@SuppressWarnings(
{
"unchecked",
"rawtypes"
})
private boolean packetCanBeDropped(WritablePacket packet)
{
return _connection.dropPackets() && (_estimateQueueSize.get() > _connection.dropPacketThreshold()) && packet.canBeDropped(this);
}
/**
* Enters the send loop for this client if it is not already active.
*/
private void writeFairPacket()
{
if (_writing.compareAndSet(false, true))
{
sendFairPacket();
}
}
/**
* Adds this client to the global pending queue, then polls one client to send its next packet.<br>
* The polled client may be {@code this} or any other client waiting to send.
*/
private void sendFairPacket()
{
PENDING_CLIENTS.offer(this);
final Client<?> nextClient = PENDING_CLIENTS.poll();
if (nextClient != null)
{
nextClient.writeNextPacket();
}
}
/**
* Sends the next queued packet, or releases the write lock if the queue is empty.
*/
private void writeNextPacket()
{
final WritablePacket<? extends Client<T>> packet = _packetsToWrite.poll();
if (packet == null)
{
releaseWritingResource();
// A packet may have been queued by another thread between the empty poll above and the release of the writing flag; that thread's CAS failed, so this thread must restart the send loop.
if (!_packetsToWrite.isEmpty())
{
writeFairPacket();
return;
}
if (_closing.get())
{
// A close(packet) may have queued its final packet concurrently, or another thread may be writing it right now; that thread's completion path will come back here and disconnect.
if (!_packetsToWrite.isEmpty() || _writing.get())
{
return;
}
disconnect();
}
}
else
{
_estimateQueueSize.decrementAndGet();
write(packet);
}
}
/**
* Writes a specified packet to the connection. Encrypts the data, writes headers and manages the buffer.<br>
* If the packet cannot be written, it handles resource release and retries.
* @param packet The packet to be written.
*/
@SuppressWarnings(
{
"unchecked",
"rawtypes"
})
private void write(WritablePacket packet)
{
boolean written = false;
WriteBuffer buffer = null;
try
{
buffer = packet.writeData(this);
final int payloadSize = buffer.limit() - ConnectionConfig.HEADER_SIZE;
if (payloadSize <= 0)
{
return;
}
if (encrypt(buffer, ConnectionConfig.HEADER_SIZE, payloadSize))
{
final int bufferLimit = buffer.limit();
_dataSentSize.set(bufferLimit);
if (bufferLimit <= ConnectionConfig.HEADER_SIZE)
{
return;
}
packet.writeHeader(buffer, bufferLimit);
written = _connection.write(buffer.toByteBuffers());
}
}
catch (Exception e)
{
// Intentionally silent - a broken packet must not crash the I/O thread.
}
finally
{
if (!written)
{
handleNotWritten(buffer);
}
}
}
/**
* Handles scenarios where a packet could not be written successfully.<br>
* Releases any associated buffer resources and re-attempts the packet send if the client is still connected.
* @param buffer The buffer containing packet data, which may need resource release.
*/
private void handleNotWritten(WriteBuffer buffer)
{
if (!releaseWritingResource() && (buffer != null))
{
buffer.releaseResources();
}
if (isConnected())
{
writeFairPacket();
}
}
/**
* Starts reading the next packet header from the connection.
*/
public void read()
{
_expectedReadSize = ConnectionConfig.HEADER_SIZE;
_readingPayload = false;
_connection.readHeader();
}
/**
* Transitions to payload-reading mode for the given data size.
* @param dataSize the number of payload bytes to read
*/
public void readPayload(int dataSize)
{
_expectedReadSize = dataSize;
_readingPayload = true;
_connection.read(dataSize);
}
/**
* Closes the connection immediately, discarding all pending outbound packets.
*/
public void close()
{
close(null);
}
/**
* Sends {@code packet} (if non-null) and then closes the connection.<br>
* All other queued packets are discarded.
* @param packet a final packet to transmit before closing, or {@code null}
*/
public void close(WritablePacket<? extends Client<T>> packet)
{
if (!isConnected())
{
return;
}
_packetsToWrite.clear();
if (packet != null)
{
_packetsToWrite.add(packet);
}
_closing.set(true);
writeFairPacket();
}
/**
* Called by {@link WriteHandler} after a partial write; adjusts the remaining-bytes counter and retries the write with the remainder.
* @param result the number of bytes that were successfully sent
*/
public void resumeSend(int result)
{
_dataSentSize.addAndGet(-result);
_connection.write();
}
/**
* Called by {@link WriteHandler} after a complete write; releases buffers and sends the next packet.
*/
public void finishWriting()
{
_connection.releaseWritingBuffer();
sendFairPacket();
}
private boolean releaseWritingResource()
{
final boolean released = _connection.releaseWritingBuffer();
_writing.set(false);
return released;
}
/**
* Disconnects the client: calls {@link #onDisconnection()}, clears all pending packets, and closes the underlying channel. Guaranteed to execute at most once.
*/
public void disconnect()
{
if (_disconnecting.compareAndSet(false, true))
{
try
{
onDisconnection();
}
finally
{
_packetsToWrite.clear();
_connection.close();
}
}
}
/**
* Returns the connection bound to this client.
* @return the connection
*/
public T getConnection()
{
return _connection;
}
/**
* Returns the total size of the data being sent in the current write operation.
* @return bytes currently in flight
*/
public int getDataSentSize()
{
return _dataSentSize.get();
}
/**
* Returns the remote peer's IP address.
* @return IP string, or an empty string if unavailable
*/
public String getHostAddress()
{
return _connection == null ? "" : _connection.getRemoteAddress();
}
/**
* Returns {@code true} if the connection is open and a close has not been requested.
* @return {@code true} if connected
*/
public boolean isConnected()
{
return _connection.isOpen() && !_closing.get();
}
/**
* Returns the estimated number of packets waiting in the outbound queue.
* @return estimated queue depth
*/
public int getEstimateQueueSize()
{
return _estimateQueueSize.get();
}
/**
* Returns the {@link ResourcePool} used by the underlying connection.
* @return the resource pool
*/
public ResourcePool getResourcePool()
{
return _connection.getResourcePool();
}
/**
* Returns {@code true} when the client is in the middle of reading a packet payload (as opposed to reading a header).
* @return {@code true} if reading payload
*/
public boolean isReadingPayload()
{
return _readingPayload;
}
/**
* Called by {@link ReadHandler} after a partial read; subtracts the bytes already received and issues another read for the remainder.
* @param bytesRead the number of bytes received in the partial read
*/
public void resumeRead(int bytesRead)
{
_expectedReadSize -= bytesRead;
_connection.read();
}
/**
* Returns the number of bytes still expected from the current read operation.
* @return remaining expected bytes
*/
public int getExpectedReadSize()
{
return _expectedReadSize;
}
/**
* Encrypts the packet data in-place within {@code data}.
* @param data the outbound buffer containing the data to encrypt
* @param offset byte offset at which the encryptable region starts
* @param size number of bytes to encrypt
* @return {@code true} if encryption succeeded; {@code false} to abort sending
*/
public abstract boolean encrypt(WriteBuffer data, int offset, int size);
/**
* Decrypts the packet data in-place within {@code data}.
* @param data the inbound buffer containing the data to decrypt
* @param offset byte offset at which the decryptable region starts
* @param size number of bytes to decrypt
* @return {@code true} if decryption succeeded; {@code false} to abort processing
*/
public abstract boolean decrypt(ReadBuffer data, int offset, int size);
/**
* Called once when the client disconnects.<br>
* Implementations must persist state and release all application-level resources.<br>
* No further packets can be sent after this method returns.
*/
protected abstract void onDisconnection();
/**
* Called once immediately after the connection is accepted.<br>
* Implementations should not block; outbound packets may be sent from this method onward.
*/
public abstract void onConnected();
}
@@ -0,0 +1,302 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.l2jmobius.commons.network.handler.ReadHandler;
import org.l2jmobius.commons.network.handler.WriteHandler;
import org.l2jmobius.commons.network.pool.ResourcePool;
/**
* Manages a single network connection backed by an {@link AsynchronousSocketChannel}.<br>
* <br>
* Owns the lifecycle of both the read buffer (single {@link ByteBuffer} obtained from the {@link ResourcePool}) and the write buffer array (supplied per-packet by the {@link Client}).<br>
* All asynchronous I/O is dispatched from here; completion logic lives in {@link ReadHandler} and {@link WriteHandler}.<br>
* <br>
* Thread-safety: the {@code _client} reference is declared {@code volatile} because it is read by NIO completion-handler threads.<br>
* An {@link AtomicBoolean} guards {@link #close()} against double-close races that can occur when a read failure and a write failure fire concurrently.
* @param <T> the client type associated with this connection
* @author JoeAlisson, Mobius
*/
public class Connection<T extends Client<Connection<T>>>
{
private final AsynchronousSocketChannel _channel;
private final ReadHandler<T> _readHandler;
private final WriteHandler<T> _writeHandler;
private final ConnectionConfig _config;
/** Volatile because NIO completion-handler threads read this field. */
private volatile T _client;
/** Ensures {@link #close()} body executes at most once. */
private final AtomicBoolean _closed = new AtomicBoolean();
private ByteBuffer _readingBuffer;
private ByteBuffer[] _writingBuffers;
/**
* Creates a new connection around an already-accepted channel.
* @param channel the open async socket channel
* @param readHandler completion handler for read operations
* @param writeHandler completion handler for write operations
* @param config shared configuration (resource pool, thresholds, etc.)
*/
public Connection(AsynchronousSocketChannel channel, ReadHandler<T> readHandler, WriteHandler<T> writeHandler, ConnectionConfig config)
{
_channel = channel;
_readHandler = readHandler;
_writeHandler = writeHandler;
_config = config;
}
/**
* Binds a client to this connection. Must be called exactly once, immediately after construction.
* @param client the owning client
*/
public void setClient(T client)
{
_client = client;
}
// ------------------------------------------------------------------
// Read operations.
// ------------------------------------------------------------------
/**
* Submits an async read into the current reading buffer.<br>
* No-op if the channel has already been closed.
*/
public void read()
{
if (_channel.isOpen())
{
_channel.read(_readingBuffer, _client, _readHandler);
}
}
/**
* Prepares for the next packet by recycling the old reading buffer, obtaining a fresh header-sized buffer from the pool and starting a read.
*/
public void readHeader()
{
if (_channel.isOpen())
{
recycleReadBuffer();
_readingBuffer = _config.resourcePool.getHeaderBuffer();
read();
}
}
/**
* Swaps the reading buffer for one that can hold {@code size} bytes and starts a read.
* @param size required capacity in bytes
*/
public void read(int size)
{
if (_channel.isOpen())
{
_readingBuffer = _config.resourcePool.recycleAndGetNew(_readingBuffer, size);
read();
}
}
/**
* Returns the buffer that is currently receiving inbound data.
* @return the reading buffer, or {@code null} between packets
*/
public ByteBuffer getReadingBuffer()
{
return _readingBuffer;
}
// ------------------------------------------------------------------
// Write operations.
// ------------------------------------------------------------------
/**
* Begins a gather-write of the supplied buffers to the channel.
* @param buffers one or more buffers containing the outgoing packet data
* @return {@code true} if the write was submitted; {@code false} if the channel is closed
*/
public boolean write(ByteBuffer[] buffers)
{
if (!_channel.isOpen())
{
return false;
}
_writingBuffers = buffers;
write();
return true;
}
/**
* Continues (or completes) a previously started gather-write.<br>
* If the channel is closed or no buffers remain, signals the client via {@link Client#finishWriting()} so it can dequeue the next packet.
*/
public void write()
{
if (_channel.isOpen() && (_writingBuffers != null))
{
_channel.write(_writingBuffers, 0, _writingBuffers.length, -1, TimeUnit.MILLISECONDS, _client, _writeHandler);
}
else if (_client != null)
{
_client.finishWriting();
}
}
/**
* Returns every write buffer to the resource pool.
* @return {@code true} if at least one buffer was recycled
*/
public boolean releaseWritingBuffer()
{
final ByteBuffer[] buffers = _writingBuffers;
if (buffers == null)
{
return false;
}
_writingBuffers = null;
for (ByteBuffer buf : buffers)
{
_config.resourcePool.recycleBuffer(buf);
}
return true;
}
// ------------------------------------------------------------------
// Lifecycle.
// ------------------------------------------------------------------
/**
* Closes the connection, releasing all pooled buffers and shutting down the channel.<br>
* Safe to call from multiple threads - only the first invocation performs cleanup.
*/
public void close()
{
if (!_closed.compareAndSet(false, true))
{
return;
}
recycleReadBuffer();
releaseWritingBuffer();
try
{
if (_channel.isOpen())
{
_channel.close();
}
}
catch (IOException ignored)
{
// Channel was already broken - nothing useful to do.
}
finally
{
_client = null;
}
}
/**
* Returns {@code true} if the underlying channel is still open.
* @return channel open state
*/
public boolean isOpen()
{
return _channel.isOpen();
}
// ------------------------------------------------------------------
// Accessors.
// ------------------------------------------------------------------
/**
* Resolves the remote peer's IP address.
* @return dotted-quad (or IPv6) address string, or {@code ""} if the channel is closed
*/
public String getRemoteAddress()
{
try
{
final InetSocketAddress remote = (InetSocketAddress) _channel.getRemoteAddress();
return remote.getAddress().getHostAddress();
}
catch (IOException e)
{
return "";
}
}
/**
* Returns the {@link ResourcePool} shared by all connections on this acceptor.
* @return the resource pool
*/
public ResourcePool getResourcePool()
{
return _config.resourcePool;
}
/**
* Indicates whether the server is configured to silently drop expendable packets when a client's outbound queue grows too large.
* @return {@code true} if packet dropping is enabled
*/
public boolean dropPackets()
{
return _config.dropPackets;
}
/**
* Returns the queue depth at which expendable packets begin to be dropped.
* @return the drop threshold
*/
public int dropPacketThreshold()
{
return _config.dropPacketThreshold;
}
// ------------------------------------------------------------------
// Internal helpers.
// ------------------------------------------------------------------
/**
* Returns the current reading buffer to the pool and nulls the reference.
*/
private void recycleReadBuffer()
{
final ByteBuffer buf = _readingBuffer;
if (buf != null)
{
_readingBuffer = null;
_config.resourcePool.recycleBuffer(buf);
}
}
}
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network;
import java.net.SocketAddress;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.l2jmobius.commons.network.pool.BufferPool;
import org.l2jmobius.commons.network.pool.ResourcePool;
import org.l2jmobius.commons.util.ConfigReader;
/**
* Configures and initializes connection parameters for the network layer.<br>
* This class handles configuration settings, buffer pools and network properties.
* @author Mobius
*/
public class ConnectionConfig
{
public static final int HEADER_SIZE = 2;
private static final Pattern BUFFER_POOL_PROPERTY = Pattern.compile("(BufferPool\\.\\w+?\\.)Size", Pattern.CASE_INSENSITIVE);
private static final int MINIMUM_POOL_GROUPS = 3;
public ResourcePool resourcePool;
public SocketAddress address;
public float initBufferPoolFactor;
public long shutdownWaitTime;
public int threadPoolSize;
public boolean useNagle;
public boolean dropPackets;
public int dropPacketThreshold;
public int threadPriority;
public boolean autoExpandPoolCapacity;
/**
* Initializes the connection configuration with the specified socket address.
* @param socketAddress the address to which this configuration applies
*/
public ConnectionConfig(SocketAddress socketAddress)
{
address = socketAddress;
threadPoolSize = 2;
// Initialize Resource Pool and default buffer settings.
resourcePool = new ResourcePool();
resourcePool.addBufferPool(HEADER_SIZE, new BufferPool(100, HEADER_SIZE));
// Read configuration properties.
final ConfigReader networkConfig = new ConfigReader("config/Network.ini");
shutdownWaitTime = networkConfig.getInt("ShutdownWaitTime", 5) * 1000L;
// Configure thread pool based on processor count.
final int processors = Runtime.getRuntime().availableProcessors();
threadPoolSize = networkConfig.getInt("ThreadPoolSize", threadPoolSize);
threadPoolSize = threadPoolSize < 1 ? processors * 4 : threadPoolSize;
// Other network and buffer configurations.
threadPriority = networkConfig.getInt("ThreadPriority", Thread.NORM_PRIORITY);
autoExpandPoolCapacity = networkConfig.getBoolean("BufferPool.AutoExpandCapacity", true);
initBufferPoolFactor = networkConfig.getFloat("BufferPool.InitFactor", 0);
dropPackets = networkConfig.getBoolean("DropPackets", dropPackets);
dropPacketThreshold = networkConfig.getInt("DropPacketThreshold", 250);
resourcePool.setBufferSegmentSize(networkConfig.getInt("BufferSegmentSize", resourcePool.getSegmentSize()));
// Set up custom buffer pools from properties.
networkConfig.getStringPropertyNames().forEach(property ->
{
final Matcher matcher = BUFFER_POOL_PROPERTY.matcher(property);
if (matcher.matches())
{
final int size = networkConfig.getInt(property, 10);
final int bufferSize = networkConfig.getInt(matcher.group(1) + "BufferSize", 1024);
resourcePool.addBufferPool(bufferSize, new BufferPool(size, bufferSize));
}
});
// Add additional buffer pool for segment size.
resourcePool.addBufferPool(resourcePool.getSegmentSize(), new BufferPool(100, resourcePool.getSegmentSize()));
// Ensure minimum pool groups.
final int missingPools = MINIMUM_POOL_GROUPS - resourcePool.bufferPoolSize();
for (int i = 0; i < missingPools; i++)
{
final int bufferSize = 256 << i;
resourcePool.addBufferPool(bufferSize, new BufferPool(10, bufferSize));
}
// Initialize resource pool buffers.
resourcePool.initializeBuffers(autoExpandPoolCapacity, initBufferPoolFactor);
}
}
@@ -0,0 +1,169 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.l2jmobius.commons.network.handler.ReadHandler;
import org.l2jmobius.commons.network.handler.WriteHandler;
import org.l2jmobius.commons.network.packet.PacketExecutor;
import org.l2jmobius.commons.network.packet.PacketHandler;
import org.l2jmobius.commons.network.packet.PacketThreadFactory;
import org.l2jmobius.commons.network.packet.ReadablePacket;
/**
* Binds a server socket, accepts incoming TCP connections and initialises a {@link Client} for each one via a caller-supplied factory.<br>
* <br>
* The underlying {@link AsynchronousChannelGroup} uses a bounded thread pool sized by {@link ConnectionConfig#threadPoolSize}. A {@link LinkedBlockingQueue} work queue allows the pool to buffer short bursts of connection-accept events without spawning unbounded threads.
* @param <T> the concrete client type created for each accepted connection
* @author Mobius
*/
public class ConnectionManager<T extends Client<Connection<T>>>
{
private final AsynchronousChannelGroup _group;
private final AsynchronousServerSocketChannel _socketChannel;
private final ConnectionConfig _config;
private final WriteHandler<T> _writeHandler;
private final ReadHandler<T> _readHandler;
private final Function<Connection<T>, T> _clientFactory;
/**
* Binds the server to {@code address} and starts accepting connections.
* @param address the local address and port to listen on
* @param clientFactory function that creates a {@link Client} for a given {@link Connection}
* @param packetHandler handler that maps incoming bytes to concrete {@link ReadablePacket}s
* @throws IOException if the server socket cannot be opened or bound
*/
public ConnectionManager(InetSocketAddress address, Function<Connection<T>, T> clientFactory, PacketHandler<T> packetHandler) throws IOException
{
_config = new ConnectionConfig(address);
_clientFactory = clientFactory;
_readHandler = new ReadHandler<>(packetHandler, new PacketExecutor<>(_config));
_writeHandler = new WriteHandler<>();
// Bounded thread pool: core == max == threadPoolSize; idle threads kept alive for 1 minute.
// LinkedBlockingQueue buffers short accept bursts without spawning extra threads.
final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(_config.threadPoolSize, _config.threadPoolSize, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new PacketThreadFactory("Server", _config.threadPriority));
threadPool.allowCoreThreadTimeOut(true);
_group = AsynchronousChannelGroup.withThreadPool(threadPool);
_socketChannel = _group.provider().openAsynchronousServerSocketChannel(_group);
_socketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
_socketChannel.bind(_config.address);
// Begin the accept loop.
_socketChannel.accept(null, new AcceptConnectionHandler());
}
/**
* Stops accepting new connections and shuts down the channel group.<br>
* Waits up to {@link ConnectionConfig#shutdownWaitTime} ms for in-flight I/O to complete.
*/
public void shutdown()
{
try
{
_socketChannel.close();
_group.shutdown();
_group.awaitTermination(_config.shutdownWaitTime, TimeUnit.MILLISECONDS);
_group.shutdownNow();
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
}
catch (Exception e)
{
// Best-effort shutdown; errors here are not actionable.
}
}
private class AcceptConnectionHandler implements CompletionHandler<AsynchronousSocketChannel, Void>
{
@Override
public void completed(AsynchronousSocketChannel clientChannel, Void attachment)
{
// Re-arm the accept loop before processing the new channel so that further connections are not delayed by client initialisation.
if (_socketChannel.isOpen())
{
_socketChannel.accept(null, this);
}
processNewConnection(clientChannel);
}
@Override
public void failed(Throwable t, Void attachment)
{
// Re-arm even on failure so transient errors do not kill the accept loop.
if (_socketChannel.isOpen())
{
_socketChannel.accept(null, this);
}
}
private void processNewConnection(AsynchronousSocketChannel channel)
{
if ((channel == null) || !channel.isOpen())
{
return;
}
try
{
channel.setOption(StandardSocketOptions.TCP_NODELAY, !_config.useNagle);
final Connection<T> connection = new Connection<>(channel, _readHandler, _writeHandler, _config);
final T client = _clientFactory.apply(connection);
connection.setClient(client);
client.onConnected();
client.read();
}
catch (ClosedChannelException e)
{
// Channel was closed between accept and setup - nothing to do.
}
catch (Exception e)
{
try
{
channel.close();
}
catch (IOException ioe)
{
// Ignore; channel is already unusable.
}
}
}
}
}
@@ -0,0 +1,216 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.buffer;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Objects;
/**
* Inbound network buffer backed by a single NIO {@link ByteBuffer} handed in by the read handler.<br>
* <br>
* Provides sequential cursor-based reads (advancing the buffer's native position) plus absolute indexed accessors (used during decryption).<br>
* <br>
* All multi-byte values are read in <b>little-endian</b> order, matching the protocol convention. The wrapped buffer's byte order and position are used as-is; its limit defines the end of the decrypted payload.
* @author Mobius
*/
public class ReadBuffer
{
private final ByteBuffer _buf;
/**
* Wraps the supplied buffer. Its byte order and position are used as-is.
* @param buffer the NIO buffer to wrap (must not be {@code null})
* @throws NullPointerException if {@code buffer} is {@code null}
*/
public ReadBuffer(ByteBuffer buffer)
{
_buf = Objects.requireNonNull(buffer);
}
/**
* Factory that wraps a standard NIO {@link ByteBuffer} as a {@link ReadBuffer}.
* @param buffer the NIO buffer to adapt
* @return a {@link ReadBuffer} backed by {@code buffer}
*/
public static ReadBuffer of(ByteBuffer buffer)
{
return new ReadBuffer(buffer);
}
// -------------------------------------------------------------------------
// Sequential reads (cursor-based).
// -------------------------------------------------------------------------
/**
* Reads a little-endian UTF-16 {@code char} (2 bytes) and advances the position by 2.
* @return the char value
*/
public char readChar()
{
return _buf.getChar();
}
/**
* Reads a single byte and advances the position by 1.
* @return the byte value
*/
public byte readByte()
{
return _buf.get();
}
/**
* Reads a little-endian {@code short} (2 bytes) and advances the position by 2.
* @return the short value
*/
public short readShort()
{
return _buf.getShort();
}
/**
* Reads a little-endian {@code int} (4 bytes) and advances the position by 4.
* @return the int value
*/
public int readInt()
{
return _buf.getInt();
}
/**
* Reads a little-endian {@code long} (8 bytes) and advances the position by 8.
* @return the long value
*/
public long readLong()
{
return _buf.getLong();
}
/**
* Reads a little-endian IEEE 754 {@code float} (4 bytes) and advances the position by 4.
* @return the float value
*/
public float readFloat()
{
return _buf.getFloat();
}
/**
* Reads a little-endian IEEE 754 {@code double} (8 bytes) and advances the position by 8.
* @return the double value
*/
public double readDouble()
{
return _buf.getDouble();
}
/**
* Allocates a new array of the given length, fills it from the current position, and advances the position by {@code length}.
* @param length number of bytes to read
* @return newly allocated array containing the bytes
*/
public byte[] readBytes(int length)
{
// Validate before allocating, so a forged length cannot exhaust the heap.
if ((length < 0) || (length > _buf.remaining()))
{
throw new BufferUnderflowException();
}
final byte[] dst = new byte[length];
_buf.get(dst);
return dst;
}
/**
* Reads exactly {@code dst.length} bytes into the supplied array and advances the position.
* @param dst destination array
*/
public void readBytes(byte[] dst)
{
_buf.get(dst);
}
/**
* Reads {@code length} bytes into {@code dst} starting at {@code offset} and advances the position.
* @param dst destination array
* @param offset start index within {@code dst}
* @param length number of bytes to transfer
*/
public void readBytes(byte[] dst, int offset, int length)
{
_buf.get(dst, offset, length);
}
/**
* Returns the number of bytes between the current position and the limit.
* @return remaining bytes available for reading
*/
public int remaining()
{
return _buf.remaining();
}
// -------------------------------------------------------------------------
// Indexed (absolute) access - used during decryption.
// -------------------------------------------------------------------------
public byte readByte(int index)
{
return _buf.get(index);
}
public short readShort(int index)
{
return _buf.getShort(index);
}
public int readInt(int index)
{
return _buf.getInt(index);
}
public void writeByte(int index, byte value)
{
_buf.put(index, value);
}
public void writeShort(int index, short value)
{
_buf.putShort(index, value);
}
public void writeInt(int index, int value)
{
_buf.putInt(index, value);
}
public int limit()
{
return _buf.limit();
}
public void limit(int newLimit)
{
_buf.limit(newLimit);
}
}
@@ -0,0 +1,424 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.buffer;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.l2jmobius.commons.network.pool.ResourcePool;
/**
* Outbound network buffer backed by a single pooled direct {@link ByteBuffer} from a {@link ResourcePool}.<br>
* <br>
* Provides sequential cursor-based writes (advancing an internal position) plus absolute indexed writes (used for header and encryption patches).<br>
* <br>
* All multi-byte values are stored in <b>little-endian</b> order, matching the protocol convention.<br>
* <br>
* <b>Growth:</b> when a write would exceed the current capacity, a larger buffer is acquired from the pool, the existing bytes are copied into it, and the old buffer is returned to the pool.<br>
* <br>
* <b>Per-class size hints:</b> the maximum observed {@code limit()} for each packet class is tracked so that the initial allocation for subsequent sends is right-sized on the first try, avoiding realloc cycles.<br>
* <br>
* <b>Broadcast cache:</b> {@link #toByteArray()} snapshots the written content for reuse; {@link #WriteBuffer(byte[], int, ResourcePool, Class)} creates a per-recipient copy seeded from that snapshot.
* @author Mobius
*/
public class WriteBuffer
{
/** Per-packet-class largest observed packet size, used to right-size the initial pooled buffer. */
private static final Map<Class<?>, Integer> MAXIMUM_PACKET_SIZE = new ConcurrentHashMap<>();
private final ResourcePool _resourcePool;
private final Class<?> _packetClass;
private final int _initialSize;
private ByteBuffer _buf;
private int _position;
private int _limit;
/**
* Creates an empty buffer sized to the historical maximum for the given packet class, falling back to the pool's segment size.
* @param resourcePool pool for acquiring direct {@link ByteBuffer}s
* @param packetClass the packet class; used for size-hint tracking
*/
public WriteBuffer(ResourcePool resourcePool, Class<?> packetClass)
{
_resourcePool = resourcePool;
_packetClass = packetClass;
_initialSize = MAXIMUM_PACKET_SIZE.getOrDefault(packetClass, resourcePool.getSegmentSize());
_buf = resourcePool.getBuffer(_initialSize);
_limit = _buf.capacity();
}
/**
* Creates a buffer seeded from a broadcast-cache snapshot.<br>
* The pooled buffer is sized to hold {@code cachedLength} bytes, the snapshot is bulk-copied in, and the logical limit is set to {@code cachedLength}.
* @param cached the byte snapshot produced by {@link #toByteArray()}
* @param cachedLength the number of bytes in {@code cached} that are meaningful
* @param resourcePool pool for acquiring direct {@link ByteBuffer}s
* @param packetClass the packet class; used for size-hint tracking
*/
public WriteBuffer(byte[] cached, int cachedLength, ResourcePool resourcePool, Class<?> packetClass)
{
_resourcePool = resourcePool;
_packetClass = packetClass;
_initialSize = MAXIMUM_PACKET_SIZE.getOrDefault(packetClass, Math.max(cachedLength, resourcePool.getSegmentSize()));
_buf = resourcePool.getBuffer(Math.max(_initialSize, cachedLength));
_buf.position(0);
_buf.put(cached, 0, cachedLength);
_buf.position(0);
_position = cachedLength;
_limit = cachedLength;
}
/**
* Grows the underlying buffer if it cannot hold at least {@code required} bytes.<br>
* Growth factor is 1.5x, clamped to at least {@code required}.
* @param required the minimum capacity needed
*/
private void ensureSize(int required)
{
final int capacity = _buf.capacity();
if (capacity < required)
{
final int newSize = Math.max(required, (int) (capacity * 1.5));
final ByteBuffer newBuf = _resourcePool.getBuffer(newSize);
newBuf.position(0);
_buf.position(0).limit(Math.min(_position, capacity));
newBuf.put(_buf);
newBuf.position(0);
_resourcePool.recycleBuffer(_buf);
_buf = newBuf;
}
}
// -------------------------------------------------------------------------
// Sequential writes (cursor-based).
// -------------------------------------------------------------------------
/**
* Writes a single byte at the current position.
* @param value the byte to write
*/
public void writeByte(byte value)
{
ensureSize(_position + 1);
_buf.put(_position++, value);
}
/**
* Writes the lowest 8 bits of the given {@code int} as a single byte.
* @param value the value whose low byte is written
*/
public void writeByte(int value)
{
writeByte((byte) value);
}
/**
* Writes a boolean as a single byte - {@code 0x01} for {@code true}, {@code 0x00} for {@code false}.
* @param value the boolean to encode
*/
public void writeByte(boolean value)
{
writeByte((byte) (value ? 1 : 0));
}
/**
* Writes a variable-length byte sequence at the current position.
* @param value the bytes to write
*/
public void writeBytes(byte... value)
{
if ((value == null) || (value.length == 0))
{
return;
}
ensureSize(_position + value.length);
_buf.put(_position, value, 0, value.length);
_position += value.length;
}
/**
* Writes a little-endian {@code short} (2 bytes) at the current position.
* @param value the short to write
*/
public void writeShort(short value)
{
ensureSize(_position + 2);
_buf.putShort(_position, value);
_position += 2;
}
/**
* Writes the lowest 16 bits of the given {@code int} as a little-endian short.
* @param value the value whose low 16 bits are written
*/
public void writeShort(int value)
{
writeShort((short) value);
}
/**
* Writes a boolean as a little-endian short - {@code 1} for {@code true}, {@code 0} for {@code false}.
* @param value the boolean to encode
*/
public void writeShort(boolean value)
{
writeShort((short) (value ? 1 : 0));
}
/**
* Writes a little-endian {@code int} (4 bytes) at the current position.
* @param value the int to write
*/
public void writeInt(int value)
{
ensureSize(_position + 4);
_buf.putInt(_position, value);
_position += 4;
}
/**
* Writes a boolean as a little-endian int - {@code 1} for {@code true}, {@code 0} for {@code false}.
* @param value the boolean to encode
*/
public void writeInt(boolean value)
{
writeInt(value ? 1 : 0);
}
/**
* Writes a little-endian {@code long} (8 bytes) at the current position.
* @param value the long to write
*/
public void writeLong(long value)
{
ensureSize(_position + 8);
_buf.putLong(_position, value);
_position += 8;
}
/**
* Writes a little-endian IEEE 754 {@code float} (4 bytes) at the current position.
* @param value the float to write
*/
public void writeFloat(float value)
{
writeInt(Float.floatToRawIntBits(value));
}
/**
* Writes a little-endian IEEE 754 {@code double} (8 bytes) at the current position.
* @param value the double to write
*/
public void writeDouble(double value)
{
writeLong(Double.doubleToRawLongBits(value));
}
/**
* Writes a little-endian UTF-16 {@code char} (2 bytes) at the current position.
* @param value the character to write
*/
public void writeChar(char value)
{
writeShort((short) value);
}
/**
* Encodes a string as null-terminated UTF-16LE.<br>
* A {@code null} input produces only the null terminator (2 zero bytes).
* @param text the string to write, or {@code null}
*/
public void writeString(CharSequence text)
{
if (text != null)
{
final int len = text.length();
ensureSize(_position + (len << 1) + 2);
for (int i = 0; i < len; i++)
{
_buf.putShort(_position, (short) text.charAt(i));
_position += 2;
}
// Null terminator - capacity already reserved above.
_buf.putShort(_position, (short) 0);
_position += 2;
return;
}
writeChar('\000');
}
/**
* Encodes a string as a length-prefixed UTF-16LE sequence (no null terminator).
* @param text the string to write, or {@code null}
*/
public void writeSizedString(CharSequence text)
{
if ((text != null) && !text.isEmpty())
{
final int len = text.length();
ensureSize(_position + 2 + (len << 1));
_buf.putShort(_position, (short) len);
_position += 2;
for (int i = 0; i < len; i++)
{
_buf.putShort(_position, (short) text.charAt(i));
_position += 2;
}
return;
}
writeShort(0);
}
// -------------------------------------------------------------------------
// Indexed (absolute) writes and reads - used for header patching and encryption.
// -------------------------------------------------------------------------
public void writeByte(int index, byte value)
{
ensureSize(index + 1);
_buf.put(index, value);
}
public void writeShort(int index, short value)
{
ensureSize(index + 2);
_buf.putShort(index, value);
}
public void writeInt(int index, int value)
{
ensureSize(index + 4);
_buf.putInt(index, value);
}
public byte readByte(int index)
{
return _buf.get(index);
}
public short readShort(int index)
{
return _buf.getShort(index);
}
public int readInt(int index)
{
return _buf.getInt(index);
}
// -------------------------------------------------------------------------
// Position / limit / mark.
// -------------------------------------------------------------------------
/**
* Returns the current sequential write position.
* @return write cursor
*/
public int position()
{
return _position;
}
/**
* Moves the sequential write cursor.
* @param pos the new position
*/
public void position(int pos)
{
_position = pos;
}
public int limit()
{
return _limit;
}
public void limit(int newLimit)
{
ensureSize(newLimit);
_limit = newLimit;
}
/**
* Marks the current write position as the logical end of the buffer's content, setting the limit to the current position.
*/
public void mark()
{
_limit = _position;
}
// -------------------------------------------------------------------------
// Export / recycle.
// -------------------------------------------------------------------------
/**
* Prepares the underlying {@link ByteBuffer} for a channel write and exports it as a single-element array.<br>
* Updates the per-class size hint if this packet was larger than previously seen.
* @return a single-element array containing the underlying buffer, positioned at 0 with limit set to the logical packet length
*/
public ByteBuffer[] toByteBuffers()
{
if (_limit > _initialSize)
{
MAXIMUM_PACKET_SIZE.put(_packetClass, Math.min(_limit, 65535));
}
_buf.position(0);
_buf.limit(_limit);
return new ByteBuffer[]
{
_buf
};
}
/**
* Snapshots the written bytes as a fresh heap {@code byte[]} for broadcast caching.<br>
* The returned array has length {@link #limit()} and contains exactly the logical packet content.
* @return a new heap byte array holding the packet data
*/
public byte[] toByteArray()
{
final byte[] snapshot = new byte[_limit];
_buf.get(0, snapshot, 0, _limit);
return snapshot;
}
/**
* Returns the pooled {@link ByteBuffer} back to the {@link ResourcePool}.
*/
public void releaseResources()
{
if (_buf != null)
{
_resourcePool.recycleBuffer(_buf);
_buf = null;
}
_position = 0;
_limit = 0;
}
}
@@ -0,0 +1,170 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.handler;
import java.nio.ByteBuffer;
import java.nio.channels.CompletionHandler;
import org.l2jmobius.commons.network.Client;
import org.l2jmobius.commons.network.Connection;
import org.l2jmobius.commons.network.ConnectionConfig;
import org.l2jmobius.commons.network.buffer.ReadBuffer;
import org.l2jmobius.commons.network.packet.PacketExecutor;
import org.l2jmobius.commons.network.packet.PacketHandler;
import org.l2jmobius.commons.network.packet.ReadablePacket;
/**
* {@link CompletionHandler} for async read operations.<br>
* Processes completed reads, assembles the header/payload pair into a {@link ReadablePacket}, decrypts it and dispatches it to the {@link PacketExecutor}.<br>
* <br>
* <b>Packet size validation:</b> the 2-byte header encodes the total packet length (header + payload).<br>
* Packets whose declared payload size is 0 are silently skipped (keep-alive / heartbeat).<br>
* Packets whose declared size exceeds {@link #MAX_PACKET_SIZE} cause immediate disconnection to protect against memory exhaustion from malformed or malicious data.
* @param <T> the client type associated with this handler
* @author JoeAlisson, Mobius
*/
public class ReadHandler<T extends Client<Connection<T>>> implements CompletionHandler<Integer, T>
{
/** Maximum accepted payload size (65533 bytes = 0xFFFF total 2-byte header). */
private static final int MAX_PACKET_SIZE = 65533;
private final PacketHandler<T> _packetHandler;
private final PacketExecutor<T> _executor;
/**
* Creates a read handler backed by the given packet factory and executor.
* @param packetHandler factory that maps opcode bytes to {@link ReadablePacket} instances
* @param executor pool that will run accepted packets off the I/O thread
*/
public ReadHandler(PacketHandler<T> packetHandler, PacketExecutor<T> executor)
{
_packetHandler = packetHandler;
_executor = executor;
}
@Override
public void completed(Integer bytesRead, T client)
{
if (!client.isConnected())
{
return;
}
if (bytesRead < 0)
{
// Clean peer close.
client.disconnect();
return;
}
if (bytesRead < client.getExpectedReadSize())
{
// Partial read - resume until the full segment arrives.
client.resumeRead(bytesRead);
return;
}
if (client.isReadingPayload())
{
handlePayload(client);
}
else
{
handleHeader(client);
}
}
private void handleHeader(T client)
{
final ByteBuffer buffer = client.getConnection().getReadingBuffer();
if (buffer == null)
{
client.disconnect();
return;
}
buffer.flip();
// The 2-byte header is the total packet length (header + payload).
final int dataSize = Short.toUnsignedInt(buffer.getShort()) - ConnectionConfig.HEADER_SIZE;
if (dataSize <= 0)
{
// Zero-payload packet (keep-alive / heartbeat) - skip silently.
client.read();
return;
}
// Guard against packets large enough to cause OOM or exceed protocol limits.
if (dataSize > MAX_PACKET_SIZE)
{
client.disconnect();
return;
}
client.readPayload(dataSize);
}
private void handlePayload(T client)
{
final ByteBuffer buffer = client.getConnection().getReadingBuffer();
if (buffer == null)
{
client.disconnect();
return;
}
buffer.flip();
parseAndExecutePacket(client, buffer);
// Immediately start reading the next packet's header.
client.read();
}
private void parseAndExecutePacket(T client, ByteBuffer incomingBuffer)
{
try
{
final ReadBuffer buffer = ReadBuffer.of(incomingBuffer);
if (client.decrypt(buffer, 0, buffer.remaining()))
{
final ReadablePacket<T> packet = _packetHandler.handle(buffer, client);
if (packet != null)
{
packet.init(client, buffer);
if (packet.read())
{
_executor.execute(packet);
}
}
}
}
catch (Exception e)
{
failed(e, client);
}
}
@Override
public void failed(Throwable e, T client)
{
client.disconnect();
}
}
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.handler;
import java.nio.channels.CompletionHandler;
import org.l2jmobius.commons.network.Client;
import org.l2jmobius.commons.network.Connection;
/**
* {@link CompletionHandler} for async scatter-write operations.<br>
* Invoked by the NIO framework when a {@link Connection#write()} completes (fully or partially).<br>
* <ul>
* <li>Negative result -> peer disconnected; disconnect the client.</li>
* <li>Partial write -> call {@link Client#resumeSend} so the remaining bytes are retried.</li>
* <li>Full write -> call {@link Client#finishWriting} to release buffers and send the next queued packet.</li>
* </ul>
* @param <T> the client type associated with this handler
* @author JoeAlisson, Mobius
*/
public class WriteHandler<T extends Client<Connection<T>>> implements CompletionHandler<Long, T>
{
@Override
public void completed(Long result, T client)
{
if (client == null)
{
return;
}
final int bytesWritten = result.intValue();
if (bytesWritten < 0)
{
if (client.isConnected())
{
client.disconnect();
}
return;
}
if ((bytesWritten > 0) && (bytesWritten < client.getDataSentSize()))
{
// Partial write - continue sending the remaining data.
client.resumeSend(bytesWritten);
}
else
{
// All bytes sent - release buffers and process the next queued packet.
client.finishWriting();
}
}
@Override
public void failed(Throwable e, T client)
{
if (client != null)
{
client.disconnect();
}
}
}
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.packet;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.l2jmobius.commons.network.Client;
import org.l2jmobius.commons.network.Connection;
import org.l2jmobius.commons.network.ConnectionConfig;
/**
* Thread-pool-based executor for incoming network packets.<br>
* Received packets are submitted here after successful parsing so that their {@link ReadablePacket#run()} logic executes off the I/O thread.
* @param <T> the client type associated with the packets being executed
* @author Mobius
*/
public class PacketExecutor<T extends Client<Connection<T>>>
{
private static final Logger LOGGER = Logger.getLogger(PacketExecutor.class.getName());
private final ThreadPoolExecutor _executor;
/**
* Creates an executor sized according to {@link ConnectionConfig#threadPoolSize}.
* @param config the connection configuration supplying pool size and thread priority
*/
public PacketExecutor(ConnectionConfig config)
{
_executor = new ThreadPoolExecutor(config.threadPoolSize, Integer.MAX_VALUE, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new PacketThreadFactory("PacketExecutor", config.threadPriority));
}
/**
* Submits a packet for execution on a worker thread.
* @param packet the packet to execute
*/
public void execute(ReadablePacket<T> packet)
{
try
{
_executor.execute(new PacketRunnable<>(packet));
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Failed to submit " + packet.getClass().getSimpleName(), e);
}
}
private static class PacketRunnable<T extends Client<Connection<T>>> implements Runnable
{
private final ReadablePacket<T> _packet;
public PacketRunnable(ReadablePacket<T> packet)
{
_packet = packet;
}
@Override
public void run()
{
try
{
_packet.run();
}
catch (Throwable t)
{
final Thread currentThread = Thread.currentThread();
final UncaughtExceptionHandler handler = currentThread.getUncaughtExceptionHandler();
if (handler != null)
{
handler.uncaughtException(currentThread, t);
}
else
{
LOGGER.log(Level.SEVERE, "Uncaught exception in " + _packet.getClass().getSimpleName(), t);
}
}
}
}
}
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.packet;
import org.l2jmobius.commons.network.Client;
import org.l2jmobius.commons.network.Connection;
import org.l2jmobius.commons.network.buffer.ReadBuffer;
/**
* Packet-identification strategy.<br>
* <br>
* Implementations inspect the leading opcode byte(s) of a decrypted payload buffer and return the {@link ReadablePacket} subclass that knows how to parse the remaining fields, or {@code null} when the opcode is not recognised (the caller will silently discard the data).<br>
* <br>
* Because exactly one method is required, this interface is marked {@link FunctionalInterface} and can be supplied as a lambda or method reference.
* @param <T> the client type bound to the connection
* @author JoeAlisson, Mobius
*/
@FunctionalInterface
public interface PacketHandler<T extends Client<Connection<T>>>
{
/**
* Maps raw buffer data to a concrete packet instance.<br>
* The buffer is positioned at the first opcode byte; the implementation should read only the bytes it needs to identify the packet and leave the rest for<br>
* {@link ReadablePacket#read()}.
* @param buffer the decrypted payload, positioned at the first opcode byte
* @param client the client that sent this data (available for per-client branching)
* @return the matching {@link ReadablePacket}, or {@code null} if the opcode is unknown
*/
ReadablePacket<T> handle(ReadBuffer buffer, T client);
}
@@ -0,0 +1,107 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.packet;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Thread factory for creating and managing threads for network server tasks.<br>
* Provides custom naming conventions and priority management for better thread identification and performance tuning.
* <ul>
* <li>Generates unique thread names with pool and thread sequence numbers.</li>
* <li>Enforces priority constraints based on thread group limitations.</li>
* <li>Handles integer overflow for thread sequence numbering.</li>
* </ul>
* @author BazookaRpm
*/
public class PacketThreadFactory implements ThreadFactory
{
// Constants.
private static final String DEFAULT_BASE_NAME = "Thread";
private static final int INITIAL_SEQUENCE_VALUE = 1;
private static final int STACK_SIZE = 0; // Use JVM default stack size.
// Global pool sequence counter.
private static final AtomicInteger POOL_SEQUENCE = new AtomicInteger(INITIAL_SEQUENCE_VALUE);
// Thread naming and priority.
private final AtomicInteger _threadSequence = new AtomicInteger(INITIAL_SEQUENCE_VALUE);
private final String _threadPrefix;
private final int _threadPriority;
/**
* Creates a new network thread factory with specified base name and priority.
* @param baseName base name for thread naming
* @param priority thread priority level
*/
public PacketThreadFactory(String baseName, int priority)
{
final String safeBaseName = ((baseName == null) || baseName.isEmpty()) ? DEFAULT_BASE_NAME : baseName;
_threadPrefix = safeBaseName + "-network-pool-" + POOL_SEQUENCE.getAndIncrement() + "-thread-";
// Clamp priority to valid range.
if (priority < Thread.MIN_PRIORITY)
{
_threadPriority = Thread.MIN_PRIORITY;
}
else if (priority > Thread.MAX_PRIORITY)
{
_threadPriority = Thread.MAX_PRIORITY;
}
else
{
_threadPriority = priority;
}
}
/**
* Creates a new thread for the given task with configured naming and priority.
* @param task the runnable task
* @return newly created thread
*/
@Override
public Thread newThread(Runnable task)
{
final int threadIndex = nextIndex();
final Thread thread = new Thread(null, task, _threadPrefix + threadIndex, STACK_SIZE);
// Apply priority respecting thread group constraints.
final ThreadGroup threadGroup = thread.getThreadGroup();
final int groupMaxPriority = (threadGroup != null) ? threadGroup.getMaxPriority() : Thread.MAX_PRIORITY;
final int effectivePriority = (_threadPriority > groupMaxPriority) ? groupMaxPriority : _threadPriority;
thread.setPriority(effectivePriority);
thread.setDaemon(false);
return thread;
}
/**
* Gets the next thread index with overflow protection.
* @return next available thread index
*/
private int nextIndex()
{
final int currentValue = _threadSequence.getAndIncrement();
return (currentValue == Integer.MIN_VALUE) ? _threadSequence.updateAndGet(x -> (x <= 0) ? INITIAL_SEQUENCE_VALUE : x) : currentValue; // Handle integer overflow by resetting to 1.
}
}
@@ -0,0 +1,247 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.packet;
import java.nio.charset.StandardCharsets;
import org.l2jmobius.commons.network.Client;
import org.l2jmobius.commons.network.Connection;
import org.l2jmobius.commons.network.buffer.ReadBuffer;
/**
* Base class for all packets received from a client.<br>
* Subclasses implement {@link #read()} to parse fields from the buffer and then override {@link #run()} (inherited from {@link Runnable}) to act on those fields.<br>
* <br>
* The lifecycle managed by the network layer is:
* <ol>
* <li>{@link PacketHandler} creates the concrete subclass instance.</li>
* <li>{@link #init(Client, ReadBuffer)} binds the client and buffer.</li>
* <li>{@link #read()} is called on the I/O thread to parse the payload.</li>
* <li>If {@link #read()} returns {@code true}, the packet is submitted to a {@link PacketExecutor} which calls {@link #run()} on a worker thread.</li>
* </ol>
* @param <T> the client type associated with this packet
* @author JoeAlisson, Mobius
*/
public abstract class ReadablePacket<T extends Client<Connection<T>>> implements Runnable
{
private ReadBuffer _buffer;
private T _client;
protected ReadablePacket()
{
}
/**
* Binds this packet to the given client and buffer before parsing begins.
* @param client the client that sent this packet
* @param buffer the decrypted payload buffer
*/
public void init(T client, ReadBuffer buffer)
{
_client = client;
_buffer = buffer;
}
// -------------------------------------------------------------------------
// Delegating read helpers - all delegate to the bound buffer.
// -------------------------------------------------------------------------
/**
* Reads a little-endian {@code char} (2 bytes).
* @return the char value
*/
protected char readChar()
{
return _buffer.readChar();
}
/**
* Reads a single byte.
* @return the byte value
*/
protected byte readByte()
{
return _buffer.readByte();
}
/**
* Reads a single byte as an unsigned int (0255).
* @return the unsigned byte value
*/
protected int readUnsignedByte()
{
return Byte.toUnsignedInt(_buffer.readByte());
}
/**
* Reads a byte and returns {@code true} if it is non-zero.
* @return {@code true} if the byte is non-zero
*/
protected boolean readBoolean()
{
return _buffer.readByte() != 0;
}
/**
* Reads {@code length} bytes into a new array.
* @param length the number of bytes to read
* @return the byte array
*/
protected byte[] readBytes(int length)
{
return _buffer.readBytes(length);
}
/**
* Reads {@code dst.length} bytes into {@code dst}.
* @param dst the destination array
*/
protected void readBytes(byte[] dst)
{
_buffer.readBytes(dst, 0, dst.length);
}
/**
* Reads {@code length} bytes into {@code dst} starting at {@code offset}.
* @param dst the destination array
* @param offset the starting offset within {@code dst}
* @param length the number of bytes to read
*/
protected void readBytes(byte[] dst, int offset, int length)
{
_buffer.readBytes(dst, offset, length);
}
/**
* Reads a little-endian {@code short} (2 bytes).
* @return the short value
*/
protected short readShort()
{
return _buffer.readShort();
}
/**
* Reads a little-endian {@code int} (4 bytes).
* @return the int value
*/
protected int readInt()
{
return _buffer.readInt();
}
/**
* Reads a little-endian {@code long} (8 bytes).
* @return the long value
*/
protected long readLong()
{
return _buffer.readLong();
}
/**
* Reads a little-endian {@code float} (4 bytes).
* @return the float value
*/
protected float readFloat()
{
return _buffer.readFloat();
}
/**
* Reads a little-endian {@code double} (8 bytes).
* @return the double value
*/
protected double readDouble()
{
return _buffer.readDouble();
}
/**
* Reads a null-terminated UTF-16LE string from the buffer.<br>
* Reads {@code short} values until a zero short is encountered.
* @return the decoded string (never {@code null})
*/
protected String readString()
{
final StringBuilder result = new StringBuilder();
try
{
int charId;
while ((charId = readShort()) != 0)
{
result.append((char) charId);
}
}
catch (Exception ignored)
{
}
return result.toString();
}
/**
* Reads a length-prefixed UTF-16LE string.<br>
* The first {@code short} is the character count; the following bytes are the UTF-16LE data.
* @return the decoded string (never {@code null})
*/
protected String readSizedString()
{
try
{
final int charCount = readShort();
if (charCount > 0)
{
return new String(readBytes(charCount * 2), StandardCharsets.UTF_16LE);
}
}
catch (Exception ignored)
{
}
return "";
}
/**
* Returns the number of unread bytes remaining in the buffer.
* @return remaining byte count
*/
protected int remaining()
{
return _buffer.remaining();
}
/**
* Returns the client that sent this packet.
* @return the client instance
*/
public T getClient()
{
return _client;
}
/**
* Parses the packet's fields from the bound buffer.<br>
* Called on the I/O thread; must not block or perform heavy work.
* @return {@code true} if parsing succeeded and the packet should be executed; {@code false} to discard
*/
public abstract boolean read();
}
@@ -0,0 +1,218 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.packet;
import java.nio.charset.StandardCharsets;
/**
* Represents a simple class for packets that can be read from client data.<br>
* Provides methods to read various data types from the packet's byte array.
* @author Mobius
* @since October 29th 2020
*/
public class SimpleReadablePacket
{
private final byte[] _bytes;
private int _position = 0;
public SimpleReadablePacket(byte[] bytes)
{
_bytes = bytes;
}
/**
* Reads a boolean value from the packet data.<br>
* 8-bit integer (00 or 01).
* @return The boolean value read from the packet.
*/
public boolean readBoolean()
{
return readByte() != 0;
}
/**
* Reads <b>String</b> from the packet data.
* @return
*/
public String readString()
{
final StringBuilder result = new StringBuilder();
try
{
int charId;
while ((charId = readShort()) != 0)
{
result.append((char) charId);
}
}
catch (Exception ignored)
{
}
return result.toString();
}
/**
* Reads <b>String</b> with fixed size specified as (short size, char[size]) from the packet data.
* @return
*/
public String readSizedString()
{
String result = "";
try
{
result = new String(readBytes(readShort() * 2), StandardCharsets.UTF_16LE);
}
catch (Exception ignored)
{
}
return result;
}
/**
* Reads <b>byte[]</b> from the packet data.<br>
* 8bit integer array (00...)
* @param length of the array.
* @return
*/
public byte[] readBytes(int length)
{
// Validate before allocating, so a forged length cannot exhaust the heap.
if ((length < 0) || (length > (_bytes.length - _position)))
{
throw new ArrayIndexOutOfBoundsException();
}
final byte[] result = new byte[length];
for (int i = 0; i < length; i++)
{
result[i] = _bytes[_position++];
}
return result;
}
/**
* Reads <b>byte[]</b> from the packet data.<br>
* 8bit integer array (00...)
* @param array used to store data.
* @return
*/
public byte[] readBytes(byte[] array)
{
for (int i = 0; i < array.length; i++)
{
array[i] = _bytes[_position++];
}
return array;
}
/**
* Reads <b>byte</b> from the packet data.<br>
* 8bit integer (00)
* @return
*/
public int readByte()
{
return _bytes[_position++] & 0xff;
}
/**
* Reads <b>short</b> from the packet data.<br>
* 16bit integer (00 00)
* @return
*/
public int readShort()
{
return (_bytes[_position++] & 0xff) //
| ((_bytes[_position++] & 0xff) << 8);
}
/**
* Reads <b>int</b> from the packet data.<br>
* 32bit integer (00 00 00 00)
* @return
*/
public int readInt()
{
return (_bytes[_position++] & 0xff) //
| ((_bytes[_position++] & 0xff) << 8) //
| ((_bytes[_position++] & 0xff) << 16) //
| ((_bytes[_position++] & 0xff) << 24);
}
/**
* Reads <b>long</b> from the packet data.<br>
* 64bit integer (00 00 00 00 00 00 00 00)
* @return
*/
public long readLong()
{
return (_bytes[_position++] & 0xff) //
| ((_bytes[_position++] & 0xffL) << 8) //
| ((_bytes[_position++] & 0xffL) << 16) //
| ((_bytes[_position++] & 0xffL) << 24) //
| ((_bytes[_position++] & 0xffL) << 32) //
| ((_bytes[_position++] & 0xffL) << 40) //
| ((_bytes[_position++] & 0xffL) << 48) //
| ((_bytes[_position++] & 0xffL) << 56);
}
/**
* Reads <b>float</b> from the packet data.<br>
* 32bit single precision float (00 00 00 00)
* @return
*/
public float readFloat()
{
return Float.intBitsToFloat(readInt());
}
/**
* Reads <b>double</b> from the packet data.<br>
* 64bit double precision float (00 00 00 00 00 00 00 00)
* @return
*/
public double readDouble()
{
return Double.longBitsToDouble(readLong());
}
/**
* Gets the number of bytes remaining to be read.
* @return The number of unread bytes.
*/
public int getRemainingLength()
{
return _bytes.length - _position;
}
/**
* Gets the total length of the byte array.
* @return The total byte size.
*/
public int getLength()
{
return _bytes.length;
}
}
@@ -0,0 +1,289 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.packet;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Abstract class for writable packets backed by a byte array, with a maximum raw data size of 65533 bytes.<br>
* Provides methods to write various types of data to the packet.
* @author Mobius
* @since October 29th 2020
*/
public abstract class SimpleWritablePacket
{
private static final Map<Class<?>, Integer> MAXIMUM_PACKET_SIZE = new ConcurrentHashMap<>();
private final int _initialSize = MAXIMUM_PACKET_SIZE.getOrDefault(getClass(), 8);
private byte[] _data;
private byte[] _sendableBytes;
private int _position = 2; // Allocate space for size (max length 65535 - size header).
protected SimpleWritablePacket()
{
_data = new byte[_initialSize];
}
public void write(byte value)
{
// Check current size.
if (_position < 65535)
{
// Check capacity.
if (_position == _data.length)
{
_data = Arrays.copyOf(_data, _data.length * 2); // Double the capacity.
}
// Set value.
_data[_position++] = value;
return;
}
throw new IndexOutOfBoundsException("Packet data exceeded the raw data size limit of 65533!");
}
/**
* Write <b>boolean</b> to the packet data.<br>
* 8bit integer (00) or (01)
* @param value
*/
public void writeBoolean(boolean value)
{
writeByte(value ? 1 : 0);
}
/**
* Write <b>String</b> to the packet data.
* @param text
*/
public void writeString(String text)
{
if (text != null)
{
writeBytes(text.getBytes(StandardCharsets.UTF_16LE));
}
writeShort(0);
}
/**
* Write <b>String</b> with fixed size specified as (short size, char[size]) to the packet data.
* @param text
*/
public void writeSizedString(String text)
{
if (text != null)
{
writeShort(text.length());
writeBytes(text.getBytes(StandardCharsets.UTF_16LE));
}
else
{
writeShort(0);
}
}
/**
* Write <b>byte[]</b> to the packet data.<br>
* 8bit integer array (00...)
* @param array
*/
public void writeBytes(byte[] array)
{
for (int i = 0; i < array.length; i++)
{
write(array[i]);
}
}
/**
* Write <b>byte</b> to the packet data.<br>
* 8bit integer (00)
* @param value
*/
public void writeByte(int value)
{
write((byte) (value & 0xff));
}
/**
* Write <b>boolean</b> to the packet data.<br>
* 8bit integer (00) or (01)
* @param value
*/
public void writeByte(boolean value)
{
writeByte(value ? 1 : 0);
}
/**
* Write <b>short</b> to the packet data.<br>
* 16bit integer (00 00)
* @param value
*/
public void writeShort(int value)
{
write((byte) (value & 0xff));
write((byte) ((value >> 8) & 0xff));
}
/**
* Write <b>boolean</b> to the packet data.<br>
* 16bit integer (00 00)
* @param value
*/
public void writeShort(boolean value)
{
writeShort(value ? 1 : 0);
}
/**
* Write <b>int</b> to the packet data.<br>
* 32bit integer (00 00 00 00)
* @param value
*/
public void writeInt(int value)
{
write((byte) (value & 0xff));
write((byte) ((value >> 8) & 0xff));
write((byte) ((value >> 16) & 0xff));
write((byte) ((value >> 24) & 0xff));
}
/**
* Write <b>boolean</b> to the packet data.<br>
* 32bit integer (00 00 00 00)
* @param value
*/
public void writeInt(boolean value)
{
writeInt(value ? 1 : 0);
}
/**
* Write <b>long</b> to the packet data.<br>
* 64bit integer (00 00 00 00 00 00 00 00)
* @param value
*/
public void writeLong(long value)
{
write((byte) (value & 0xff));
write((byte) ((value >> 8) & 0xff));
write((byte) ((value >> 16) & 0xff));
write((byte) ((value >> 24) & 0xff));
write((byte) ((value >> 32) & 0xff));
write((byte) ((value >> 40) & 0xff));
write((byte) ((value >> 48) & 0xff));
write((byte) ((value >> 56) & 0xff));
}
/**
* Write <b>boolean</b> to the packet data.<br>
* 64bit integer (00 00 00 00 00 00 00 00)
* @param value
*/
public void writeLong(boolean value)
{
writeLong(value ? 1 : 0);
}
/**
* Write <b>float</b> to the packet data.<br>
* 32bit single precision float (00 00 00 00)
* @param value
*/
public void writeFloat(float value)
{
writeInt(Float.floatToRawIntBits(value));
}
/**
* Write <b>double</b> to the packet data.<br>
* 64bit double precision float (00 00 00 00 00 00 00 00)
* @param value
*/
public void writeDouble(double value)
{
writeLong(Double.doubleToRawLongBits(value));
}
/**
* Can be overridden to write data after packet has initialized.<br>
* Called when getSendableBytes generates data, ensures that the data are processed only once.
*/
public void write()
{
// Overridden by server implementation.
}
/**
* Returns the byte array containing the packet's data, including the size header.<br>
* This method should be called after all data has been written to the packet.
* @return Byte array of the sendable packet data.
*/
public synchronized byte[] getSendableBytes()
{
// Generate sendable byte array.
if (_sendableBytes == null /* Not processed */)
{
// Write packet implementation (only once).
if (_position == 2)
{
write();
// Update maximum packet size if needed.
if (_position > _initialSize)
{
MAXIMUM_PACKET_SIZE.put(getClass(), Math.min(_position, 65535));
}
}
// Check if data was written.
if (_position > 2)
{
// Trim array of data.
_sendableBytes = Arrays.copyOf(_data, _position);
// Add size info at start (unsigned short - max size 65535).
_sendableBytes[0] = (byte) (_position & 0xff);
_sendableBytes[1] = (byte) ((_position >> 8) & 0xffff);
}
}
// Return the data.
return _sendableBytes;
}
/**
* Gets the length of the data written to the packet, including the size header.<br>
* Note that the data must be written first before calling this method.
* @return The length of the data.
*/
public int getLength()
{
return _position;
}
}
@@ -0,0 +1,153 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.packet;
import org.l2jmobius.commons.network.Client;
import org.l2jmobius.commons.network.Connection;
import org.l2jmobius.commons.network.ConnectionConfig;
import org.l2jmobius.commons.network.buffer.WriteBuffer;
/**
* Base class for all packets sent to clients.<br>
* All packets carry a 2-byte little-endian header that encodes the total packet length.<br>
* <br>
* <b>Broadcast optimisation:</b> when a packet will be sent to multiple clients, call {@link #sendInBroadcast()} before the first {@link Client#writePacket} call.<br>
* The packet data is then written once into a {@link WriteBuffer}, a heap {@code byte[]} snapshot is cached on this packet instance, and every subsequent recipient gets a fresh {@link WriteBuffer} seeded from that snapshot. Each client still encrypts its own copy independently.
* @param <T> the client type associated with this packet
* @author Mobius
*/
public abstract class WritablePacket<T extends Client<Connection<T>>>
{
private volatile boolean _broadcast;
/**
* Cached snapshot of the serialized packet bytes for broadcast reuse.<br>
* Populated on the first send when {@link #_broadcast} is {@code true}; every later send bulk-copies these bytes into a fresh per-client {@link WriteBuffer}. Guarded by {@code this} (synchronized on the WritablePacket instance).
*/
private byte[] _broadcastCacheBytes;
private int _broadcastCacheLength;
protected WritablePacket()
{
}
/**
* Produces the buffer containing this packet's encoded data for the given client.<br>
* For broadcast packets the first call populates a shared byte-array cache; subsequent calls seed a fresh per-client {@link WriteBuffer} from that cache so each client can encrypt independently.
* @param client the recipient client
* @return a {@link WriteBuffer} whose logical limit is the packet length
* @throws Exception if the {@link #write} implementation signals a failure
*/
public WriteBuffer writeData(T client) throws Exception
{
if (_broadcast)
{
return writeDataWithCache(client);
}
return writeDataToBuffer(client);
}
/**
* Broadcast path: returns a fresh per-client buffer seeded from the shared cache, building the cache on the first call.
* @param client the recipient client
* @return per-client buffer populated with the cached bytes
* @throws Exception if the underlying write fails on first cache population
*/
private synchronized WriteBuffer writeDataWithCache(T client) throws Exception
{
if (_broadcastCacheBytes != null)
{
return new WriteBuffer(_broadcastCacheBytes, _broadcastCacheLength, client.getResourcePool(), getClass());
}
final WriteBuffer buffer = writeDataToBuffer(client);
_broadcastCacheBytes = buffer.toByteArray();
_broadcastCacheLength = buffer.limit();
return buffer;
}
/**
* Writes packet data into a fresh {@link WriteBuffer} sized by the per-class historical maximum.
* @param client the recipient client
* @return buffer containing the written packet data, positioned at 0 with limit at packet end
* @throws Exception if {@link #write} returns {@code false}
*/
private WriteBuffer writeDataToBuffer(T client) throws Exception
{
final WriteBuffer buffer = new WriteBuffer(client.getResourcePool(), getClass());
buffer.position(ConnectionConfig.HEADER_SIZE);
if (write(client, buffer))
{
buffer.mark();
return buffer;
}
buffer.releaseResources();
throw new Exception("WritablePacket.write() returned false for " + getClass().getSimpleName());
}
/**
* Writes the 2-byte packet-length header at offset 0 of {@code buffer}.
* @param buffer the buffer whose first 2 bytes receive the total length
* @param header the total packet length (including the 2-byte header itself)
*/
public void writeHeader(WriteBuffer buffer, int header)
{
buffer.writeShort(0, (short) header);
}
/**
* Marks this packet as a broadcast packet.<br>
* Must be called <em>before</em> the first {@link Client#writePacket} invocation.<br>
* After this call, packet data is written once and the resulting bytes are cached on this instance for reuse across all recipients.
*/
public void sendInBroadcast()
{
_broadcast = true;
}
/**
* Returns whether this packet may be silently discarded when the client's send queue exceeds the configured drop threshold.<br>
* The default implementation always returns {@code false} (never drop).
* @param client the recipient client
* @return {@code true} if the packet is expendable and may be dropped under pressure
*/
public boolean canBeDropped(T client)
{
return false;
}
/**
* Writes the packet's payload into {@code buffer}.<br>
* The buffer's position is already set to {@link ConnectionConfig#HEADER_SIZE} on entry; the implementation should write all payload bytes and return {@code true} on success.
* @param client the recipient client (available for per-client customisation)
* @param buffer the buffer to write payload data into
* @return {@code true} if the packet was written successfully; {@code false} to cancel sending
*/
protected abstract boolean write(T client, WriteBuffer buffer);
@Override
public String toString()
{
return getClass().getSimpleName();
}
}
@@ -0,0 +1,218 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.pool;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.concurrent.locks.ReentrantLock;
/**
* Represents a pool of {@link ByteBuffer} objects for efficient reuse, reducing allocation overhead.<br>
* Uses an {@link ArrayDeque} backed by a {@link ReentrantLock} for correct, thread-safe pool management without the estimate-drift issues of a lock-free approach.
* @author Mobius
*/
public class BufferPool
{
private final ArrayDeque<ByteBuffer> _buffers;
private final ReentrantLock _lock = new ReentrantLock();
private final int _bufferSize;
private int _maxSize;
/**
* Creates a buffer pool with the specified capacity and buffer size.
* @param maxSize the maximum number of pooled buffers
* @param bufferSize the capacity (in bytes) of each pooled buffer
*/
public BufferPool(int maxSize, int bufferSize)
{
_maxSize = maxSize;
_bufferSize = bufferSize;
_buffers = new ArrayDeque<>(maxSize);
}
/**
* Pre-allocates buffers into the pool based on the given factor.<br>
* The number pre-allocated is {@code min(maxSize, maxSize * factor)}.
* @param factor the fraction of the pool to pre-fill (0 = none, 1.0 = full)
*/
public void initialize(float factor)
{
final int amount = (int) Math.min(_maxSize, _maxSize * factor);
_lock.lock();
try
{
for (int i = 0; i < amount; i++)
{
_buffers.offer(ByteBuffer.allocateDirect(_bufferSize).order(ByteOrder.LITTLE_ENDIAN));
}
}
finally
{
_lock.unlock();
}
}
/**
* Retrieves a pooled {@link ByteBuffer}, or {@code null} if the pool is empty.
* @return a cleared buffer ready for use, or {@code null}
*/
public ByteBuffer get()
{
_lock.lock();
try
{
return _buffers.poll();
}
finally
{
_lock.unlock();
}
}
/**
* Returns a buffer to the pool if capacity allows.<br>
* The buffer is cleared before being stored.
* @param buffer the buffer to recycle
* @return {@code true} if the buffer was accepted; {@code false} if the pool was full
*/
public boolean recycle(ByteBuffer buffer)
{
_lock.lock();
try
{
if (_buffers.size() < _maxSize)
{
_buffers.offer(buffer.clear());
return true;
}
return false;
}
finally
{
_lock.unlock();
}
}
/**
* Expands the pool's maximum capacity and optionally allocates additional buffers.<br>
* If {@code factor > 0}, new buffers are allocated immediately and the max size is increased.<br>
* Otherwise, the max size is simply doubled to allow future recycling.
* @param factor allocation factor; if {@code > 0}, new buffers are created immediately
* @param limit the pool only expands when its current max size does not exceed this value
*/
public void expandCapacity(float factor, int limit)
{
_lock.lock();
try
{
if (_maxSize > limit)
{
return;
}
if (factor > 0)
{
final int amount = (int) (_maxSize * factor);
for (int i = 0; i < amount; i++)
{
_buffers.offer(ByteBuffer.allocateDirect(_bufferSize).order(ByteOrder.LITTLE_ENDIAN));
}
_maxSize += amount;
}
else
{
_maxSize *= 2;
}
}
finally
{
_lock.unlock();
}
}
/**
* Returns the configured maximum capacity of this pool.
* @return the maximum number of buffers that may be held
*/
public int getMaxSize()
{
_lock.lock();
try
{
return _maxSize;
}
finally
{
_lock.unlock();
}
}
/**
* Returns {@code true} when the pool holds at least as many buffers as its current max size.
* @return {@code true} if the pool is at capacity
*/
public boolean isFull()
{
_lock.lock();
try
{
return _buffers.size() >= _maxSize;
}
finally
{
_lock.unlock();
}
}
/**
* Returns {@code true} when the pool holds no buffers.
* @return {@code true} if the pool is empty
*/
public boolean isEmpty()
{
_lock.lock();
try
{
return _buffers.isEmpty();
}
finally
{
_lock.unlock();
}
}
@Override
public String toString()
{
_lock.lock();
try
{
return "Pool {maxSize=" + _maxSize + ", bufferSize=" + _bufferSize + ", currentSize=" + _buffers.size() + '}';
}
finally
{
_lock.unlock();
}
}
}
@@ -0,0 +1,264 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.network.pool;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import org.l2jmobius.commons.network.ConnectionConfig;
/**
* Manages multiple {@link BufferPool}s indexed by buffer size for efficient {@link ByteBuffer} reuse.<br>
* <br>
* Two concurrent data structures are maintained in parallel:
* <ul>
* <li>A {@link ConcurrentSkipListMap} keyed by size for {@code ceiling} lookups (finding the smallest pool whose buffer capacity is &ge; the requested size).</li>
* <li>A {@link ConcurrentHashMap} keyed by exact capacity for O(1) recycle path (returning a buffer whose capacity exactly matches a known pool).</li>
* </ul>
* Both structures are written together in {@link #addBufferPool}, so they are always consistent.
* @author JoeAlisson, Mobius
*/
public class ResourcePool
{
/** Ordered map for ceiling-entry lookups (find smallest pool >= requested size). */
private final ConcurrentSkipListMap<Integer, BufferPool> _sizedPools = new ConcurrentSkipListMap<>();
/** Fast O(1) exact-capacity lookup used by the recycle path. */
private final ConcurrentHashMap<Integer, BufferPool> _exactPools = new ConcurrentHashMap<>();
private volatile boolean _autoExpandCapacity = true;
private volatile boolean _initBufferPools = false;
private volatile float _initBufferPoolFactor = 0;
private volatile int _bufferSegmentSize = 64;
public ResourcePool()
{
}
/**
* Returns a {@link ByteBuffer} sized for the packet header (2 bytes).
* @return a header-sized buffer
*/
public ByteBuffer getHeaderBuffer()
{
return getSizedBuffer(ConnectionConfig.HEADER_SIZE);
}
/**
* Returns a {@link ByteBuffer} with at least {@code size} bytes of capacity.
* @param size the minimum required capacity
* @return a buffer with capacity &ge; {@code size}
*/
public ByteBuffer getBuffer(int size)
{
return getSizedBuffer(determineBufferSize(size));
}
/**
* Recycles {@code buffer} (if non-null) and returns a new buffer of {@code newSize}.<br>
* If {@code buffer} already has the right capacity, it is cleared and reused directly.
* @param buffer the buffer to recycle, or {@code null}
* @param newSize the required capacity of the returned buffer
* @return a buffer with capacity exactly matching the pool size for {@code newSize}
*/
public ByteBuffer recycleAndGetNew(ByteBuffer buffer, int newSize)
{
final int poolSize = determineBufferSize(newSize);
if (buffer != null)
{
if (buffer.clear().limit() == poolSize)
{
return buffer.limit(newSize);
}
recycleBuffer(buffer);
}
return getSizedBuffer(poolSize).limit(newSize);
}
/**
* Returns a buffer of exactly {@code size} bytes from the appropriate pool, expanding or creating pools as needed.
* @param size the exact capacity to serve (must be a pool key)
* @return a direct {@link ByteBuffer} with the requested capacity
*/
private ByteBuffer getSizedBuffer(int size)
{
final Entry<Integer, BufferPool> entry = _sizedPools.ceilingEntry(size);
if (entry != null)
{
final BufferPool pool = entry.getValue();
if (_autoExpandCapacity)
{
if (_initBufferPools)
{
if (pool.isEmpty())
{
pool.expandCapacity(_initBufferPoolFactor, pool.getMaxSize());
}
}
else if (pool.isFull())
{
pool.expandCapacity(_initBufferPoolFactor, pool.getMaxSize());
}
}
final ByteBuffer buffer = pool.get();
if (buffer != null)
{
return buffer;
}
}
// No pool or pool was empty and no expansion produced a buffer - allocate directly.
if (entry == null)
{
// Unknown size: register a new pool so future requests are served from the pool.
final BufferPool pool = new BufferPool(10, size);
if (_initBufferPools)
{
pool.initialize(_initBufferPoolFactor);
}
_sizedPools.putIfAbsent(size, pool);
_exactPools.putIfAbsent(size, _sizedPools.get(size));
}
return ByteBuffer.allocateDirect(size).order(ByteOrder.LITTLE_ENDIAN);
}
/**
* Returns the pool key (capacity) to use for the given {@code size}.
* @param size the minimum required capacity
* @return the capacity of the smallest pool that can satisfy {@code size}
*/
private int determineBufferSize(int size)
{
final Entry<Integer, BufferPool> entry = _sizedPools.ceilingEntry(size);
if (entry != null)
{
return entry.getKey();
}
// Unknown size - register a new pool.
final BufferPool pool = new BufferPool(10, size);
if (_initBufferPools)
{
pool.initialize(_initBufferPoolFactor);
}
_sizedPools.putIfAbsent(size, pool);
_exactPools.putIfAbsent(size, _sizedPools.get(size));
return size;
}
/**
* Returns {@code buffer} to its pool using an O(1) exact-capacity lookup.<br>
* Silently ignores {@code null} or buffers whose capacity has no matching pool.
* @param buffer the buffer to recycle
*/
public void recycleBuffer(ByteBuffer buffer)
{
if (buffer == null)
{
return;
}
final BufferPool pool = _exactPools.get(buffer.capacity());
if (pool != null)
{
pool.recycle(buffer);
}
}
/**
* Registers a {@link BufferPool} for the given buffer size.<br>
* If a pool for that size already exists it is not replaced.
* @param bufferSize the capacity (in bytes) of buffers managed by the pool
* @param bufferPool the pool to register
*/
public void addBufferPool(int bufferSize, BufferPool bufferPool)
{
_sizedPools.putIfAbsent(bufferSize, bufferPool);
_exactPools.putIfAbsent(bufferSize, bufferPool);
}
/**
* Returns the number of registered buffer pools.
* @return pool count
*/
public int bufferPoolSize()
{
return _sizedPools.size();
}
/**
* Finalises pool configuration and optionally pre-allocates buffers.
* @param autoExpandCapacity whether pools should grow automatically when exhausted
* @param initBufferPoolFactor fraction of each pool to pre-allocate (0 = none)
*/
public void initializeBuffers(boolean autoExpandCapacity, float initBufferPoolFactor)
{
_autoExpandCapacity = autoExpandCapacity;
_initBufferPoolFactor = initBufferPoolFactor;
_initBufferPools = initBufferPoolFactor > 0;
if (_initBufferPools)
{
_sizedPools.values().forEach(pool -> pool.initialize(initBufferPoolFactor));
}
}
/**
* Returns the configured buffer segment size (default 64).
* @return segment size in bytes
*/
public int getSegmentSize()
{
return _bufferSegmentSize;
}
/**
* Sets the buffer segment size used as the default initial size for dynamic packet buffers.
* @param size the new segment size in bytes
*/
public void setBufferSegmentSize(int size)
{
_bufferSegmentSize = size;
}
/**
* Returns a diagnostic string listing all registered pools.
* @return multi-line pool statistics
*/
public String stats()
{
final StringBuilder sb = new StringBuilder();
for (BufferPool pool : _sizedPools.values())
{
sb.append(pool.toString()).append(System.lineSeparator());
}
return sb.toString();
}
}
@@ -0,0 +1,302 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.threads;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import org.l2jmobius.commons.config.ThreadConfig;
import org.l2jmobius.commons.util.StringUtil;
import org.l2jmobius.commons.util.TraceUtil;
/**
* Provides methods to schedule tasks with delays, fixed rates and immediate execution.<br>
* Manages multiple thread pools including scheduled tasks, instant execution and high priority scheduling.
* <ul>
* <li>Scheduled thread pool for delayed and recurring tasks.</li>
* <li>Instant thread pool for immediate task execution.</li>
* <li>High priority scheduled thread pool for critical tasks.</li>
* <li>Automatic task purging and cleanup mechanisms.</li>
* </ul>
* @author Mobius
*/
public class ThreadPool
{
private static final Logger LOGGER = Logger.getLogger(ThreadPool.class.getName());
// Constants.
private static final long ONE_HUNDRED_YEARS_MS = 3155695200000L;
private static final long MIN_DELAY_MS = 0L;
private static final long PURGE_INTERVAL_MS = 60000L;
private static final int INSTANT_POOL_KEEP_ALIVE_MINUTES = 1;
// Thread Pool Executors.
private static ScheduledThreadPoolExecutor HIGH_PRIORITY_SCHEDULED_POOL;
private static ScheduledThreadPoolExecutor SCHEDULED_POOL;
private static ThreadPoolExecutor INSTANT_POOL;
/**
* Initializes thread pool executors and starts maintenance tasks.
*/
public static void init()
{
LOGGER.info("ThreadPool: Initializing.");
// Load configurations.
ThreadConfig.load();
// Configure High Priority ScheduledThreadPoolExecutor.
if (ThreadConfig.HIGH_PRIORITY_SCHEDULED_THREAD_POOL_SIZE > 0)
{
HIGH_PRIORITY_SCHEDULED_POOL = new ScheduledThreadPoolExecutor(ThreadConfig.HIGH_PRIORITY_SCHEDULED_THREAD_POOL_SIZE, new ThreadProvider("L2jMobius High Priority ScheduledThread", ThreadPriority.PRIORITY_8), new ThreadPoolExecutor.CallerRunsPolicy());
LOGGER.info(StringUtil.concat("...scheduled pool executor with ", String.valueOf(ThreadConfig.HIGH_PRIORITY_SCHEDULED_THREAD_POOL_SIZE), " high priority threads."));
}
// Configure ScheduledThreadPoolExecutor.
SCHEDULED_POOL = new ScheduledThreadPoolExecutor(ThreadConfig.SCHEDULED_THREAD_POOL_SIZE, new ThreadProvider("L2jMobius ScheduledThread"), new ThreadPoolExecutor.CallerRunsPolicy());
SCHEDULED_POOL.setRejectedExecutionHandler(new RejectedExecutionHandlerImpl());
SCHEDULED_POOL.setRemoveOnCancelPolicy(true);
SCHEDULED_POOL.prestartAllCoreThreads();
// Configure ThreadPoolExecutor.
INSTANT_POOL = new ThreadPoolExecutor(ThreadConfig.INSTANT_THREAD_POOL_SIZE, Integer.MAX_VALUE, INSTANT_POOL_KEEP_ALIVE_MINUTES, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new ThreadProvider("L2jMobius Thread"));
INSTANT_POOL.setRejectedExecutionHandler(new RejectedExecutionHandlerImpl());
INSTANT_POOL.prestartAllCoreThreads();
// Schedule the purge task.
scheduleAtFixedRate(ThreadPool::purge, PURGE_INTERVAL_MS, PURGE_INTERVAL_MS);
// Log thread pool configuration.
LOGGER.info(StringUtil.concat("...scheduled pool executor with ", String.valueOf(ThreadConfig.SCHEDULED_THREAD_POOL_SIZE), " total threads."));
LOGGER.info(StringUtil.concat("...instant pool executor with ", String.valueOf(ThreadConfig.INSTANT_THREAD_POOL_SIZE), " total threads."));
}
/**
* Purges cancelled tasks from all thread pools to free memory.
*/
public static void purge()
{
SCHEDULED_POOL.purge();
INSTANT_POOL.purge();
if (HIGH_PRIORITY_SCHEDULED_POOL != null)
{
HIGH_PRIORITY_SCHEDULED_POOL.purge();
}
}
/**
* Creates and executes a one-shot action that becomes enabled after the given delay.
* @param runnable the task to execute
* @param delay the time from now to delay execution
* @return a ScheduledFuture representing pending completion of the task and whose get() method will return null upon completion
*/
public static ScheduledFuture<?> schedule(Runnable runnable, long delay)
{
try
{
return SCHEDULED_POOL.schedule(new RunnableWrapper(runnable), validateDelay(delay), TimeUnit.MILLISECONDS);
}
catch (Exception e)
{
LOGGER.warning(StringUtil.concat("ThreadPool: Failed to schedule task ", runnable.getClass().getSimpleName(), " with delay ", String.valueOf(delay), "ms: ", e.getMessage(), System.lineSeparator(), TraceUtil.getStackTrace(e)));
return null;
}
}
/**
* Creates and executes a periodic action that becomes enabled first after the given initial delay.
* @param runnable the task to execute
* @param initialDelay the time to delay first execution
* @param period the period between successive executions
* @return a ScheduledFuture representing pending completion of the task and whose get() method will throw an exception upon cancellation
*/
public static ScheduledFuture<?> scheduleAtFixedRate(Runnable runnable, long initialDelay, long period)
{
try
{
return SCHEDULED_POOL.scheduleAtFixedRate(new RunnableWrapper(runnable), validateDelay(initialDelay), validateDelay(period), TimeUnit.MILLISECONDS);
}
catch (Exception e)
{
LOGGER.warning(StringUtil.concat("ThreadPool: Failed to schedule recurring task ", runnable.getClass().getSimpleName(), " with initial delay ", String.valueOf(initialDelay), "ms and period ", String.valueOf(period), "ms: ", e.getMessage(), System.lineSeparator(), TraceUtil.getStackTrace(e)));
return null;
}
}
/**
* Creates and executes a periodic action using high priority thread pool.<br>
* Designed for tasks requiring immediate or high-priority execution.
* @param runnable the task to execute
* @param initialDelay the time to delay first execution
* @param period the period between successive executions
* @return a ScheduledFuture representing pending completion of the task and whose get() method will throw an exception upon cancellation
*/
public static ScheduledFuture<?> schedulePriorityTaskAtFixedRate(Runnable runnable, long initialDelay, long period)
{
if (HIGH_PRIORITY_SCHEDULED_POOL == null)
{
return scheduleAtFixedRate(runnable, initialDelay, period);
}
try
{
return HIGH_PRIORITY_SCHEDULED_POOL.scheduleAtFixedRate(new RunnableWrapper(runnable), validateDelay(initialDelay), validateDelay(period), TimeUnit.MILLISECONDS);
}
catch (Exception e)
{
LOGGER.warning(StringUtil.concat("ThreadPool: Failed to schedule high priority task ", runnable.getClass().getSimpleName(), " with initial delay ", String.valueOf(initialDelay), "ms and period ", String.valueOf(period), "ms: ", e.getMessage(), System.lineSeparator(), TraceUtil.getStackTrace(e)));
return null;
}
}
/**
* Executes the given task sometime in the future using the instant thread pool.
* @param runnable the task to execute
*/
public static void execute(Runnable runnable)
{
try
{
INSTANT_POOL.execute(new RunnableWrapper(runnable));
}
catch (Exception e)
{
LOGGER.warning(StringUtil.concat("ThreadPool: Failed to execute task ", runnable.getClass().getSimpleName(), ": ", e.getMessage(), System.lineSeparator(), TraceUtil.getStackTrace(e)));
}
}
/**
* Validates delay value to ensure it falls within acceptable bounds.
* @param delay the delay to validate
* @return a valid delay value between MIN_DELAY_MS and ONE_HUNDRED_YEARS_MS
*/
private static long validateDelay(long delay)
{
if (delay < MIN_DELAY_MS)
{
LOGGER.warning(StringUtil.concat("ThreadPool: Invalid delay ", String.valueOf(delay), "ms is below minimum, using ", String.valueOf(MIN_DELAY_MS), "ms instead."));
LOGGER.warning(TraceUtil.getStackTrace(new Exception()));
return MIN_DELAY_MS;
}
if (delay > ONE_HUNDRED_YEARS_MS)
{
LOGGER.warning(StringUtil.concat("ThreadPool: Invalid delay ", String.valueOf(delay), "ms exceeds maximum, using ", String.valueOf(ONE_HUNDRED_YEARS_MS), "ms instead."));
LOGGER.warning(TraceUtil.getStackTrace(new Exception()));
return ONE_HUNDRED_YEARS_MS;
}
return delay;
}
/**
* Shutdown thread pooling system correctly.
*/
public static void shutdown()
{
try
{
LOGGER.info("ThreadPool: Shutting down all thread pools.");
SCHEDULED_POOL.shutdownNow();
INSTANT_POOL.shutdownNow();
if (HIGH_PRIORITY_SCHEDULED_POOL != null)
{
HIGH_PRIORITY_SCHEDULED_POOL.shutdownNow();
}
}
catch (Throwable t)
{
LOGGER.warning(StringUtil.concat("ThreadPool: Exception occurred during shutdown: ", t.getMessage()));
}
}
/**
* Handles tasks rejected by ThreadPoolExecutor by running them in new thread or current thread.<br>
* Decision based on current thread priority to prevent blocking high priority operations.
*/
private static class RejectedExecutionHandlerImpl implements RejectedExecutionHandler
{
private static final Logger LOGGER = Logger.getLogger(RejectedExecutionHandlerImpl.class.getName());
@Override
public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor)
{
if (executor.isShutdown())
{
return;
}
LOGGER.warning(StringUtil.concat("ThreadPool: Task ", runnable.getClass().getSimpleName(), " rejected by executor ", String.valueOf(executor), ", attempting recovery execution."));
// Run in new thread for high priority contexts, current thread otherwise.
if (Thread.currentThread().getPriority() > Thread.NORM_PRIORITY)
{
new Thread(runnable).start();
}
else
{
runnable.run();
}
}
}
/**
* Wraps a Runnable to handle uncaught exceptions during execution.<br>
* Passes exceptions to the thread's uncaught exception handler for proper error management.
*/
private static class RunnableWrapper implements Runnable
{
private final Runnable _wrappedRunnable;
/**
* Creates a new RunnableWrapper for the specified runnable.
* @param runnable the runnable to wrap
*/
public RunnableWrapper(Runnable runnable)
{
_wrappedRunnable = runnable;
}
@Override
public void run()
{
try
{
_wrappedRunnable.run();
}
catch (Throwable t)
{
final Thread currentThread = Thread.currentThread();
final UncaughtExceptionHandler exceptionHandler = currentThread.getUncaughtExceptionHandler();
if (exceptionHandler != null)
{
exceptionHandler.uncaughtException(currentThread, t);
}
}
}
}
}
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.threads;
/**
* Defines different levels of thread priorities.<br>
* This enum provides a convenient way to set thread priorities in a readable and maintainable manner.<br>
* The priorities range from 1 (lowest) to 10 (highest), aligning with Java's standard thread priority levels.
* @author Mobius
* @since December 8th, 2023
*/
public enum ThreadPriority
{
/**
* Priority level 1, equivalent to {@link Thread#MIN_PRIORITY}.
*/
PRIORITY_1(1),
/**
* Priority level 2.
*/
PRIORITY_2(2),
/**
* Priority level 3.
*/
PRIORITY_3(3),
/**
* Priority level 4.
*/
PRIORITY_4(4),
/**
* Priority level 5, equivalent to {@link Thread#NORM_PRIORITY}.
*/
PRIORITY_5(5),
/**
* Priority level 6.
*/
PRIORITY_6(6),
/**
* Priority level 7.
*/
PRIORITY_7(7),
/**
* Priority level 8.
*/
PRIORITY_8(8),
/**
* Priority level 9.
*/
PRIORITY_9(9),
/**
* Priority level 10, equivalent to {@link Thread#MAX_PRIORITY}.
*/
PRIORITY_10(10);
private final int _id;
/**
* Constructs a new {@code ThreadPriority} instance with the specified priority level.
* @param id the priority level, ranging from 1 to 10.
*/
ThreadPriority(int id)
{
_id = id;
}
/**
* Returns the numerical ID of the priority level.
* @return the priority level as an integer.
*/
public int getId()
{
return _id;
}
}
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.threads;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* ThreadFactory implementation that allows setting a thread name prefix, priority, and daemon status when creating new threads.
* @author Mobius
* @since October 18th 2022
*/
public class ThreadProvider implements ThreadFactory
{
private final AtomicInteger _id = new AtomicInteger();
private final String _prefix;
private final int _priority;
private final boolean _daemon;
/**
* Creates a new ThreadProvider with the specified prefix, normal thread priority, and non-daemon threads.
* @param prefix the prefix to be used for thread names
*/
public ThreadProvider(String prefix)
{
this(prefix, ThreadPriority.PRIORITY_5, false);
}
/**
* Creates a new ThreadProvider with the specified prefix and daemon status, and normal thread priority.
* @param prefix the prefix to be used for thread names
* @param daemon whether the threads should be daemon threads
*/
public ThreadProvider(String prefix, boolean daemon)
{
this(prefix, ThreadPriority.PRIORITY_5, daemon);
}
/**
* Creates a new ThreadProvider with the specified prefix and priority, and non-daemon threads.
* @param prefix the prefix to be used for thread names
* @param priority the priority of the threads
*/
public ThreadProvider(String prefix, ThreadPriority priority)
{
this(prefix, priority, false);
}
/**
* Creates a new ThreadProvider with the specified prefix, priority, and daemon status.
* @param prefix the prefix to be used for thread names
* @param priority the priority of the threads
* @param daemon whether the threads should be daemon threads
*/
public ThreadProvider(String prefix, ThreadPriority priority, boolean daemon)
{
_prefix = prefix + " ";
_priority = priority.getId();
_daemon = daemon;
}
/**
* Creates a new Thread with the specified Runnable object and with the properties defined in this ThreadProvider.
* @param runnable the object whose run method is invoked when this thread is started
* @return the created Thread
*/
@Override
public Thread newThread(Runnable runnable)
{
final Thread thread = new Thread(runnable, _prefix + _id.incrementAndGet());
thread.setPriority(_priority);
thread.setDaemon(_daemon);
return thread;
}
}
@@ -0,0 +1,744 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.time;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import org.l2jmobius.commons.util.Rnd;
import org.l2jmobius.commons.util.StringUtil;
/**
* A UNIX cron-like pattern parser for scheduling tasks.<br>
* Supports extended syntax with randomization and time offset modifiers for flexible task scheduling.
* <ul>
* <li>Standard cron format with 5 or 6 space-separated fields (minute, hour, day, month, weekday, optional week offset).</li>
* <li>Extended modifiers: ~N for random delays, +N for time offsets, L for last day of month.</li>
* <li>Multiple pattern support using pipe (|) separator for OR conditions.</li>
* <li>Month and weekday name aliases (jan-dec, sun-sat) for improved readability.</li>
* </ul>
* @author Mobius
*/
public class SchedulingPattern
{
// Constants.
private static final int MINUTE_MIN = 0;
private static final int MINUTE_MAX = 59;
private static final int HOUR_MIN = 0;
private static final int HOUR_MAX = 23;
private static final int DAY_MIN = 1;
private static final int DAY_MAX = 31;
private static final int MONTH_MIN = 1;
private static final int MONTH_MAX = 12;
private static final int DAY_OF_WEEK_MIN = 0; // 0 = Sunday
private static final int DAY_OF_WEEK_MAX = 6;
private static final int LAST_DAY_MARKER = 32; // Special marker for last day of month.
private static final int CALENDAR_MONTH_OFFSET = 1; // Calendar months are 0-based.
private static final int CALENDAR_DAY_OF_WEEK_OFFSET = 1; // Convert to 0 = Sunday.
private static final int SEARCH_LIMIT_YEARS = 4; // Maximum years to search for next match.
private static final int MINIMUM_CRON_FIELDS = 5;
private static final int MAXIMUM_CRON_FIELDS = 6;
private static final int CRON_PARTS_EXPECTED = 2;
private static final String PIPE_SEPARATOR = "\\|";
private static final String WHITESPACE_PATTERN = "\\s+";
private static final String FIELD_VALIDATION_REGEX = "^[0-9a-zA-Z*,\\-/:~+L]+$";
private static final String NO_FUTURE_MATCH_MESSAGE = "No future match.";
// Month aliases for improved readability.
private static final Map<String, Integer> MONTH_ALIASES = new HashMap<>();
static
{
MONTH_ALIASES.put("jan", 1);
MONTH_ALIASES.put("feb", 2);
MONTH_ALIASES.put("mar", 3);
MONTH_ALIASES.put("apr", 4);
MONTH_ALIASES.put("may", 5);
MONTH_ALIASES.put("jun", 6);
MONTH_ALIASES.put("jul", 7);
MONTH_ALIASES.put("aug", 8);
MONTH_ALIASES.put("sep", 9);
MONTH_ALIASES.put("oct", 10);
MONTH_ALIASES.put("nov", 11);
MONTH_ALIASES.put("dec", 12);
}
// Day of week aliases for improved readability.
private static final Map<String, Integer> DAY_ALIASES = new HashMap<>();
static
{
DAY_ALIASES.put("sun", 0);
DAY_ALIASES.put("mon", 1);
DAY_ALIASES.put("tue", 2);
DAY_ALIASES.put("wed", 3);
DAY_ALIASES.put("thu", 4);
DAY_ALIASES.put("fri", 5);
DAY_ALIASES.put("sat", 6);
}
// Pattern data.
private final String _originalPattern;
private final List<CronExpression> _cronExpressions;
/**
* Creates a new scheduling pattern from a cron-like string.
* @param pattern The cron pattern string
* @throws RuntimeException if the pattern is invalid.
*/
public SchedulingPattern(String pattern) throws RuntimeException
{
_originalPattern = Objects.requireNonNull(pattern, "Pattern cannot be null.");
try
{
_cronExpressions = parsePattern(pattern);
}
catch (Exception e)
{
throw new RuntimeException("Invalid scheduling pattern: " + pattern, e);
}
}
/**
* Validates whether a string is a valid scheduling pattern.
* @param schedulingPattern The pattern to validate
* @return true if valid, false otherwise
*/
public static boolean validate(String schedulingPattern)
{
if (schedulingPattern == null)
{
return false;
}
try
{
// Lightweight validation without full parsing.
final String[] orPatterns = schedulingPattern.split(PIPE_SEPARATOR);
for (String orPattern : orPatterns)
{
final String[] fields = orPattern.trim().split(WHITESPACE_PATTERN);
if ((fields.length < MINIMUM_CRON_FIELDS) || (fields.length > MAXIMUM_CRON_FIELDS))
{
return false;
}
// Basic syntax validation for each field.
if (!isValidField(fields[0]) || !isValidField(fields[1]) || !isValidField(fields[2]) || !isValidField(fields[3]) || !isValidField(fields[4]))
{
return false;
}
// Validate optional week offset field.
if (fields.length == MAXIMUM_CRON_FIELDS)
{
final String weekField = fields[5].trim();
if (!weekField.startsWith("+") || !StringUtil.isNumeric(weekField.substring(1)))
{
return false;
}
}
}
return true;
}
catch (Exception e)
{
return false;
}
}
/**
* Lightweight field validation for pattern syntax checking.
* @param field the field to validate
* @return true if field has valid basic syntax
*/
private static boolean isValidField(String field)
{
if ((field == null) || field.trim().isEmpty())
{
return false;
}
// Check for valid characters and basic syntax.
return field.matches(FIELD_VALIDATION_REGEX);
}
/**
* Checks if the given timestamp matches this pattern.
* @param timezone The timezone to use
* @param millis The timestamp in milliseconds
* @return true if the timestamp matches
*/
public boolean match(TimeZone timezone, long millis)
{
final Calendar calendar = Calendar.getInstance(timezone);
calendar.setTimeInMillis(millis);
// Normalize to minute precision.
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// Use traditional loop for better performance in hot paths.
for (CronExpression cronExpression : _cronExpressions)
{
if (cronExpression.matches(calendar))
{
return true;
}
}
return false;
}
/**
* Checks if the given timestamp matches this pattern using system timezone.
* @param millis The timestamp in milliseconds
* @return true if the timestamp matches
*/
public boolean match(long millis)
{
return match(TimeZone.getDefault(), millis);
}
/**
* Finds the next matching time after the given timestamp.
* @param timezone The timezone to use
* @param millis The timestamp to search after
* @return The next matching timestamp in milliseconds
*/
public long next(TimeZone timezone, long millis)
{
long earliestMatch = -1L;
// Use traditional loop for better performance in hot paths.
for (CronExpression cronExpression : _cronExpressions)
{
final long nextMatch = cronExpression.getNextMatch(millis, timezone);
if ((nextMatch > millis) && ((earliestMatch == -1L) || (nextMatch < earliestMatch)))
{
earliestMatch = nextMatch;
}
}
return earliestMatch;
}
/**
* Finds the next matching time after the given timestamp using system timezone.
* @param millis The timestamp to search after
* @return The next matching timestamp in milliseconds
*/
public long next(long millis)
{
return next(TimeZone.getDefault(), millis);
}
/**
* Gets delay from a specific time until next match.
* @param millis The base timestamp
* @return Delay in milliseconds until next match after millis
*/
public long nextFrom(long millis)
{
final long nextMatch = next(millis);
return nextMatch > millis ? nextMatch - millis : -1;
}
/**
* Gets delay from current time until next match.
* @return Delay in milliseconds until next match from now
*/
public long nextFromNow()
{
return nextFrom(System.currentTimeMillis());
}
/**
* Gets the delay in milliseconds until the next match from now.
* @return Delay in milliseconds until next match
*/
public long getDelayToNextFromNow()
{
return nextFromNow();
}
/**
* Gets delay with offset subtraction.
* @param offsetInMinutes Offset to subtract from delay
* @return Adjusted delay in milliseconds
*/
public long getOffsettedDelayToNextFromNow(int offsetInMinutes)
{
final long delay = getDelayToNextFromNow();
final long offsetMillis = TimeUnit.MINUTES.toMillis(offsetInMinutes);
return Math.max(0, delay - offsetMillis);
}
/**
* Gets the next matching time from now as a formatted date string.
* @return Formatted date string of next match
*/
public String getNextAsFormattedDateString()
{
final long nextMatch = next(System.currentTimeMillis());
return nextMatch > 0 ? new Date(nextMatch).toString() : NO_FUTURE_MATCH_MESSAGE;
}
@Override
public String toString()
{
return _originalPattern;
}
/**
* Parses the pattern string into cron expressions.
* @param pattern the pattern string to parse
* @return list of cron expressions
*/
private List<CronExpression> parsePattern(String pattern)
{
final List<CronExpression> result = new ArrayList<>();
// Split on pipe for OR expressions.
final String[] orPatterns = pattern.split(PIPE_SEPARATOR);
for (String orPattern : orPatterns)
{
final String[] fields = orPattern.trim().split(WHITESPACE_PATTERN);
if ((fields.length < MINIMUM_CRON_FIELDS) || (fields.length > MAXIMUM_CRON_FIELDS))
{
throw new IllegalArgumentException("Pattern must have 5 or 6 fields: " + orPattern);
}
try
{
// Parse fields with extended syntax support.
final ExtendedFieldResult minuteResult = parseExtendedField(fields[0]);
final ExtendedFieldResult hourResult = parseExtendedField(fields[1]);
final ExtendedFieldResult dayResult = parseExtendedField(fields[2]);
final FieldMatcher minuteMatcher = parseField(minuteResult.pattern, MINUTE_MIN, MINUTE_MAX, null);
final FieldMatcher hourMatcher = parseField(hourResult.pattern, HOUR_MIN, HOUR_MAX, null);
final FieldMatcher dayMatcher = parseField(dayResult.pattern, DAY_MIN, DAY_MAX, null);
final FieldMatcher monthMatcher = parseField(fields[3], MONTH_MIN, MONTH_MAX, MONTH_ALIASES);
final FieldMatcher dayOfWeekMatcher = parseField(fields[4], DAY_OF_WEEK_MIN, DAY_OF_WEEK_MAX, DAY_ALIASES);
// Parse optional week offset (6th field).
int weekOffset = 0;
if (fields.length == MAXIMUM_CRON_FIELDS)
{
final String weekField = fields[5].trim();
if (weekField.startsWith("+"))
{
weekOffset = Integer.parseInt(weekField.substring(1));
}
else
{
throw new IllegalArgumentException("Week offset must start with '+': " + weekField);
}
}
result.add(new CronExpression(minuteMatcher, hourMatcher, dayMatcher, monthMatcher, dayOfWeekMatcher, minuteResult.randomModifier, hourResult.randomModifier, hourResult.addModifier, dayResult.addModifier, weekOffset));
}
catch (Exception e)
{
throw new IllegalArgumentException("Invalid pattern format: " + orPattern, e);
}
}
return result;
}
/**
* Result of parsing an extended field with modifiers.
*/
private static class ExtendedFieldResult
{
final String pattern;
final int randomModifier;
final int addModifier;
ExtendedFieldResult(String pattern, int randomModifier, int addModifier)
{
this.pattern = pattern;
this.randomModifier = randomModifier;
this.addModifier = addModifier;
}
}
/**
* Parses a field that may contain extended syntax modifiers.<br>
* Format: [modifier:]pattern where modifier can be ~N or +N.
* @param field the field to parse
* @return extended field result with pattern and modifiers
*/
private ExtendedFieldResult parseExtendedField(String field)
{
if (!field.contains(":"))
{
return new ExtendedFieldResult(field, 0, 0);
}
final String[] parts = field.split(":");
if (parts.length != CRON_PARTS_EXPECTED)
{
throw new IllegalArgumentException("Invalid extended field format: " + field);
}
final String modifier = parts[0];
final String pattern = parts[1];
int randomModifier = 0;
int addModifier = 0;
if (modifier.startsWith("~"))
{
randomModifier = Integer.parseInt(modifier.substring(1));
}
else if (modifier.startsWith("+"))
{
addModifier = Integer.parseInt(modifier.substring(1));
}
else if (!modifier.isEmpty())
{
throw new IllegalArgumentException("Unknown modifier: " + modifier);
}
return new ExtendedFieldResult(pattern, randomModifier, addModifier);
}
/**
* Parses a field value with support for wildcards, ranges, lists and step values.
* @param field the field string to parse
* @param min minimum allowed value
* @param max maximum allowed value
* @param aliases optional aliases map for named values
* @return field matcher for the parsed field
*/
private FieldMatcher parseField(String field, int min, int max, Map<String, Integer> aliases)
{
if ("*".equals(field))
{
return new WildcardMatcher();
}
final Set<Integer> values = new HashSet<>();
final String[] parts = field.split(",");
for (String part : parts)
{
values.addAll(parseFieldPart(part.trim(), min, max, aliases));
}
return new ValueSetMatcher(values);
}
/**
* Parses a single field part with support for ranges and step values.
* @param part the field part to parse
* @param min minimum allowed value
* @param max maximum allowed value
* @param aliases optional aliases map for named values
* @return set of integer values matching the part
*/
private Set<Integer> parseFieldPart(String part, int min, int max, Map<String, Integer> aliases)
{
final Set<Integer> values = new HashSet<>();
// Handle step values (e.g., */5 or 1-10/2).
final String[] stepParts = part.split("/");
final int step = stepParts.length > 1 ? Integer.parseInt(stepParts[1]) : 1;
final String rangePart = stepParts[0];
if ("*".equals(rangePart))
{
// */step pattern.
for (int i = min; i <= max; i += step)
{
values.add(i);
}
}
else if (rangePart.contains("-"))
{
// Range pattern (e.g., 1-5 or mon-fri).
final String[] range = rangePart.split("-", 2);
final int start = parseValue(range[0], aliases);
final int end = parseValue(range[1], aliases);
if (start <= end)
{
for (int i = start; i <= end; i += step)
{
values.add(i);
}
}
else
{
// Wrap-around range (e.g., fri-mon for days).
for (int i = start; i <= max; i += step)
{
values.add(i);
}
for (int i = min; i <= end; i += step)
{
values.add(i);
}
}
}
else // Single value.
{
values.add(parseValue(rangePart, aliases));
}
return values;
}
/**
* Parses a single value with support for aliases and special markers.
* @param value the value string to parse
* @param aliases optional aliases map for named values
* @return parsed integer value
*/
private int parseValue(String value, Map<String, Integer> aliases)
{
if ("L".equalsIgnoreCase(value))
{
return LAST_DAY_MARKER; // Special marker for last day of month.
}
if ((aliases != null) && aliases.containsKey(value.toLowerCase()))
{
return aliases.get(value.toLowerCase());
}
try
{
return Integer.parseInt(value);
}
catch (NumberFormatException e)
{
throw new IllegalArgumentException("Invalid value: " + value, e);
}
}
/**
* Interface for matching field values against calendar dates.
*/
private interface FieldMatcher
{
boolean matches(int value, Calendar calendar);
}
/**
* Matcher that accepts any value (wildcard).
*/
private static class WildcardMatcher implements FieldMatcher
{
@Override
public boolean matches(int value, Calendar calendar)
{
return true;
}
}
/**
* Matcher that checks against a predefined set of values.
*/
private static class ValueSetMatcher implements FieldMatcher
{
private final Set<Integer> _values;
ValueSetMatcher(Set<Integer> values)
{
_values = new HashSet<>(values);
}
@Override
public boolean matches(int value, Calendar calendar)
{
if (_values.contains(value))
{
return true;
}
// Handle last day of month (L).
if (_values.contains(LAST_DAY_MARKER) && isLastDayOfMonth(calendar))
{
return true;
}
return false;
}
/**
* Checks if the calendar date is the last day of the month.
* @param calendar the calendar to check
* @return true if it's the last day of the month
*/
private boolean isLastDayOfMonth(Calendar calendar)
{
final int currentDay = calendar.get(Calendar.DAY_OF_MONTH);
final int lastDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
return currentDay == lastDay;
}
}
/**
* Represents a single cron expression with extended modifiers.
*/
private static class CronExpression
{
// Field matchers.
private final FieldMatcher _minuteMatcher;
private final FieldMatcher _hourMatcher;
private final FieldMatcher _dayMatcher;
private final FieldMatcher _monthMatcher;
private final FieldMatcher _dayOfWeekMatcher;
// Extended syntax modifiers.
private final int _minuteRandomModifier;
private final int _hourRandomModifier;
private final int _hourAddModifier;
private final int _dayAddModifier;
private final int _weekOffset;
CronExpression(FieldMatcher minuteMatcher, FieldMatcher hourMatcher, FieldMatcher dayMatcher, FieldMatcher monthMatcher, FieldMatcher dayOfWeekMatcher, int minuteRandomModifier, int hourRandomModifier, int hourAddModifier, int dayAddModifier, int weekOffset)
{
_minuteMatcher = minuteMatcher;
_hourMatcher = hourMatcher;
_dayMatcher = dayMatcher;
_monthMatcher = monthMatcher;
_dayOfWeekMatcher = dayOfWeekMatcher;
_minuteRandomModifier = minuteRandomModifier;
_hourRandomModifier = hourRandomModifier;
_hourAddModifier = hourAddModifier;
_dayAddModifier = dayAddModifier;
_weekOffset = weekOffset;
}
/**
* Checks if the calendar date matches this cron expression.
* @param calendar the calendar to test
* @return true if the date matches
*/
boolean matches(Calendar calendar)
{
// Create a copy for testing with offsets applied.
final Calendar testCalendar = Calendar.getInstance(calendar.getTimeZone());
testCalendar.setTimeInMillis(calendar.getTimeInMillis());
// Apply reverse offsets for matching (subtract what would be added).
if (_weekOffset != 0)
{
testCalendar.add(Calendar.WEEK_OF_YEAR, -_weekOffset);
}
if (_dayAddModifier != 0)
{
testCalendar.add(Calendar.DAY_OF_YEAR, -_dayAddModifier);
}
if (_hourAddModifier != 0)
{
testCalendar.add(Calendar.HOUR_OF_DAY, -_hourAddModifier);
}
final int minute = testCalendar.get(Calendar.MINUTE);
final int hour = testCalendar.get(Calendar.HOUR_OF_DAY);
final int day = testCalendar.get(Calendar.DAY_OF_MONTH);
final int month = testCalendar.get(Calendar.MONTH) + CALENDAR_MONTH_OFFSET; // Calendar months are 0-based.
final int dayOfWeek = testCalendar.get(Calendar.DAY_OF_WEEK) - CALENDAR_DAY_OF_WEEK_OFFSET; // Convert to 0 = Sunday.
return _minuteMatcher.matches(minute, testCalendar) && _hourMatcher.matches(hour, testCalendar) && _dayMatcher.matches(day, testCalendar) && _monthMatcher.matches(month, testCalendar) && _dayOfWeekMatcher.matches(dayOfWeek, testCalendar);
}
/**
* Finds the next matching time after the specified timestamp.
* @param afterMillis timestamp to search after
* @param timeZone timezone for calculation
* @return next matching timestamp in milliseconds
*/
long getNextMatch(long afterMillis, TimeZone timeZone)
{
final Calendar calendar = Calendar.getInstance(timeZone);
calendar.setTimeInMillis(afterMillis);
calendar.add(Calendar.MINUTE, 1); // Start from next minute.
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// Search up to 4 years in the future to avoid infinite loops.
final Calendar endCalendar = Calendar.getInstance(timeZone);
endCalendar.setTimeInMillis(afterMillis);
endCalendar.add(Calendar.YEAR, SEARCH_LIMIT_YEARS);
// Reuse single calendar instance for result calculations.
final Calendar resultCalendar = Calendar.getInstance(timeZone);
while (calendar.before(endCalendar))
{
if (matches(calendar))
{
// Apply forward offsets and randomization.
resultCalendar.setTimeInMillis(calendar.getTimeInMillis());
// Apply fixed offsets.
if (_weekOffset != 0)
{
resultCalendar.add(Calendar.WEEK_OF_YEAR, _weekOffset);
}
if (_dayAddModifier != 0)
{
resultCalendar.add(Calendar.DAY_OF_YEAR, _dayAddModifier);
}
if (_hourAddModifier != 0)
{
resultCalendar.add(Calendar.HOUR_OF_DAY, _hourAddModifier);
}
// Apply random offsets.
if (_hourRandomModifier > 0)
{
resultCalendar.add(Calendar.HOUR_OF_DAY, Rnd.get(_hourRandomModifier + 1));
}
if (_minuteRandomModifier > 0)
{
resultCalendar.add(Calendar.MINUTE, Rnd.get(_minuteRandomModifier + 1));
}
return resultCalendar.getTimeInMillis();
}
calendar.add(Calendar.MINUTE, 1);
}
return -1; // No match found.
}
}
}
@@ -0,0 +1,268 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.time;
import java.time.Duration;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Date;
/**
* Utility class for time-related operations, such as parsing durations, scheduling future dates and formatting dates.
* @author Mobius
*/
public class TimeUtil
{
/**
* Parses a string duration (e.g., "5days", "2hours") into a {@link Duration} object.
* @param durationString the string representing the duration with a numeric value and time unit (e.g., "5days", "10hours", "2weeks").
* @return a {@link Duration} object representing the specified duration.
* @throws IllegalArgumentException if the input format is invalid or the unit is unrecognized.
*/
public static Duration parseDuration(String durationString)
{
int index = 0;
while ((index < durationString.length()) && Character.isDigit(durationString.charAt(index)))
{
index++;
}
if ((index == 0) || (index == durationString.length()))
{
throw new IllegalArgumentException("Invalid duration format: " + durationString);
}
int durationValue;
String durationUnit;
try
{
durationValue = Integer.parseInt(durationString.substring(0, index));
durationUnit = durationString.substring(index).toLowerCase();
}
catch (NumberFormatException e)
{
throw new IllegalArgumentException("Invalid duration format: " + durationString);
}
switch (durationUnit)
{
case "sec":
case "secs":
{
return Duration.ofSeconds(durationValue);
}
case "min":
case "mins":
{
return Duration.ofMinutes(durationValue);
}
case "hour":
case "hours":
{
return Duration.ofHours(durationValue);
}
case "day":
case "days":
{
return Duration.ofDays(durationValue);
}
case "week":
case "weeks":
{
return Duration.ofDays(durationValue * 7L);
}
case "month":
case "months":
{
return Duration.ofDays(durationValue * 30L);
}
case "year":
case "years":
{
return Duration.ofDays(durationValue * 365L);
}
default:
{
throw new IllegalArgumentException("Unrecognized time unit: " + durationUnit);
}
}
}
/**
* Formats a duration in milliseconds into a user-friendly string, specifying the number of days, hours, minutes, seconds, and milliseconds (if any).
* @param millis the duration in milliseconds.
* @return a formatted string representing the duration.
*/
public static String formatDuration(long millis)
{
if (millis < 1)
{
return "0 milliseconds";
}
long days = millis / (24 * 60 * 60 * 1000);
millis %= (24 * 60 * 60 * 1000);
long hours = millis / (60 * 60 * 1000);
millis %= (60 * 60 * 1000);
long minutes = millis / (60 * 1000);
millis %= (60 * 1000);
long seconds = millis / 1000;
millis %= 1000;
final StringBuilder sb = new StringBuilder();
if (days > 0)
{
sb.append(days).append(" day").append(days > 1 ? "s" : "").append(", ");
}
if (hours > 0)
{
sb.append(hours).append(" hour").append(hours > 1 ? "s" : "").append(", ");
}
if (minutes > 0)
{
sb.append(minutes).append(" minute").append(minutes > 1 ? "s" : "").append(", ");
}
if (seconds > 0)
{
sb.append(seconds).append(" second").append(seconds > 1 ? "s" : "").append(", ");
}
if (millis > 0)
{
sb.append(millis).append(" millisecond").append(millis > 1 ? "s" : "");
}
// Remove the trailing comma and space, if present.
if ((sb.length() > 2) && (sb.charAt(sb.length() - 2) == ','))
{
sb.setLength(sb.length() - 2);
}
return sb.toString();
}
/**
* Formats a date into a string based on the provided format pattern.
* @param date the {@link Date} object to format.
* @param format the date format pattern (e.g., "dd/MM/yyyy").
* @return a formatted date string or null if the date is null.
*/
public static String formatDate(Date date, String format)
{
return date == null ? null : DateTimeFormatter.ofPattern(format).format(date.toInstant().atZone(ZoneId.systemDefault()));
}
/**
* Formats a date to a string in the "dd/MM/yyyy" format.
* @param date the {@link Date} object to format.
* @return a formatted date string or null if the date is null.
*/
public static String getDateString(Date date)
{
return formatDate(date, "dd/MM/yyyy");
}
/**
* Formats a date to a string in the "dd/MM/yyyy HH:mm:ss" format.
* @param date the {@link Date} object to format.
* @return a formatted date-time string or null if the date is null.
*/
public static String getDateTimeString(Date date)
{
return formatDate(date, "dd/MM/yyyy HH:mm:ss");
}
/**
* Formats a timestamp (in milliseconds) to a string in the "dd/MM/yyyy" format.
* @param millis the timestamp in milliseconds.
* @return a formatted date string.
*/
public static String getDateString(long millis)
{
return getDateString(new Date(millis));
}
/**
* Formats a timestamp (in milliseconds) to a string in the "dd/MM/yyyy HH:mm:ss" format.
* @param millis the timestamp in milliseconds.
* @return a formatted date-time string.
*/
public static String getDateTimeString(long millis)
{
return getDateTimeString(new Date(millis));
}
/**
* Gets the next occurrence of the specified day of the week, hour, and minute. If the specified time is in the past for today, the next week will be scheduled.
* @param dayOfWeek the desired day of the week (e.g., {@link Calendar#MONDAY}).
* @param hour the hour of the day (0-23).
* @param minute the minute of the hour (0-59).
* @return a {@link Calendar} object set to the next occurrence of the specified day and time.
*/
public static Calendar getNextDayTime(int dayOfWeek, int hour, int minute)
{
final Calendar calendar = Calendar.getInstance();
final int today = calendar.get(Calendar.DAY_OF_WEEK);
int daysUntilNext = ((dayOfWeek - today) + 7) % 7;
if ((daysUntilNext == 0) && ((calendar.get(Calendar.HOUR_OF_DAY) > hour) || ((calendar.get(Calendar.HOUR_OF_DAY) == hour) && (calendar.get(Calendar.MINUTE) >= minute))))
{
daysUntilNext = 7; // Schedule for the next week if today's time has passed.
}
calendar.add(Calendar.DAY_OF_MONTH, daysUntilNext);
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar;
}
/**
* Gets the next occurrence of the specified hour and minute on the current day. If the specified time is in the past for today, the next day will be scheduled.
* @param hour the hour of the day (0-23).
* @param minute the minute of the hour (0-59).
* @return a {@link Calendar} object set to the next occurrence of the specified time.
*/
public static Calendar getNextTime(int hour, int minute)
{
final Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// If the target time has already passed today, schedule for the next day.
if (calendar.before(Calendar.getInstance()))
{
calendar.add(Calendar.DAY_OF_YEAR, 1);
}
return calendar;
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.ui;
import java.awt.Color;
import javax.swing.UIManager;
import javax.swing.plaf.nimbus.NimbusLookAndFeel;
/**
* @author Mobius
*/
public class DarkTheme
{
public static void activate()
{
// Modify existing white Nimbus look and feel to dark.
UIManager.put("control", new Color(128, 128, 128));
UIManager.put("info", new Color(128, 128, 128));
UIManager.put("nimbusBase", Color.DARK_GRAY); // new Color(18, 30, 49)
UIManager.put("nimbusAlertYellow", new Color(248, 187, 0));
UIManager.put("nimbusDisabledText", new Color(128, 128, 128));
UIManager.put("nimbusFocus", Color.DARK_GRAY); // new Color(115, 164, 209)
UIManager.put("nimbusGreen", new Color(176, 179, 50));
UIManager.put("nimbusInfoBlue", Color.DARK_GRAY); // new Color(66, 139, 221)
UIManager.put("nimbusLightBackground", Color.DARK_GRAY); // new Color(18, 30, 49)
UIManager.put("nimbusOrange", new Color(191, 98, 4));
UIManager.put("nimbusRed", new Color(169, 46, 34));
UIManager.put("nimbusSelectedText", new Color(255, 255, 255));
UIManager.put("nimbusSelectionBackground", new Color(104, 93, 156));
UIManager.put("text", new Color(230, 230, 230));
// Set look and feel.
try
{
UIManager.setLookAndFeel(new NimbusLookAndFeel());
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
@@ -0,0 +1,170 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.ui;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
/**
* A {@link DocumentListener} to limit the maximum number of lines in a Document.<br>
* If the number of lines exceeds the specified limit, excess lines will be removed either from the start or the end of the Document, depending on the specified configuration:<br>
* a) Removing from the start is typically used when appending text.<br>
* b) Removing from the end is used when inserting text at the beginning.
* @author Mobius
*/
public class LineLimitListener implements DocumentListener
{
private final boolean _removeFromStart;
private final int _maxLines;
/**
* Constructs a LineLimitListener with a specified maximum line count.<br>
* By default, this configuration removes excess lines from the start of the Document.
* @param maxLines the maximum number of lines to retain in the Document
*/
public LineLimitListener(int maxLines)
{
this(maxLines, true);
}
/**
* Constructs a LineLimitListener with a specified maximum line count<br>
* and a setting to control where excess lines are removed from.
* @param maxLines the maximum number of lines to retain in the Document
* @param removeFromStart if true, excess lines are removed from the start; if false, from the end
*/
public LineLimitListener(int maxLines, boolean removeFromStart)
{
_removeFromStart = removeFromStart;
_maxLines = maxLines;
}
/**
* Returns the maximum number of lines that this listener will retain in the Document.
* @return the maximum line count allowed in the Document
*/
public int getLimitLines()
{
return _maxLines;
}
/**
* Removes excess lines from the Document when the line count exceeds the maximum limit.<br>
* This method determines whether to remove lines from the start or end based on the configured setting.
* @param event the DocumentEvent that triggered this method call
*/
private void removeLines(DocumentEvent event)
{
// The root Element of the Document will tell us the total number of line in the Document.
final Document document = event.getDocument();
final Element root = document.getDefaultRootElement();
while (root.getElementCount() > _maxLines)
{
if (_removeFromStart)
{
removeFromStart(document, root);
}
else
{
removeFromEnd(document, root);
}
}
}
/**
* Removes lines from the start of the Document until the line count is within the limit.
* @param document the Document to be modified
* @param root the root Element representing all lines in the Document
*/
private void removeFromStart(Document document, Element root)
{
final Element line = root.getElement(0);
final int end = line.getEndOffset();
try
{
document.remove(0, end);
}
catch (BadLocationException ble)
{
System.out.println(ble);
}
}
/**
* Removes lines from the end of the Document until the line count is within the limit.<br>
* The newline character preceding the last line is also removed to maintain line integrity.
* @param document the Document to be modified
* @param root the root Element representing all lines in the Document
*/
private void removeFromEnd(Document document, Element root)
{
// We use start minus 1 to make sure we remove the newline character of the previous line.
final Element line = root.getElement(root.getElementCount() - 1);
final int start = line.getStartOffset();
final int end = line.getEndOffset();
try
{
document.remove(start - 1, end - start);
}
catch (BadLocationException e)
{
System.out.println(e);
}
}
/**
* Handles the insertion of new text into the Document. After text is inserted,<br>
* this method schedules a check (on the EDT) to remove lines if the maximum line count is exceeded.
* @param event the DocumentEvent representing the text insertion
*/
@Override
public void insertUpdate(DocumentEvent event)
{
SwingUtilities.invokeLater(() -> removeLines(event));
}
/**
* No action taken on text removal, as this event does not affect line limit enforcement.
* @param event the DocumentEvent representing the text removal
*/
@Override
public void removeUpdate(DocumentEvent event)
{
// No action required.
}
/**
* No action taken on attribute changes, as this event does not affect line limit enforcement.
* @param event the DocumentEvent representing the attribute change
*/
@Override
public void changedUpdate(DocumentEvent event)
{
// No action required.
}
}
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.ui;
import java.awt.Color;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JWindow;
/**
* @author Mobius
*/
public class SplashScreen extends JWindow
{
private final Image _image;
/**
* @param path of image file
* @param time in milliseconds
* @param parent frame to set visible after time ends
*/
public SplashScreen(String path, long time, JFrame parent)
{
setBackground(new Color(0, 255, 0, 0)); // Transparency.
_image = Toolkit.getDefaultToolkit().getImage(path);
final ImageIcon imageIcon = new ImageIcon(_image);
setSize(imageIcon.getIconWidth(), imageIcon.getIconHeight());
setLocationRelativeTo(null);
setAlwaysOnTop(true);
setVisible(true);
new Timer().schedule(new TimerTask()
{
@Override
public void run()
{
setVisible(false);
if (parent != null)
{
// Make parent visible.
parent.setVisible(true);
// Focus parent window.
parent.toFront();
parent.setState(Frame.ICONIFIED);
parent.setState(Frame.NORMAL);
}
dispose();
}
}, imageIcon.getIconWidth() > 0 ? time : 100);
}
@Override
public void paint(Graphics g)
{
g.drawImage(_image, 0, 0, null);
}
}
@@ -0,0 +1,411 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Properties;
import java.util.logging.Logger;
import org.l2jmobius.commons.time.TimeUtil;
/**
* ConfigReader is a utility class that reads and provides access to configuration properties from a file.
* @author Mobius
*/
public class ConfigReader
{
private static final Logger LOGGER = Logger.getLogger(ConfigReader.class.getName());
private final Properties _properties = new Properties();
private final File _file;
/**
* Constructs a ConfigReader with the specified file path using the system's default Charset.
* @param filePath the path to the configuration file
*/
public ConfigReader(String filePath)
{
_file = new File(filePath);
if (!Files.exists(_file.toPath()))
{
LOGGER.warning("Configuration file not found: " + _file.getAbsolutePath());
return;
}
try (InputStream input = Files.newInputStream(_file.toPath());
InputStreamReader reader = new InputStreamReader(input, Charset.defaultCharset()))
{
_properties.load(reader);
}
catch (IOException e)
{
LOGGER.warning("Failed to load configurations from " + _file.getName() + ": " + e.getMessage());
}
}
/**
* Checks if the specified configuration key exists in the configurations.
* @param config the configuration key
* @return true if the key exists, false otherwise
*/
public boolean containsKey(String config)
{
return _properties.containsKey(config);
}
/**
* Retrieves the value associated with the specified key as a String.
* @param config the configuration key
* @return the property value as a String, or null if the key does not exist
*/
public String getValue(String config)
{
return _properties.getProperty(config);
}
/**
* Returns an unmodifiable collection of all property names that have string values.
* @return a {@link Collection} of property names as strings.
*/
public Collection<String> getStringPropertyNames()
{
return _properties.stringPropertyNames();
}
/**
* Retrieves the value associated with the specified key as a boolean.
* @param config the configuration key
* @param defaultValue the default value if the key does not exist or is malformed
* @return the property value as a boolean, or the default value if the key does not exist or is malformed
*/
public boolean getBoolean(String config, boolean defaultValue)
{
final String value = _properties.getProperty(config);
if (value != null)
{
try
{
return Boolean.parseBoolean(value);
}
catch (Exception e)
{
LOGGER.warning("Invalid boolean for config '" + config + "' in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
}
else
{
LOGGER.warning("Config '" + config + "' not found in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
return defaultValue;
}
/**
* Retrieves the value associated with the specified key as a byte.
* @param config the configuration key
* @param defaultValue the default value if the key does not exist or is malformed
* @return the property value as a byte, or the default value if the key does not exist or is malformed
*/
public byte getByte(String config, byte defaultValue)
{
final String value = _properties.getProperty(config);
if (value != null)
{
try
{
return Byte.parseByte(value);
}
catch (Exception e)
{
LOGGER.warning("Invalid byte for config '" + config + "' in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
}
else
{
LOGGER.warning("Config '" + config + "' not found in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
return defaultValue;
}
/**
* Retrieves the value associated with the specified key as a short.
* @param config the configuration key
* @param defaultValue the default value if the key does not exist or is malformed
* @return the property value as a short, or the default value if the key does not exist or is malformed
*/
public short getShort(String config, short defaultValue)
{
final String value = _properties.getProperty(config);
if (value != null)
{
try
{
return Short.parseShort(value);
}
catch (Exception e)
{
LOGGER.warning("Invalid short for config '" + config + "' in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
}
else
{
LOGGER.warning("Config '" + config + "' not found in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
return defaultValue;
}
/**
* Retrieves the value associated with the specified key as an int.
* @param config the configuration key
* @param defaultValue the default value if the key does not exist or is malformed
* @return the property value as an int, or the default value if the key does not exist or is malformed
*/
public int getInt(String config, int defaultValue)
{
final String value = _properties.getProperty(config);
if (value != null)
{
try
{
return Integer.parseInt(value);
}
catch (Exception e)
{
LOGGER.warning("Invalid int for config '" + config + "' in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
}
else
{
LOGGER.warning("Config '" + config + "' not found in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
return defaultValue;
}
/**
* Retrieves the value associated with the specified key as a long.
* @param config the configuration key
* @param defaultValue the default value if the key does not exist or is malformed
* @return the property value as a long, or the default value if the key does not exist or is malformed
*/
public long getLong(String config, long defaultValue)
{
final String value = _properties.getProperty(config);
if (value != null)
{
try
{
return Long.parseLong(value);
}
catch (Exception e)
{
LOGGER.warning("Invalid long for config '" + config + "' in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
}
else
{
LOGGER.warning("Config '" + config + "' not found in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
return defaultValue;
}
/**
* Retrieves the value associated with the specified key as a float.
* @param config the configuration key
* @param defaultValue the default value if the key does not exist or is malformed
* @return the property value as a float, or the default value if the key does not exist or is malformed
*/
public float getFloat(String config, float defaultValue)
{
final String value = _properties.getProperty(config);
if (value != null)
{
try
{
return Float.parseFloat(value);
}
catch (Exception e)
{
LOGGER.warning("Invalid float for config '" + config + "' in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
}
else
{
LOGGER.warning("Config '" + config + "' not found in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
return defaultValue;
}
/**
* Retrieves the value associated with the specified key as a double.
* @param config the configuration key
* @param defaultValue the default value if the key does not exist or is malformed
* @return the property value as a double, or the default value if the key does not exist or is malformed
*/
public double getDouble(String config, double defaultValue)
{
final String value = _properties.getProperty(config);
if (value != null)
{
try
{
return Double.parseDouble(value);
}
catch (Exception e)
{
LOGGER.warning("Invalid double for config '" + config + "' in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
}
else
{
LOGGER.warning("Config '" + config + "' not found in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
return defaultValue;
}
/**
* Retrieves the value associated with the specified key as a String.
* @param config the configuration key
* @param defaultValue the default value if the key does not exist
* @return the property value as a String, or the default value if the key does not exist
*/
public String getString(String config, String defaultValue)
{
final String value = _properties.getProperty(config);
if (value == null)
{
LOGGER.warning("Config '" + config + "' not found in file '" + _file.getName() + "', using default: " + defaultValue + ".");
return defaultValue;
}
return value;
}
/**
* Retrieves the value associated with the specified key as an enum constant of the specified type.
* @param <T> the type of the enum
* @param config the configuration key
* @param clazz the enum class to parse the value as
* @param defaultValue the default value if the key does not exist or is malformed
* @return the property value as an enum constant, or the default value if the key does not exist or is malformed
*/
public <T extends Enum<T>> T getEnum(String config, Class<T> clazz, T defaultValue)
{
final String value = _properties.getProperty(config);
if (value != null)
{
try
{
return Enum.valueOf(clazz, value);
}
catch (Exception e)
{
LOGGER.warning("Invalid enum for config '" + config + "' in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
}
else
{
LOGGER.warning("Config '" + config + "' not found in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
return defaultValue;
}
/**
* Retrieves the value associated with the specified key as a Duration.
* @param config the configuration key
* @param defaultValue the default value as a string if the key does not exist or is malformed
* @return the property value as a Duration, or the parsed default value if the key does not exist or is malformed
*/
public Duration getDuration(String config, String defaultValue)
{
final String value = _properties.getProperty(config);
if (value != null)
{
try
{
return TimeUtil.parseDuration(value);
}
catch (Exception e)
{
LOGGER.warning("Invalid duration for config '" + config + "' in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
}
else
{
LOGGER.warning("Config '" + config + "' not found in file '" + _file.getName() + "', using default: " + defaultValue + ".");
}
return TimeUtil.parseDuration(defaultValue);
}
/**
* Retrieves the value associated with the specified key as an int array.
* @param config the configuration key
* @param delimiter the separator used to split the string into integers
* @param defaultValue the default value as a string if the key does not exist or is malformed
* @return the property value, or the default value if the key does not exist or is malformed, as an int array
*/
public int[] getIntArray(String config, String delimiter, String defaultValue)
{
final String value = _properties.getProperty(config);
if (value != null)
{
try
{
return Arrays.stream(value.split(delimiter)).map(String::trim).mapToInt(Integer::parseInt).toArray();
}
catch (NumberFormatException e)
{
LOGGER.warning("Invalid int array for config '" + config + "' in file '" + _file.getName() + "', using default values.");
}
}
else
{
LOGGER.warning("Config '" + config + "' not found in file '" + _file.getName() + "', using default values.");
}
try
{
return Arrays.stream(defaultValue.split(delimiter)).map(String::trim).mapToInt(Integer::parseInt).toArray();
}
catch (NumberFormatException e)
{
LOGGER.warning("Invalid default values for config '" + config + "' in file '" + _file.getName() + "', using empty array.");
}
return new int[0];
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.util;
/**
* Utility class for handling operations related to hexadecimal data.
* @author Mobius
*/
public class HexUtil
{
/**
* Generates a byte array of the specified size filled with random non-zero values.
* @param size the size of the byte array to generate
* @return a byte array filled with random non-zero values
*/
public static byte[] generateHexBytes(int size)
{
final byte[] array = new byte[size];
Rnd.nextBytes(array);
// Ensure no zero values are in the array.
for (int i = 0; i < array.length; i++)
{
while (array[i] == 0)
{
array[i] = (byte) Rnd.get(Byte.MAX_VALUE);
}
}
return array;
}
}
@@ -0,0 +1,738 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.util;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXParseException;
import org.l2jmobius.commons.config.ThreadConfig;
/**
* Interface for XML parsers.
* @author Zoey76, Mobius
*/
public interface IXmlReader
{
static final Logger LOGGER = Logger.getLogger(IXmlReader.class.getName());
static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
/**
* Loads or reloads the data. It is recommended to clear the data storage (either a list or a map) before loading.
*/
void load();
/**
* Parses an XML file located within the datapack directory. This is a helper method for {@link #parseFile(File)}.
* @param path the relative path of the XML file within the datapack directory.
*/
default void parseDatapackFile(String path)
{
parseFile(new File(".", path));
}
/**
* Parses a single XML file. Calls {@link #parseDocument(Document, File)} if the file is successfully parsed. <b>Validation is enabled by default.</b>
* @param file the XML file to parse.
*/
default void parseFile(File file)
{
if (!isValidXmlFile(file))
{
LOGGER.warning("Cannot parse " + file.getName() + ": file does not exist or is not valid.");
return;
}
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(isValidating());
factory.setIgnoringComments(true);
try
{
factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
final DocumentBuilder builder = factory.newDocumentBuilder();
parseDocument(builder.parse(file), file);
}
catch (SAXParseException e)
{
LOGGER.log(Level.WARNING, "Error parsing " + file.getName() + " at line " + e.getLineNumber() + ", column " + e.getColumnNumber() + ".", e);
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Error parsing " + file.getName(), e);
}
}
/**
* Parses XML files in the specified directory. This is a helper method for {@link #parseDirectory(File, boolean)}.
* @param directory the path to the directory with XML files.
* @return {@code false} if the directory is not found, {@code true} otherwise.
*/
default boolean parseDirectory(File directory)
{
return parseDirectory(directory, false);
}
/**
* Parses XML files in a directory within the datapack. This is a helper method for {@link #parseDirectory(File, boolean)}.
* @param path the path to the directory within the datapack.
* @param recursive if {@code true}, parses files in all subdirectories.
* @return {@code false} if the directory is not found, {@code true} otherwise.
*/
default boolean parseDatapackDirectory(String path, boolean recursive)
{
return parseDirectory(new File(".", path), recursive);
}
/**
* Loads all XML files from the specified directory and parses each file.
* @param directory the directory to scan for XML files.
* @param recursive if {@code true}, parses files in all subdirectories.
* @return {@code false} if the directory is not found, {@code true} otherwise.
*/
default boolean parseDirectory(File directory, boolean recursive)
{
if (!directory.exists())
{
LOGGER.warning("Directory not found: " + directory.getAbsolutePath());
return false;
}
// If multithreading is enabled, use a thread pool to parse files.
if (ThreadConfig.THREADS_FOR_LOADING)
{
final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());
final List<Future<?>> tasks = new ArrayList<>();
final File[] files = directory.listFiles();
if (files != null)
{
for (File file : files)
{
if (recursive && file.isDirectory())
{
parseDirectory(file, true);
}
else if (isValidXmlFile(file))
{
tasks.add(executorService.schedule(() -> parseFile(file), 0, TimeUnit.MILLISECONDS));
}
}
}
for (Future<?> task : tasks)
{
try
{
task.get();
}
catch (Exception e)
{
LOGGER.warning("Failed to parse file: " + e.getMessage());
}
}
executorService.shutdown();
try
{
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
LOGGER.warning("Parsing process was interrupted: " + e.getMessage());
}
}
else // Parse files sequentially if multithreading is not enabled.
{
final File[] files = directory.listFiles();
if (files != null)
{
for (File file : files)
{
if (recursive && file.isDirectory())
{
parseDirectory(file, true);
}
else if (isValidXmlFile(file))
{
parseFile(file);
}
}
}
}
return true;
}
/**
* Abstract method for parsing the current document. Called from {@link #parseFile(File)}.
* @param document the document to parse
* @param file the file being processed
*/
void parseDocument(Document document, File file);
/**
* Parses a boolean value from the given node.
* @param node the XML node to parse
* @param defaultValue the default value to return if the node is null
* @return the parsed boolean value, or the default value if the node is null
*/
default Boolean parseBoolean(Node node, Boolean defaultValue)
{
return node != null ? Boolean.valueOf(node.getNodeValue()) : defaultValue;
}
/**
* Parses a boolean value from the given node.
* @param node the XML node to parse
* @return the parsed boolean value, or null if the node is null
*/
default Boolean parseBoolean(Node node)
{
return parseBoolean(node, null);
}
/**
* Parses a boolean value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @return the parsed boolean value, or null if the attribute is not found
*/
default Boolean parseBoolean(NamedNodeMap attributes, String name)
{
return parseBoolean(attributes.getNamedItem(name));
}
/**
* Parses a boolean value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @param defaultValue the default value to return if the attribute is not found
* @return the parsed boolean value, or the default value if the attribute is not found
*/
default Boolean parseBoolean(NamedNodeMap attributes, String name, Boolean defaultValue)
{
return parseBoolean(attributes.getNamedItem(name), defaultValue);
}
/**
* Parses a byte value from the given node.
* @param node the XML node to parse
* @param defaultValue the default value to return if the node is null
* @return the parsed byte value, or the default value if the node is null
*/
default Byte parseByte(Node node, Byte defaultValue)
{
return node != null ? Byte.decode(node.getNodeValue()) : defaultValue;
}
/**
* Parses a byte value from the given node.
* @param node the XML node to parse
* @return the parsed byte value, or null if the node is null
*/
default Byte parseByte(Node node)
{
return parseByte(node, null);
}
/**
* Parses a byte value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @return the parsed byte value, or null if the attribute is not found
*/
default Byte parseByte(NamedNodeMap attributes, String name)
{
return parseByte(attributes.getNamedItem(name));
}
/**
* Parses a byte value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @param defaultValue the default value to return if the attribute is not found
* @return the parsed byte value, or the default value if the attribute is not found
*/
default Byte parseByte(NamedNodeMap attributes, String name, Byte defaultValue)
{
return parseByte(attributes.getNamedItem(name), defaultValue);
}
/**
* Parses a short value from the given node.
* @param node the XML node to parse
* @param defaultValue the default value to return if the node is null
* @return the parsed short value, or the default value if the node is null
*/
default Short parseShort(Node node, Short defaultValue)
{
return node != null ? Short.decode(node.getNodeValue()) : defaultValue;
}
/**
* Parses a short value from the given node.
* @param node the XML node to parse
* @return the parsed short value, or null if the node is null
*/
default Short parseShort(Node node)
{
return parseShort(node, null);
}
/**
* Parses a short value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @return the parsed short value, or null if the attribute is not found
*/
default Short parseShort(NamedNodeMap attributes, String name)
{
return parseShort(attributes.getNamedItem(name));
}
/**
* Parses a short value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @param defaultValue the default value to return if the attribute is not found
* @return the parsed short value, or the default value if the attribute is not found
*/
default Short parseShort(NamedNodeMap attributes, String name, Short defaultValue)
{
return parseShort(attributes.getNamedItem(name), defaultValue);
}
/**
* Parses an int value from the given node.
* @param node the XML node to parse
* @param defaultValue the default value to return if the node is null
* @return the parsed int value, or the default value if the node is null
*/
default int parseInt(Node node, Integer defaultValue)
{
return node != null ? Integer.decode(node.getNodeValue()) : defaultValue;
}
/**
* Parses an int value from the given node, using -1 as the default value.
* @param node the XML node to parse
* @return the parsed int value, or -1 if the node is null
*/
default int parseInt(Node node)
{
return parseInt(node, -1);
}
/**
* Parses an Integer value from the given node.
* @param node the XML node to parse
* @param defaultValue the default value to return if the node is null
* @return the parsed Integer value, or the default value if the node is null
*/
default Integer parseInteger(Node node, Integer defaultValue)
{
return node != null ? Integer.decode(node.getNodeValue()) : defaultValue;
}
/**
* Parses an Integer value from the given node.
* @param node the XML node to parse
* @return the parsed Integer value, or null if the node is null
*/
default Integer parseInteger(Node node)
{
return parseInteger(node, null);
}
/**
* Parses an Integer value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @return the parsed Integer value, or null if the attribute is not found
*/
default Integer parseInteger(NamedNodeMap attributes, String name)
{
return parseInteger(attributes.getNamedItem(name));
}
/**
* Parses an Integer value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @param defaultValue the default value to return if the attribute is not found
* @return the parsed Integer value, or the default value if the attribute is not found
*/
default Integer parseInteger(NamedNodeMap attributes, String name, Integer defaultValue)
{
return parseInteger(attributes.getNamedItem(name), defaultValue);
}
/**
* Parses a Long value from the given node.
* @param node the XML node to parse
* @param defaultValue the default value to return if the node is null
* @return the parsed Long value, or the default value if the node is null
*/
default Long parseLong(Node node, Long defaultValue)
{
return node != null ? Long.decode(node.getNodeValue()) : defaultValue;
}
/**
* Parses a Long value from the given node.
* @param node the XML node to parse
* @return the parsed Long value, or null if the node is null
*/
default Long parseLong(Node node)
{
return parseLong(node, null);
}
/**
* Parses a Long value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @return the parsed Long value, or null if the attribute is not found
*/
default Long parseLong(NamedNodeMap attributes, String name)
{
return parseLong(attributes.getNamedItem(name));
}
/**
* Parses a Long value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @param defaultValue the default value to return if the attribute is not found
* @return the parsed Long value, or the default value if the attribute is not found
*/
default Long parseLong(NamedNodeMap attributes, String name, Long defaultValue)
{
return parseLong(attributes.getNamedItem(name), defaultValue);
}
/**
* Parses a float value from the given node.
* @param node the XML node to parse
* @param defaultValue the default value to return if the node is null
* @return the parsed float value, or the default value if the node is null
*/
default Float parseFloat(Node node, Float defaultValue)
{
return node != null ? Float.valueOf(node.getNodeValue()) : defaultValue;
}
/**
* Parses a float value from the given node.
* @param node the XML node to parse
* @return the parsed float value, or null if the node is null
*/
default Float parseFloat(Node node)
{
return parseFloat(node, null);
}
/**
* Parses a float value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @return the parsed float value, or null if the attribute is not found
*/
default Float parseFloat(NamedNodeMap attributes, String name)
{
return parseFloat(attributes.getNamedItem(name));
}
/**
* Parses a float value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @param defaultValue the default value to return if the attribute is not found
* @return the parsed float value, or the default value if the attribute is not found
*/
default Float parseFloat(NamedNodeMap attributes, String name, Float defaultValue)
{
return parseFloat(attributes.getNamedItem(name), defaultValue);
}
/**
* Parses a double value from the given node.
* @param node the XML node to parse
* @param defaultValue the default value to return if the node is null
* @return the parsed double value, or the default value if the node is null
*/
default Double parseDouble(Node node, Double defaultValue)
{
return node != null ? Double.valueOf(node.getNodeValue()) : defaultValue;
}
/**
* Parses a double value from the given node.
* @param node the XML node to parse
* @return the parsed double value, or null if the node is null
*/
default Double parseDouble(Node node)
{
return parseDouble(node, null);
}
/**
* Parses a double value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @return the parsed double value, or null if the attribute is not found
*/
default Double parseDouble(NamedNodeMap attributes, String name)
{
return parseDouble(attributes.getNamedItem(name));
}
/**
* Parses a double value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @param defaultValue the default value to return if the attribute is not found
* @return the parsed double value, or the default value if the attribute is not found
*/
default Double parseDouble(NamedNodeMap attributes, String name, Double defaultValue)
{
return parseDouble(attributes.getNamedItem(name), defaultValue);
}
/**
* Parses a String value from the given node.
* @param node the XML node to parse
* @param defaultValue the default value to return if the node is null
* @return the parsed String value, or the default value if the node is null
*/
default String parseString(Node node, String defaultValue)
{
return node != null ? node.getNodeValue() : defaultValue;
}
/**
* Parses a String value from the given node.
* @param node the XML node to parse
* @return the parsed String value, or null if the node is null
*/
default String parseString(Node node)
{
return parseString(node, null);
}
/**
* Parses a String value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @return the parsed String value, or null if the attribute is not found
*/
default String parseString(NamedNodeMap attributes, String name)
{
return parseString(attributes.getNamedItem(name));
}
/**
* Parses a String value from the specified attribute in the given attributes map.
* @param attributes the attributes map
* @param name the name of the attribute to parse
* @param defaultValue the default value to return if the attribute is not found
* @return the parsed String value, or the default value if the attribute is not found
*/
default String parseString(NamedNodeMap attributes, String name, String defaultValue)
{
return parseString(attributes.getNamedItem(name), defaultValue);
}
/**
* Parses an enum value from the given node.
* @param <T> the enum type
* @param node the XML node to parse
* @param enumClass the class of the enum type
* @param defaultValue the default value to return if parsing fails
* @return the parsed enum value, or the default value if parsing fails
*/
default <T extends Enum<T>> T parseEnum(Node node, Class<T> enumClass, T defaultValue)
{
if (node == null)
{
return defaultValue;
}
try
{
return Enum.valueOf(enumClass, node.getNodeValue());
}
catch (IllegalArgumentException e)
{
LOGGER.warning("Invalid value for node: " + node.getNodeName() + ", specified value: " + node.getNodeValue() + " should be an enum of type \"" + enumClass.getSimpleName() + "\". Using default value: " + defaultValue);
return defaultValue;
}
}
/**
* Parses an enum value from the given node.
* @param <T> the enum type
* @param node the XML node to parse
* @param enumClass the class of the enum type
* @return the parsed enum value, or null if parsing fails
*/
default <T extends Enum<T>> T parseEnum(Node node, Class<T> enumClass)
{
return parseEnum(node, enumClass, null);
}
/**
* Parses an enum value from the specified attribute in the given attributes map.
* @param <T> the enum type
* @param attributes the attributes map
* @param enumClass the class of the enum type
* @param name the name of the attribute to parse
* @return the parsed enum value, or null if the attribute is not found or parsing fails
*/
default <T extends Enum<T>> T parseEnum(NamedNodeMap attributes, Class<T> enumClass, String name)
{
return parseEnum(attributes.getNamedItem(name), enumClass);
}
/**
* Parses an enum value from the specified attribute in the given attributes map.
* @param <T> the enum type
* @param attributes the attributes map
* @param enumClass the class of the enum type
* @param name the name of the attribute to parse
* @param defaultValue the default value to return if parsing fails
* @return the parsed enum value, or the default value if parsing fails
*/
default <T extends Enum<T>> T parseEnum(NamedNodeMap attributes, Class<T> enumClass, String name, T defaultValue)
{
return parseEnum(attributes.getNamedItem(name), enumClass, defaultValue);
}
/**
* Parses all attributes from the given node into a map.
* @param node the XML node to parse
* @return a map containing all attributes of the node as key-value pairs
*/
default Map<String, Object> parseAttributes(Node node)
{
final NamedNodeMap attributes = node.getAttributes();
final Map<String, Object> attributeMap = new LinkedHashMap<>();
for (int i = 0; i < attributes.getLength(); i++)
{
final Node attribute = attributes.item(i);
attributeMap.put(attribute.getNodeName(), attribute.getNodeValue());
}
return attributeMap;
}
/**
* Applies an action to each child node.
* @param node the parent XML node
* @param action the action to perform on each child node
*/
default void forEach(Node node, Consumer<Node> action)
{
forEach(node, _ -> true, action);
}
/**
* Applies an action to each child node with a matching name.
* @param node the parent XML node
* @param nodeName the name of the child nodes to match
* @param action the action to perform on each matching child node
*/
default void forEach(Node node, String nodeName, Consumer<Node> action)
{
forEach(node, child -> nodeName.equalsIgnoreCase(child.getNodeName()), action);
}
/**
* Applies an action to each child node that meets a specified filter condition.
* @param node the parent XML node
* @param filter a filter to select specific child nodes
* @param action the action to perform on each matching child node
*/
default void forEach(Node node, Predicate<Node> filter, Consumer<Node> action)
{
final NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++)
{
final Node childNode = children.item(i);
if (filter.test(childNode))
{
action.accept(childNode);
}
}
}
/**
* Checks if the specified file is a valid XML file.
* @param file the file to check
* @return true if the file is an XML file and exists, false otherwise
*/
default boolean isValidXmlFile(File file)
{
return (file != null) && file.isFile() && file.getName().toLowerCase().endsWith(".xml");
}
/**
* Checks if XML validation is enabled.
* @return {@code true} if validation is enabled, {@code false} otherwise.
*/
default boolean isValidating()
{
return true;
}
/**
* Checks if a node is of element type.
* @param node the XML node to check
* @return {@code true} if the node is an element, {@code false} otherwise
*/
static boolean isNode(Node node)
{
return node.getNodeType() == Node.ELEMENT_NODE;
}
}
@@ -0,0 +1,168 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.util;
import java.util.concurrent.ThreadLocalRandom;
/**
* @author Mobius
* @since September 15th 2018
*/
public class Rnd
{
private static final int MINIMUM_POSITIVE_INT = 1;
private static final long MINIMUM_POSITIVE_LONG = 1L;
private static final float MINIMUM_POSITIVE_FLOAT = Float.intBitsToFloat(0x1);
private static final double MINIMUM_POSITIVE_DOUBLE = Double.longBitsToDouble(0x1L);
/**
* @return a random boolean value.
*/
public static boolean nextBoolean()
{
return ThreadLocalRandom.current().nextBoolean();
}
/**
* Generates random bytes and places them into a user-supplied byte array. The number of random bytes produced is equal to the length of the byte array.
* @param bytes the byte array to fill with random bytes.
*/
public static void nextBytes(byte[] bytes)
{
ThreadLocalRandom.current().nextBytes(bytes);
}
/**
* @param bound (int)
* @return a random int value between zero (inclusive) and the specified bound (exclusive).
*/
public static int get(int bound)
{
return bound <= 0 ? 0 : ThreadLocalRandom.current().nextInt(bound);
}
/**
* @param origin (int)
* @param bound (int)
* @return a random int value between the specified origin (inclusive) and the specified bound (inclusive).
*/
public static int get(int origin, int bound)
{
return origin >= bound ? origin : ThreadLocalRandom.current().nextInt(origin, bound == Integer.MAX_VALUE ? bound : bound + MINIMUM_POSITIVE_INT);
}
/**
* @return a random int value.
*/
public static int nextInt()
{
return ThreadLocalRandom.current().nextInt();
}
/**
* @param bound (long)
* @return a random long value between zero (inclusive) and the specified bound (exclusive).
*/
public static long get(long bound)
{
return bound <= 0 ? 0 : ThreadLocalRandom.current().nextLong(bound);
}
/**
* @param origin (long)
* @param bound (long)
* @return a random long value between the specified origin (inclusive) and the specified bound (inclusive).
*/
public static long get(long origin, long bound)
{
return origin >= bound ? origin : ThreadLocalRandom.current().nextLong(origin, bound == Long.MAX_VALUE ? bound : bound + MINIMUM_POSITIVE_LONG);
}
/**
* @return a random long value.
*/
public static long nextLong()
{
return ThreadLocalRandom.current().nextLong();
}
/**
* @param bound (float)
* @return a random float value between zero (inclusive) and the specified bound (exclusive).
*/
public static float get(float bound)
{
return bound <= 0 ? 0 : ThreadLocalRandom.current().nextFloat(bound);
}
/**
* @param origin (float)
* @param bound (float)
* @return a random float value between the specified origin (inclusive) and the specified bound (inclusive).
*/
public static float get(float origin, float bound)
{
return origin >= bound ? origin : ThreadLocalRandom.current().nextFloat(origin, bound == Float.MAX_VALUE ? bound : bound + MINIMUM_POSITIVE_FLOAT);
}
/**
* @return a random float value between zero (inclusive) and one (exclusive).
*/
public static float nextFloat()
{
return ThreadLocalRandom.current().nextFloat();
}
/**
* @param bound (double)
* @return a random double value between zero (inclusive) and the specified bound (exclusive).
*/
public static double get(double bound)
{
return bound <= 0 ? 0 : ThreadLocalRandom.current().nextDouble(bound);
}
/**
* @param origin (double)
* @param bound (double)
* @return a random double value between the specified origin (inclusive) and the specified bound (inclusive).
*/
public static double get(double origin, double bound)
{
return origin >= bound ? origin : ThreadLocalRandom.current().nextDouble(origin, bound == Double.MAX_VALUE ? bound : bound + MINIMUM_POSITIVE_DOUBLE);
}
/**
* @return a random double value between zero (inclusive) and one (exclusive).
*/
public static double nextDouble()
{
return ThreadLocalRandom.current().nextDouble();
}
/**
* @return the next random, Gaussian ("normally") distributed double value with mean 0.0 and standard deviation 1.0 from this random number generator's sequence.
*/
public static double nextGaussian()
{
return ThreadLocalRandom.current().nextGaussian();
}
}
@@ -0,0 +1,499 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.util;
import java.util.StringJoiner;
import java.util.StringTokenizer;
/**
* Utility class for String operations, providing methods to efficiently build and format strings.
* @author Mobius
*/
public class StringUtil
{
/**
* Appends a string to a given StringBuilder.<br>
* This method avoids the unnecessary use of `public static void append(StringBuilder sb, String... args)`,<br>
* which could introduce overhead due to varargs processing when only a single argument is appended.
* @param sb the StringBuilder to append to
* @param arg the string to append
*/
public static void append(StringBuilder sb, String arg)
{
sb.append(arg);
}
/**
* Appends multiple strings to a given StringBuilder.
* @param sb the StringBuilder to append to
* @param args the strings to append
*/
public static void append(StringBuilder sb, String... args)
{
// Directly calculate the required capacity and ensure it in one step.
int totalLength = sb.length();
for (String arg : args)
{
totalLength += (arg != null ? arg.length() : 4);
}
sb.ensureCapacity(totalLength);
// Append each argument.
for (String arg : args)
{
sb.append(arg);
}
}
/**
* Appends multiple objects to a given StringBuilder.
* @param sb the StringBuilder to append to
* @param args the objects to append
*/
public static void append(StringBuilder sb, Object... args)
{
// Calculate the total length and store converted strings.
int totalLength = sb.length();
final String[] strings = new String[args.length];
for (int i = 0; i < args.length; i++)
{
strings[i] = String.valueOf(args[i]);
totalLength += strings[i].length();
}
sb.ensureCapacity(totalLength);
// Append each stored string.
for (String string : strings)
{
sb.append(string);
}
}
/**
* Concatenates multiple strings into a single string.
* @param args the strings to concatenate
* @return the concatenated string
*/
public static String concat(String... args)
{
// Calculate the total length of all strings.
int totalLength = 0;
for (String arg : args)
{
totalLength += (arg != null ? arg.length() : 4);
}
// Append each argument.
final StringBuilder sb = new StringBuilder(totalLength);
for (String arg : args)
{
sb.append(arg);
}
return sb.toString();
}
/**
* Concatenates multiple objects into a single string.
* @param args the objects to concatenate
* @return the concatenated string
*/
public static String concat(Object... args)
{
// Calculate the total length and store converted strings.
int totalLength = 0;
final String[] strings = new String[args.length];
for (int i = 0; i < args.length; i++)
{
strings[i] = String.valueOf(args[i]);
totalLength += strings[i].length();
}
// Append each stored string.
final StringBuilder sb = new StringBuilder(totalLength);
for (String string : strings)
{
sb.append(string);
}
return sb.toString();
}
/**
* Concatenates elements in an Iterable into a single string with a specified delimiter.
* @param <T> the type of elements in the iterable
* @param items the iterable collection of elements to join
* @param delimiter the delimiter to place between elements
* @return a single string with each element separated by the delimiter
*/
public static <T> String implode(Iterable<T> items, String delimiter)
{
final StringJoiner joiner = new StringJoiner(delimiter);
for (T item : items)
{
joiner.add(item.toString());
}
return joiner.toString();
}
/**
* Concatenates elements in an array into a single string with a specified delimiter.
* @param <T> the type of elements in the array
* @param array the array of elements to join
* @param delimiter the delimiter to place between elements
* @return a single string with each element separated by the delimiter
*/
public static <T> String implode(T[] array, String delimiter)
{
final StringJoiner joiner = new StringJoiner(delimiter);
for (T element : array)
{
joiner.add(element.toString());
}
return joiner.toString();
}
/**
* Capitalizes the first letter of a given string and converts the rest to lowercase.
* @param text the input string to be formatted
* @return the formatted string with the first letter capitalized and the rest in lowercase, or the original string if it is null or empty
*/
public static String capitalizeFirst(String text)
{
// Return the original if it's null or empty.
if ((text == null) || text.isEmpty())
{
return text;
}
// Capitalize the first letter and set the remaining letters to lowercase.
return Character.toUpperCase(text.charAt(0)) + text.substring(1).toLowerCase();
}
/**
* Splits a camelCase or PascalCase string into words separated by a space.
* @param text the string to split into words
* @return a string with words separated by a space, or the original text if null or empty
*/
public static String separateWords(String text)
{
if ((text == null) || text.isEmpty())
{
return text;
}
final StringBuilder result = new StringBuilder();
final char[] chars = text.toCharArray();
for (int i = 0; i < chars.length; i++)
{
final char current = chars[i];
// Check if the current character is uppercase and it's not the first character.
if (Character.isUpperCase(current) && (i > 0) && Character.isLowerCase(chars[i - 1]))
{
result.append(' ');
}
result.append(current);
}
return result.toString();
}
/**
* Converts an enum constant's name to a formatted string with proper casing.<br>
* For example, an enum constant named "ENUM_CONSTANT_NAME" will be formatted as "Enum Constant Name".
* @param enumeration the enum constant to format
* @return a formatted string with each word capitalized
*/
public static String enumToString(Enum<?> enumeration)
{
final String name = enumeration.name().toLowerCase();
final StringBuilder sb = new StringBuilder(name.length());
boolean capitalizeNext = true;
for (int i = 0; i < name.length(); i++)
{
char c = name.charAt(i);
if (c == '_')
{
sb.append(' ');
capitalizeNext = true;
}
else if (capitalizeNext)
{
sb.append(Character.toUpperCase(c));
capitalizeNext = false;
}
else
{
sb.append(c);
}
}
return sb.toString();
}
/**
* Parse a string value to the appropriate data type.
* @param value the string value to parse
* @return the parsed value as Boolean, Long, Integer, Float, Double, or String
*/
public static Object parseValue(String value)
{
if (value == null)
{
return null;
}
final String val = value.trim();
if (val.equalsIgnoreCase("true"))
{
return Boolean.TRUE;
}
else if (val.equalsIgnoreCase("false"))
{
return Boolean.FALSE;
}
else
{
try
{
// Try Long first (to avoid Integer parsing of larger numbers).
return Long.valueOf(val);
}
catch (NumberFormatException e1)
{
try
{
// Try Integer.
return Integer.valueOf(val);
}
catch (NumberFormatException e2)
{
try
{
// Try Float before Double to get single-precision when possible.
return Float.valueOf(val);
}
catch (NumberFormatException e3)
{
try
{
// Try Double.
return Double.valueOf(val);
}
catch (NumberFormatException e4)
{
return val;
}
}
}
}
}
}
/**
* Parses a string as an integer, returning a default value if parsing fails.
* @param text the string to parse
* @param defaultValue the value to return if parsing fails
* @return the parsed integer, or the default value if parsing fails
*/
public static int parseInt(String text, int defaultValue)
{
try
{
return Integer.parseInt(text);
}
catch (NumberFormatException e)
{
return defaultValue;
}
}
/**
* Parses the next token from a StringTokenizer as an integer, returning a default value if parsing fails or if there are no more tokens.
* @param tokenizer the StringTokenizer containing tokens
* @param defaultValue the value to return if parsing fails or if there are no more tokens
* @return the parsed integer, or the default value if parsing fails or if there are no tokens
*/
public static int parseNextInt(StringTokenizer tokenizer, int defaultValue)
{
if (tokenizer.hasMoreTokens())
{
try
{
final String value = tokenizer.nextToken().trim();
return Integer.parseInt(value);
}
catch (NumberFormatException e)
{
// Parsing failed, fall back to default.
}
}
return defaultValue;
}
/**
* Checks if the given text contains only letters and/or numbers.
* @param text the text to check
* @return {@code true} if {@code text} contains only alphanumeric characters, {@code false} otherwise
*/
public static boolean isAlphaNumeric(String text)
{
if ((text == null) || text.isEmpty())
{
return false;
}
for (int i = 0; i < text.length(); i++)
{
if (!Character.isLetterOrDigit(text.charAt(i)))
{
return false;
}
}
return true;
}
/**
* Checks if the given text contains only digits.
* @param text the text to check
* @return {@code true} if {@code text} contains only numbers, {@code false} otherwise
*/
public static boolean isNumeric(String text)
{
if ((text == null) || text.isEmpty())
{
return false;
}
for (int i = 0; i < text.length(); i++)
{
if (!Character.isDigit(text.charAt(i)))
{
return false;
}
}
return true;
}
/**
* Checks if the given text represents a valid integer.
* @param text the text to check
* @return {@code true} if {@code text} is an integer, {@code false} otherwise
*/
public static boolean isInteger(String text)
{
if ((text == null) || text.isEmpty())
{
return false;
}
try
{
Integer.parseInt(text);
return true;
}
catch (NumberFormatException e)
{
return false;
}
}
/**
* Checks if the given text represents a valid float.
* @param text the text to check
* @return {@code true} if {@code text} is a float, {@code false} otherwise
*/
public static boolean isFloat(String text)
{
if ((text == null) || text.isEmpty())
{
return false;
}
try
{
Float.parseFloat(text);
return true;
}
catch (NumberFormatException e)
{
return false;
}
}
/**
* Checks if the given text represents a valid double.
* @param text the text to check
* @return {@code true} if {@code text} is a double, {@code false} otherwise
*/
public static boolean isDouble(String text)
{
if ((text == null) || text.isEmpty())
{
return false;
}
try
{
Double.parseDouble(text);
return true;
}
catch (NumberFormatException e)
{
return false;
}
}
/**
* Checks if the given text matches any constant in the specified enum type.
* @param name the text to check
* @param enumType the class of the enum
* @param <T> the type of the enum
* @return {@code true} if {@code text} is a valid enum constant, {@code false} otherwise
*/
public static <T extends Enum<T>> boolean isEnum(String name, Class<T> enumType)
{
if ((name == null) || name.isEmpty())
{
return false;
}
try
{
Enum.valueOf(enumType, name);
return true;
}
catch (IllegalArgumentException e)
{
return false;
}
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.commons.util;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.StringJoiner;
/**
* Utility class for handling and formatting stack traces.
* @author Mobius
*/
public class TraceUtil
{
/**
* Returns the stack trace of a throwable as a String.
* @param throwable the throwable whose stack trace is needed
* @return the stack trace as a String
*/
public static String getStackTrace(Throwable throwable)
{
final StringWriter writer = new StringWriter();
throwable.printStackTrace(new PrintWriter(writer));
return writer.toString();
}
/**
* Constructs a string from an array of stack trace elements, each element on a new line.
* @param stackTraceElements the array of stack trace elements
* @return a String containing the stack trace elements, each on a new line
*/
public static String getTraceString(StackTraceElement[] stackTraceElements)
{
final StringJoiner joiner = new StringJoiner(System.lineSeparator());
for (StackTraceElement stackTraceElement : stackTraceElements)
{
joiner.add(stackTraceElement.toString());
}
return joiner.toString();
}
}
@@ -0,0 +1,493 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver;
import java.awt.Toolkit;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import org.l2jmobius.commons.config.InterfaceConfig;
import org.l2jmobius.commons.database.DatabaseFactory;
import org.l2jmobius.commons.network.ConnectionManager;
import org.l2jmobius.commons.threads.ThreadPool;
import org.l2jmobius.commons.time.TimeUtil;
import org.l2jmobius.commons.util.StringUtil;
import org.l2jmobius.gameserver.cache.HtmCache;
import org.l2jmobius.gameserver.config.ConfigLoader;
import org.l2jmobius.gameserver.config.DevelopmentConfig;
import org.l2jmobius.gameserver.config.GeneralConfig;
import org.l2jmobius.gameserver.config.ServerConfig;
import org.l2jmobius.gameserver.config.custom.CustomMailManagerConfig;
import org.l2jmobius.gameserver.config.custom.MultilingualSupportConfig;
import org.l2jmobius.gameserver.config.custom.OfflinePlayConfig;
import org.l2jmobius.gameserver.config.custom.OfflineTradeConfig;
import org.l2jmobius.gameserver.config.custom.PremiumSystemConfig;
import org.l2jmobius.gameserver.config.custom.SellBuffsConfig;
import org.l2jmobius.gameserver.config.custom.WeddingConfig;
import org.l2jmobius.gameserver.data.AugmentationData;
import org.l2jmobius.gameserver.data.MerchantPriceConfigTable;
import org.l2jmobius.gameserver.data.SchemeBufferTable;
import org.l2jmobius.gameserver.data.sql.AnnouncementsTable;
import org.l2jmobius.gameserver.data.sql.CharInfoTable;
import org.l2jmobius.gameserver.data.sql.CharSummonTable;
import org.l2jmobius.gameserver.data.sql.ClanHallTable;
import org.l2jmobius.gameserver.data.sql.ClanTable;
import org.l2jmobius.gameserver.data.sql.CrestTable;
import org.l2jmobius.gameserver.data.sql.OfflinePlayTable;
import org.l2jmobius.gameserver.data.sql.OfflineTraderTable;
import org.l2jmobius.gameserver.data.xml.AdminData;
import org.l2jmobius.gameserver.data.xml.ArmorSetData;
import org.l2jmobius.gameserver.data.xml.BuyListData;
import org.l2jmobius.gameserver.data.xml.CategoryData;
import org.l2jmobius.gameserver.data.xml.ClassListData;
import org.l2jmobius.gameserver.data.xml.CubicData;
import org.l2jmobius.gameserver.data.xml.DoorData;
import org.l2jmobius.gameserver.data.xml.DynamicExpRateData;
import org.l2jmobius.gameserver.data.xml.EnchantItemData;
import org.l2jmobius.gameserver.data.xml.EnchantItemGroupsData;
import org.l2jmobius.gameserver.data.xml.EnchantItemHPBonusData;
import org.l2jmobius.gameserver.data.xml.EnchantSkillTreeData;
import org.l2jmobius.gameserver.data.xml.ExperienceData;
import org.l2jmobius.gameserver.data.xml.ExperienceLossData;
import org.l2jmobius.gameserver.data.xml.FenceData;
import org.l2jmobius.gameserver.data.xml.FishData;
import org.l2jmobius.gameserver.data.xml.FishingMonstersData;
import org.l2jmobius.gameserver.data.xml.FishingRodsData;
import org.l2jmobius.gameserver.data.xml.HennaData;
import org.l2jmobius.gameserver.data.xml.HitConditionBonusData;
import org.l2jmobius.gameserver.data.xml.InitialEquipmentData;
import org.l2jmobius.gameserver.data.xml.InitialShortcutData;
import org.l2jmobius.gameserver.data.xml.ItemData;
import org.l2jmobius.gameserver.data.xml.KarmaLossData;
import org.l2jmobius.gameserver.data.xml.LevelUpCrystalData;
import org.l2jmobius.gameserver.data.xml.MapRegionData;
import org.l2jmobius.gameserver.data.xml.MultisellData;
import org.l2jmobius.gameserver.data.xml.NpcData;
import org.l2jmobius.gameserver.data.xml.NpcNameLocalisationData;
import org.l2jmobius.gameserver.data.xml.OptionData;
import org.l2jmobius.gameserver.data.xml.PetDataTable;
import org.l2jmobius.gameserver.data.xml.PetSkillData;
import org.l2jmobius.gameserver.data.xml.PlayerTemplateData;
import org.l2jmobius.gameserver.data.xml.RecipeData;
import org.l2jmobius.gameserver.data.xml.SendMessageLocalisationData;
import org.l2jmobius.gameserver.data.xml.SiegeScheduleData;
import org.l2jmobius.gameserver.data.xml.SkillData;
import org.l2jmobius.gameserver.data.xml.SkillLearnData;
import org.l2jmobius.gameserver.data.xml.SkillTreeData;
import org.l2jmobius.gameserver.data.xml.SpawnData;
import org.l2jmobius.gameserver.data.xml.StaticObjectData;
import org.l2jmobius.gameserver.data.xml.TeleporterData;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.entity.groups.matching.PartyMatchRoomList;
import org.l2jmobius.gameserver.entity.groups.matching.PartyMatchWaitingList;
import org.l2jmobius.gameserver.entity.spawns.AutoSpawnHandler;
import org.l2jmobius.gameserver.geoengine.GeoEngine;
import org.l2jmobius.gameserver.handler.EffectHandler;
import org.l2jmobius.gameserver.managers.AntiFeedManager;
import org.l2jmobius.gameserver.managers.BoatManager;
import org.l2jmobius.gameserver.managers.CHSiegeManager;
import org.l2jmobius.gameserver.managers.CaptchaManager;
import org.l2jmobius.gameserver.managers.CastleManager;
import org.l2jmobius.gameserver.managers.CastleManorManager;
import org.l2jmobius.gameserver.managers.ClanHallAuctionManager;
import org.l2jmobius.gameserver.managers.CoupleManager;
import org.l2jmobius.gameserver.managers.CursedWeaponsManager;
import org.l2jmobius.gameserver.managers.CustomMailManager;
import org.l2jmobius.gameserver.managers.DailyResetManager;
import org.l2jmobius.gameserver.managers.DayNightSpawnManager;
import org.l2jmobius.gameserver.managers.DimensionalRiftManager;
import org.l2jmobius.gameserver.managers.EventDropManager;
import org.l2jmobius.gameserver.managers.FakePlayerChatManager;
import org.l2jmobius.gameserver.managers.FishingChampionshipManager;
import org.l2jmobius.gameserver.managers.GlobalVariablesManager;
import org.l2jmobius.gameserver.managers.GrandBossManager;
import org.l2jmobius.gameserver.managers.IdManager;
import org.l2jmobius.gameserver.managers.InstanceManager;
import org.l2jmobius.gameserver.managers.ItemsOnGroundManager;
import org.l2jmobius.gameserver.managers.MercTicketManager;
import org.l2jmobius.gameserver.managers.PcCafePointsManager;
import org.l2jmobius.gameserver.managers.PetitionManager;
import org.l2jmobius.gameserver.managers.PrecautionaryRestartManager;
import org.l2jmobius.gameserver.managers.PremiumManager;
import org.l2jmobius.gameserver.managers.PunishmentManager;
import org.l2jmobius.gameserver.managers.RaidBossPointsManager;
import org.l2jmobius.gameserver.managers.RaidBossSpawnManager;
import org.l2jmobius.gameserver.managers.ScriptManager;
import org.l2jmobius.gameserver.managers.SellBuffsManager;
import org.l2jmobius.gameserver.managers.ServerRestartManager;
import org.l2jmobius.gameserver.managers.SiegeManager;
import org.l2jmobius.gameserver.managers.WalkingManager;
import org.l2jmobius.gameserver.managers.ZoneManager;
import org.l2jmobius.gameserver.managers.games.LotteryManager;
import org.l2jmobius.gameserver.managers.games.MonsterRaceManager;
import org.l2jmobius.gameserver.mechanics.events.EventDispatcher;
import org.l2jmobius.gameserver.mechanics.events.EventType;
import org.l2jmobius.gameserver.mechanics.events.holders.OnServerStart;
import org.l2jmobius.gameserver.mechanics.olympiad.Hero;
import org.l2jmobius.gameserver.mechanics.olympiad.Olympiad;
import org.l2jmobius.gameserver.mechanics.sevensigns.SevenSigns;
import org.l2jmobius.gameserver.mechanics.sevensigns.SevenSignsFestival;
import org.l2jmobius.gameserver.network.GameClient;
import org.l2jmobius.gameserver.network.GamePacketHandler;
import org.l2jmobius.gameserver.network.SystemMessageId;
import org.l2jmobius.gameserver.scripting.ScriptEngine;
import org.l2jmobius.gameserver.taskmanagers.GameTimeTaskManager;
import org.l2jmobius.gameserver.taskmanagers.ItemLifeTimeTaskManager;
import org.l2jmobius.gameserver.taskmanagers.ItemsAutoDestroyTaskManager;
import org.l2jmobius.gameserver.ui.Gui;
import org.l2jmobius.gameserver.util.DeadlockWatcher;
public class GameServer
{
private static final Logger LOGGER = Logger.getLogger(GameServer.class.getName());
private static final long START_TIME = System.currentTimeMillis();
private long _sectionStartTime = START_TIME;
private String _previousSectionName = null;
public GameServer() throws Exception
{
// GUI
InterfaceConfig.load();
if (InterfaceConfig.ENABLE_GUI)
{
System.out.println("GameServer: Running in GUI mode.");
new Gui();
}
// Create log folder
final File logFolder = new File(".", "log");
logFolder.mkdir();
// Create input stream for log file -- or store file data into memory.
try (InputStream is = new FileInputStream(new File("./log.cfg")))
{
LogManager.getLogManager().readConfiguration(is);
}
// Initialize config
ConfigLoader.init();
printSection("Database");
DatabaseFactory.init();
printSection("ThreadPool");
ThreadPool.init();
// Start game time task manager early.
GameTimeTaskManager.getInstance();
printSection("IdManager");
IdManager.getInstance();
printSection("Scripting Engine");
EventDispatcher.getInstance();
ScriptEngine.getInstance();
printSection("World");
InstanceManager.getInstance();
World.init();
MapRegionData.getInstance();
AnnouncementsTable.getInstance();
GlobalVariablesManager.getInstance();
printSection("Data");
CategoryData.getInstance();
CubicData.getInstance();
DynamicExpRateData.getInstance();
printSection("Skills");
EffectHandler.getInstance().executeScript();
EnchantSkillTreeData.getInstance();
SkillTreeData.getInstance();
SkillData.getInstance();
PetSkillData.getInstance();
printSection("Items");
ItemData.getInstance();
EnchantItemGroupsData.getInstance();
EnchantItemData.getInstance();
OptionData.getInstance();
EnchantItemHPBonusData.getInstance();
MerchantPriceConfigTable.getInstance().loadInstances();
BuyListData.getInstance();
MultisellData.getInstance();
RecipeData.getInstance();
ArmorSetData.getInstance();
FishData.getInstance();
FishingMonstersData.getInstance();
FishingRodsData.getInstance();
HennaData.getInstance();
PcCafePointsManager.getInstance();
ItemLifeTimeTaskManager.getInstance();
printSection("Characters");
ClassListData.getInstance();
InitialEquipmentData.getInstance();
InitialShortcutData.getInstance();
ExperienceData.getInstance();
ExperienceLossData.getInstance();
KarmaLossData.getInstance();
HitConditionBonusData.getInstance();
PlayerTemplateData.getInstance();
CharInfoTable.getInstance();
AdminData.getInstance();
RaidBossPointsManager.getInstance();
PetDataTable.getInstance();
CharSummonTable.getInstance().init();
CaptchaManager.getInstance();
if (PremiumSystemConfig.PREMIUM_SYSTEM_ENABLED)
{
LOGGER.info("PremiumManager: Premium system is enabled.");
PremiumManager.getInstance();
}
printSection("Clans");
ClanTable.getInstance();
CHSiegeManager.getInstance();
ClanHallTable.getInstance();
ClanHallAuctionManager.getInstance();
printSection("Geodata");
GeoEngine.getInstance();
printSection("NPCs");
DoorData.getInstance();
FenceData.getInstance();
SkillLearnData.getInstance();
NpcData.getInstance();
LevelUpCrystalData.getInstance();
FakePlayerChatManager.getInstance();
WalkingManager.getInstance();
StaticObjectData.getInstance();
CastleManager.getInstance().loadInstances();
SchemeBufferTable.getInstance();
ZoneManager.getInstance();
GrandBossManager.getInstance().initZones();
EventDropManager.getInstance();
printSection("Olympiad");
Olympiad.getInstance();
Hero.getInstance();
printSection("Seven Signs");
SevenSigns.getInstance();
// Call to load caches.
printSection("Cache");
HtmCache.getInstance();
CrestTable.getInstance();
TeleporterData.getInstance();
PartyMatchWaitingList.getInstance();
PartyMatchRoomList.getInstance();
PetitionManager.getInstance();
AugmentationData.getInstance();
CursedWeaponsManager.getInstance();
if (SellBuffsConfig.SELLBUFF_ENABLED)
{
SellBuffsManager.getInstance();
}
if (MultilingualSupportConfig.MULTILANG_ENABLE)
{
SystemMessageId.loadLocalisations();
SendMessageLocalisationData.getInstance();
NpcNameLocalisationData.getInstance();
}
printSection("Scripts");
ScriptManager.getInstance();
BoatManager.getInstance();
try
{
LOGGER.info("Loading server scripts...");
ScriptEngine.getInstance().executeScript(ScriptEngine.MASTER_HANDLER_FILE);
ScriptEngine.getInstance().executeScriptList();
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Failed to execute script list!", e);
}
SpawnData.getInstance();
DayNightSpawnManager.getInstance().trim().notifyChangeMode();
DimensionalRiftManager.getInstance();
RaidBossSpawnManager.getInstance();
printSection("Siege");
SiegeManager.getInstance().getSieges();
CastleManager.getInstance().activateInstances();
SiegeScheduleData.getInstance();
MerchantPriceConfigTable.getInstance().updateReferences();
CastleManorManager.getInstance();
MercTicketManager.getInstance();
ScriptManager.getInstance().report();
if (GeneralConfig.SAVE_DROPPED_ITEM)
{
ItemsOnGroundManager.getInstance();
}
if ((GeneralConfig.AUTODESTROY_ITEM_AFTER > 0) || (GeneralConfig.HERB_AUTO_DESTROY_TIME > 0))
{
ItemsAutoDestroyTaskManager.getInstance();
}
MonsterRaceManager.getInstance();
LotteryManager.getInstance();
SevenSigns.getInstance().spawnSevenSignsNPC();
SevenSignsFestival.getInstance();
AutoSpawnHandler.getInstance();
LOGGER.info("AutoSpawnHandler: Loaded " + AutoSpawnHandler.getInstance().size() + " handlers in total.");
if (WeddingConfig.ALLOW_WEDDING)
{
CoupleManager.getInstance();
}
if (GeneralConfig.ALT_FISH_CHAMPIONSHIP_ENABLED)
{
FishingChampionshipManager.getInstance();
}
DailyResetManager.getInstance();
AntiFeedManager.getInstance().registerEvent(AntiFeedManager.GAME_ID);
if (OfflinePlayConfig.ENABLE_OFFLINE_PLAY_COMMAND)
{
AntiFeedManager.getInstance().registerEvent(AntiFeedManager.OFFLINE_PLAY);
}
if (CustomMailManagerConfig.CUSTOM_MAIL_MANAGER_ENABLED)
{
CustomMailManager.getInstance();
}
if (EventDispatcher.getInstance().hasListener(EventType.ON_SERVER_START))
{
EventDispatcher.getInstance().notifyEventAsync(new OnServerStart());
}
PunishmentManager.getInstance();
Runtime.getRuntime().addShutdownHook(Shutdown.getInstance());
LOGGER.info("IdManager: Free ObjectID's remaining: " + IdManager.getInstance().getAvailableIdCount());
if ((OfflineTradeConfig.OFFLINE_TRADE_ENABLE || OfflineTradeConfig.OFFLINE_CRAFT_ENABLE) && OfflineTradeConfig.RESTORE_OFFLINERS)
{
OfflineTraderTable.getInstance().restoreOfflineTraders();
}
if (OfflinePlayConfig.ENABLE_OFFLINE_PLAY_COMMAND && OfflinePlayConfig.RESTORE_AUTO_PLAY_OFFLINERS)
{
OfflinePlayTable.getInstance().restoreOfflinePlayers();
}
if (ServerConfig.SERVER_RESTART_SCHEDULE_ENABLED)
{
ServerRestartManager.getInstance();
}
if (ServerConfig.PRECAUTIONARY_RESTART_ENABLED)
{
PrecautionaryRestartManager.getInstance();
}
if (ServerConfig.DEADLOCK_WATCHER)
{
final DeadlockWatcher deadlockWatcher = new DeadlockWatcher(Duration.ofSeconds(ServerConfig.DEADLOCK_CHECK_INTERVAL), () ->
{
if (ServerConfig.RESTART_ON_DEADLOCK)
{
World.broadcastToAllOnlinePlayers("Server has stability issues - restarting now.");
Shutdown.getInstance().startShutdown(null, 60, true);
}
});
deadlockWatcher.setDaemon(true);
deadlockWatcher.start();
}
System.gc();
final long totalMem = Runtime.getRuntime().maxMemory() / 1048576;
LOGGER.info(StringUtil.concat(getClass().getSimpleName(), ": Started, using ", getUsedMemoryMB(), " of ", totalMem, " MB total memory."));
LOGGER.info(StringUtil.concat(getClass().getSimpleName(), ": Maximum number of connected players is ", ServerConfig.MAXIMUM_ONLINE_USERS, "."));
LOGGER.info(StringUtil.concat(getClass().getSimpleName(), ": Server loaded in ", ((System.currentTimeMillis() - START_TIME) / 1000), " seconds."));
new ConnectionManager<>(new InetSocketAddress(ServerConfig.PORT_GAME), GameClient::new, new GamePacketHandler());
LoginServerThread.getInstance().start();
Toolkit.getDefaultToolkit().beep();
}
private void printSection(String section)
{
if (DevelopmentConfig.LOG_SERVER_LOAD_TIMES)
{
// Calculate elapsed time for previous section.
final long currentTime = System.currentTimeMillis();
final long sectionElapsed = currentTime - _sectionStartTime;
// Log elapsed time for previous section if not the first section.
if (_previousSectionName != null)
{
LOGGER.info(StringUtil.concat("...section [ ", _previousSectionName, " ] loaded in ", TimeUtil.formatDuration(sectionElapsed), "."));
}
// Update for next measurement.
_previousSectionName = section;
_sectionStartTime = currentTime;
}
// Build and log the new section header.
final StringBuilder sb = new StringBuilder(61);
sb.append("=[ ").append(section).append(" ]");
while (sb.length() < 61)
{
sb.insert(0, '-');
}
LOGGER.info(sb.toString());
}
public long getUsedMemoryMB()
{
return (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1048576;
}
public static long getStartTime()
{
return START_TIME;
}
public static void main(String[] args) throws Exception
{
new GameServer();
}
}
@@ -0,0 +1,828 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAKeyGenParameterSpec;
import java.security.spec.RSAPublicKeySpec;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.l2jmobius.commons.crypt.NewCrypt;
import org.l2jmobius.commons.database.DatabaseFactory;
import org.l2jmobius.commons.network.packet.SimpleWritablePacket;
import org.l2jmobius.commons.util.HexUtil;
import org.l2jmobius.commons.util.TraceUtil;
import org.l2jmobius.gameserver.config.GeneralConfig;
import org.l2jmobius.gameserver.config.ServerConfig;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.network.ConnectionState;
import org.l2jmobius.gameserver.network.Disconnection;
import org.l2jmobius.gameserver.network.GameClient;
import org.l2jmobius.gameserver.network.SystemMessageId;
import org.l2jmobius.gameserver.network.loginserverpackets.receive.AuthResponse;
import org.l2jmobius.gameserver.network.loginserverpackets.receive.ChangePasswordResponse;
import org.l2jmobius.gameserver.network.loginserverpackets.receive.InitLS;
import org.l2jmobius.gameserver.network.loginserverpackets.receive.KickPlayer;
import org.l2jmobius.gameserver.network.loginserverpackets.receive.LoginServerFail;
import org.l2jmobius.gameserver.network.loginserverpackets.receive.PlayerAuthResponse;
import org.l2jmobius.gameserver.network.loginserverpackets.receive.RequestCharacters;
import org.l2jmobius.gameserver.network.loginserverpackets.send.AuthRequest;
import org.l2jmobius.gameserver.network.loginserverpackets.send.BlowFishKey;
import org.l2jmobius.gameserver.network.loginserverpackets.send.ChangeAccessLevel;
import org.l2jmobius.gameserver.network.loginserverpackets.send.ChangePassword;
import org.l2jmobius.gameserver.network.loginserverpackets.send.PlayerAuthRequest;
import org.l2jmobius.gameserver.network.loginserverpackets.send.PlayerInGame;
import org.l2jmobius.gameserver.network.loginserverpackets.send.PlayerLogout;
import org.l2jmobius.gameserver.network.loginserverpackets.send.PlayerTracert;
import org.l2jmobius.gameserver.network.loginserverpackets.send.ReplyCharacters;
import org.l2jmobius.gameserver.network.loginserverpackets.send.SendMail;
import org.l2jmobius.gameserver.network.loginserverpackets.send.ServerStatus;
import org.l2jmobius.gameserver.network.loginserverpackets.send.TempBan;
import org.l2jmobius.gameserver.network.serverpackets.CharSelectionInfo;
import org.l2jmobius.gameserver.network.serverpackets.LoginFail;
import org.l2jmobius.gameserver.network.serverpackets.SystemMessage;
/**
* Handles communication between the game server and login server.<br>
* Manages player authentication, server status updates and various administrative functions.
*/
public class LoginServerThread extends Thread
{
protected static final Logger LOGGER = Logger.getLogger(LoginServerThread.class.getName());
protected static final Logger ACCOUNTING_LOGGER = Logger.getLogger("accounting");
// Protocol constants.
private static final int REVISION = 0x0106; // @see org.l2jmobius.loginserver.LoginServer#PROTOCOL_REV
private static final int RECONNECT_DELAY = 5000; // 5 seconds.
private static final int BLOWFISH_KEY_SIZE = 40;
private static final int HEX_ID_SIZE = 16;
private static final int PACKET_PADDING = 8;
// Connection configuration.
private final String _hostname;
private final int _port;
private final int _gamePort;
private final boolean _acceptAlternate;
private final boolean _reserveHost;
private final List<String> _subnets;
private final List<String> _hosts;
// Connection state.
private Socket _socket;
private OutputStream _out;
private NewCrypt _blowfish;
private byte[] _hexID;
private int _requestID;
// Server state.
private int _maxPlayer;
private final Set<WaitingClient> _waitingClients = ConcurrentHashMap.newKeySet();
private final Map<String, GameClient> _accountsInGameServer = new ConcurrentHashMap<>();
private int _status;
private String _serverName;
protected LoginServerThread()
{
super("LoginServerThread");
// Initialize connection settings.
_port = ServerConfig.GAME_SERVER_LOGIN_PORT;
_gamePort = ServerConfig.PORT_GAME;
_hostname = ServerConfig.GAME_SERVER_LOGIN_HOST;
_acceptAlternate = ServerConfig.ACCEPT_ALTERNATE_ID;
_reserveHost = ServerConfig.RESERVE_HOST_ON_LOGIN;
_subnets = ServerConfig.GAME_SERVER_SUBNETS;
_hosts = ServerConfig.GAME_SERVER_HOSTS;
_maxPlayer = ServerConfig.MAXIMUM_ONLINE_USERS;
// Initialize server identification.
_hexID = ServerConfig.HEX_ID;
if (_hexID == null)
{
_requestID = ServerConfig.REQUEST_ID;
_hexID = HexUtil.generateHexBytes(HEX_ID_SIZE);
}
else
{
_requestID = ServerConfig.SERVER_ID;
}
}
@Override
public void run()
{
while (!isInterrupted())
{
int lengthHi = 0;
int lengthLo = 0;
int length = 0;
boolean checksumOk = false;
try
{
// Establish connection to login server.
LOGGER.info(getClass().getSimpleName() + ": Connecting to login on " + _hostname + ":" + _port);
_socket = new Socket(_hostname, _port);
final InputStream in = _socket.getInputStream();
_out = new BufferedOutputStream(_socket.getOutputStream());
// Initialize Blowfish encryption with default key.
final byte[] blowfishKey = HexUtil.generateHexBytes(BLOWFISH_KEY_SIZE);
_blowfish = new NewCrypt("_;v.]05-31!|+-%xT!^[$\00");
while (!isInterrupted())
{
// Read packet length.
lengthLo = in.read();
lengthHi = in.read();
length = (lengthHi * 256) + lengthLo;
if (lengthHi < 0)
{
LOGGER.finer(getClass().getSimpleName() + ": Login terminated the connection.");
break;
}
// Read packet data.
final byte[] incoming = new byte[length - 2];
int receivedBytes = 0;
int newBytes = 0;
int left = length - 2;
while ((newBytes != -1) && (receivedBytes < (length - 2)))
{
newBytes = in.read(incoming, receivedBytes, left);
receivedBytes += newBytes;
left -= newBytes;
}
if (receivedBytes != (length - 2))
{
LOGGER.warning(getClass().getSimpleName() + ": Incomplete packet received, closing connection (LS)");
break;
}
// Decrypt and verify packet.
_blowfish.decrypt(incoming, 0, incoming.length);
checksumOk = NewCrypt.verifyChecksum(incoming);
if (!checksumOk)
{
LOGGER.warning(getClass().getSimpleName() + ": Incorrect packet checksum, ignoring packet (LS)");
break;
}
// Process packet based on type.
final int packetType = incoming[0] & 0xff;
switch (packetType)
{
case 0x00: // InitLS - Initialize login server communication.
{
final InitLS init = new InitLS(incoming);
if (init.getRevision() != REVISION)
{
LOGGER.warning("/!\\ Revision mismatch between LS and GS /!\\");
break;
}
// Generate RSA public key from login server data.
RSAPublicKey publicKey;
try
{
final KeyFactory kfac = KeyFactory.getInstance("RSA");
final BigInteger modulus = new BigInteger(init.getRSAKey());
final RSAPublicKeySpec kspec1 = new RSAPublicKeySpec(modulus, RSAKeyGenParameterSpec.F4);
publicKey = (RSAPublicKey) kfac.generatePublic(kspec1);
}
catch (GeneralSecurityException e)
{
LOGGER.warning(getClass().getSimpleName() + ": Trouble initializing RSA public key from login server.");
break;
}
// Send encrypted blowfish key and switch to secure communication.
sendPacket(new BlowFishKey(blowfishKey, publicKey));
_blowfish = new NewCrypt(blowfishKey);
sendPacket(new AuthRequest(_requestID, _acceptAlternate, _hexID, _gamePort, _reserveHost, _maxPlayer, _subnets, _hosts));
break;
}
case 0x01: // LoginServerFail - Registration failure.
{
final LoginServerFail lsf = new LoginServerFail(incoming);
LOGGER.info(getClass().getSimpleName() + ": Registration failed: " + lsf.getReasonString());
break;
}
case 0x02: // AuthResponse - Server registration successful.
{
final AuthResponse aresp = new AuthResponse(incoming);
final int serverID = aresp.getServerId();
_serverName = aresp.getServerName();
ServerConfig.saveHexid(serverID, hexToString(_hexID));
LOGGER.info(getClass().getSimpleName() + ": Registered on login as Server " + serverID + ": " + _serverName);
// Configure and send server status.
final ServerStatus st = new ServerStatus();
if (ServerConfig.SERVER_LIST_BRACKET)
{
st.addAttribute(ServerStatus.SERVER_LIST_SQUARE_BRACKET, ServerStatus.ON);
}
else
{
st.addAttribute(ServerStatus.SERVER_LIST_SQUARE_BRACKET, ServerStatus.OFF);
}
st.addAttribute(ServerStatus.SERVER_TYPE, ServerConfig.SERVER_LIST_TYPE);
if (GeneralConfig.SERVER_GMONLY)
{
st.addAttribute(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_GM_ONLY);
}
else
{
st.addAttribute(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_AUTO);
}
if (ServerConfig.SERVER_LIST_AGE == 15)
{
st.addAttribute(ServerStatus.SERVER_AGE, ServerStatus.SERVER_AGE_15);
}
else if (ServerConfig.SERVER_LIST_AGE == 18)
{
st.addAttribute(ServerStatus.SERVER_AGE, ServerStatus.SERVER_AGE_18);
}
else
{
st.addAttribute(ServerStatus.SERVER_AGE, ServerStatus.SERVER_AGE_ALL);
}
sendPacket(st);
// Send list of currently online players.
final List<String> playerList = new ArrayList<>();
for (Player player : World.getPlayers())
{
if (!player.isInOfflineMode())
{
playerList.add(player.getAccountName());
}
}
if (!playerList.isEmpty())
{
sendPacket(new PlayerInGame(playerList));
}
break;
}
case 0x03: // PlayerAuthResponse - Player authentication result.
{
final PlayerAuthResponse par = new PlayerAuthResponse(incoming);
final String account = par.getAccount();
WaitingClient wcToRemove = null;
// Find the waiting client for this account.
synchronized (_waitingClients)
{
for (WaitingClient wc : _waitingClients)
{
if (wc.account.equals(account))
{
wcToRemove = wc;
break;
}
}
}
if (wcToRemove != null)
{
if (par.isAuthed())
{
// Authentication successful - setup client.
final PlayerInGame pig = new PlayerInGame(par.getAccount());
sendPacket(pig);
wcToRemove.gameClient.setConnectionState(ConnectionState.AUTHENTICATED);
wcToRemove.gameClient.setSessionId(wcToRemove.sessionKey);
final CharSelectionInfo cl = new CharSelectionInfo(wcToRemove.account, wcToRemove.gameClient.getSessionId().playOkID1);
wcToRemove.gameClient.sendPacket(cl);
wcToRemove.gameClient.setCharSelection(cl.getCharInfo());
}
else
{
// Authentication failed - close connection.
LOGGER.warning(getClass().getSimpleName() + ": Session key incorrect. Closing connection for account " + wcToRemove.account);
wcToRemove.gameClient.close(new LoginFail(LoginFail.SYSTEM_ERROR_LOGIN_LATER));
sendLogout(wcToRemove.account);
}
_waitingClients.remove(wcToRemove);
}
break;
}
case 0x04: // KickPlayer - Force disconnect player.
{
final KickPlayer kp = new KickPlayer(incoming);
doKickPlayer(kp.getAccount());
break;
}
case 0x05: // RequestCharacters - Get character info for account.
{
final RequestCharacters rc = new RequestCharacters(incoming);
getCharsOnServer(rc.getAccount());
break;
}
case 0x06: // ChangePasswordResponse - Password change result.
{
new ChangePasswordResponse(incoming);
break;
}
}
}
}
catch (UnknownHostException e)
{
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Unknown host: ", e);
}
catch (SocketException e)
{
LOGGER.warning(getClass().getSimpleName() + ": LoginServer not available, trying to reconnect...");
}
catch (IOException e)
{
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Disconnected from Login, trying to reconnect: ", e);
}
finally
{
// Clean up connection resources.
try
{
_socket.close();
if (isInterrupted())
{
return;
}
}
catch (Exception e)
{
// Ignore cleanup exceptions.
}
}
// Wait before attempting reconnection.
try
{
Thread.sleep(RECONNECT_DELAY);
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
break;
}
}
}
/**
* Adds waiting client and sends authentication request to login server.
* @param accountName the account name
* @param client the game client
* @param key the session key
*/
public void addWaitingClientAndSendRequest(String accountName, GameClient client, SessionKey key)
{
synchronized (_waitingClients)
{
_waitingClients.add(new WaitingClient(accountName, client, key));
}
sendPacket(new PlayerAuthRequest(accountName, key));
}
/**
* Removes waiting client from authentication queue.
* @param client the game client to remove
*/
public void removeWaitingClient(GameClient client)
{
WaitingClient toRemove = null;
synchronized (_waitingClients)
{
for (WaitingClient c : _waitingClients)
{
if (c.gameClient == client)
{
toRemove = c;
break;
}
}
if (toRemove != null)
{
_waitingClients.remove(toRemove);
}
}
}
/**
* Sends logout notification for specified account.
* @param account the account name
*/
public void sendLogout(String account)
{
if (account == null)
{
return;
}
final GameClient removed = _accountsInGameServer.remove(account);
if (removed != null)
{
removed.disconnect();
}
sendPacket(new PlayerLogout(account));
}
/**
* Adds game server login entry for account tracking.
* @param account the account name
* @param client the game client
* @return true if account was not already logged in, false otherwise
*/
public boolean addGameServerLogin(String account, GameClient client)
{
return _accountsInGameServer.putIfAbsent(account, client) == null;
}
/**
* Sends access level change notification to login server.
* @param account the account name
* @param level the new access level
*/
public void sendAccessLevel(String account, int level)
{
sendPacket(new ChangeAccessLevel(account, level));
}
/**
* Sends client trace route information to login server.
* @param account the account name
* @param address the trace route addresses (5 hops)
*/
public void sendClientTracert(String account, String[] address)
{
sendPacket(new PlayerTracert(account, address[0], address[1], address[2], address[3], address[4]));
}
/**
* Sends mail notification to login server.
* @param account the account name
* @param mailId the mail identifier
* @param args additional mail arguments
*/
public void sendMail(String account, String mailId, String... args)
{
sendPacket(new SendMail(account, mailId, args));
}
/**
* Sends temporary ban notification to login server.
* @param account the account name
* @param ip the IP address to ban
* @param time the ban duration in milliseconds
*/
public void sendTempBan(String account, String ip, long time)
{
sendPacket(new TempBan(account, ip, time));
}
/**
* Converts hex byte array to hexadecimal string representation.
* @param hex the hex byte array
* @return the hex string
*/
private String hexToString(byte[] hex)
{
return new BigInteger(hex).toString(16);
}
/**
* Forcibly kicks player from the game server.
* @param account the account name to kick
*/
private void doKickPlayer(String account)
{
final GameClient client = _accountsInGameServer.get(account);
if (client != null)
{
final SystemMessage msg = new SystemMessage(SystemMessageId.ANOTHER_PERSON_HAS_LOGGED_IN_WITH_THE_SAME_ACCOUNT);
if (client.isDetached())
{
final Player player = client.getPlayer();
if (player != null)
{
player.storeMe();
player.deleteMe();
}
client.close(msg);
}
else
{
Disconnection.of(client).storeAndDeleteWith(msg);
ACCOUNTING_LOGGER.info("Kicked by login, " + client);
}
}
sendLogout(account);
}
/**
* Retrieves character count and deletion times for specified account.
* @param account the account name
*/
private void getCharsOnServer(String account)
{
int chars = 0;
final List<Long> charToDel = new ArrayList<>();
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement ps = con.prepareStatement("SELECT deletetime FROM characters WHERE account_name=?"))
{
ps.setString(1, account);
try (ResultSet rs = ps.executeQuery())
{
while (rs.next())
{
chars++;
final long delTime = rs.getLong("deletetime");
if (delTime != 0)
{
charToDel.add(delTime);
}
}
}
}
catch (SQLException e)
{
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Exception in getCharsOnServer: " + e.getMessage(), e);
}
sendPacket(new ReplyCharacters(account, chars, charToDel));
}
/**
* Sends packet to login server with proper encryption and checksums.
* @param packet the packet to send
*/
private void sendPacket(SimpleWritablePacket packet)
{
if ((_blowfish == null) || (_socket == null) || _socket.isClosed())
{
return;
}
try
{
packet.write(); // Write initial packet data.
packet.writeInt(0); // Reserved space for checksum.
int size = packet.getLength() - 2; // Size without header.
// Add padding to align packet to 8-byte boundary.
final int padding = size % PACKET_PADDING;
if (padding != 0)
{
for (int i = padding; i < PACKET_PADDING; i++)
{
packet.writeByte(0);
}
}
// Get final packet data for encryption.
final byte[] data = packet.getSendableBytes();
size = data.length - 2; // Data size without header.
synchronized (_out)
{
NewCrypt.appendChecksum(data, 2, size);
_blowfish.crypt(data, 2, size);
_out.write(data);
try
{
_out.flush();
}
catch (IOException e)
{
// LoginServer might have terminated connection.
}
}
}
catch (Exception e)
{
LOGGER.severe("LoginServerThread: IOException while sending packet " + packet.getClass().getSimpleName());
LOGGER.severe(TraceUtil.getStackTrace(e));
}
}
/**
* Sets maximum player count and notifies login server.
* @param maxPlayer the maximum player count
*/
public void setMaxPlayer(int maxPlayer)
{
sendServerStatus(ServerStatus.MAX_PLAYERS, maxPlayer);
_maxPlayer = maxPlayer;
}
/**
* Gets current maximum player count.
* @return the maximum player count
*/
public int getMaxPlayer()
{
return _maxPlayer;
}
/**
* Sends server status update to login server.
* @param id the status attribute identifier
* @param value the status value
*/
public void sendServerStatus(int id, int value)
{
final ServerStatus serverStatus = new ServerStatus();
serverStatus.addAttribute(id, value);
sendPacket(serverStatus);
}
/**
* Sends server type configuration to login server.
*/
public void sendServerType()
{
final ServerStatus serverStatus = new ServerStatus();
serverStatus.addAttribute(ServerStatus.SERVER_TYPE, ServerConfig.SERVER_LIST_TYPE);
sendPacket(serverStatus);
}
/**
* Sends password change request to login server.
* @param accountName the account name
* @param charName the character name
* @param oldpass the current password
* @param newpass the new password
*/
public void sendChangePassword(String accountName, String charName, String oldpass, String newpass)
{
sendPacket(new ChangePassword(accountName, charName, oldpass, newpass));
}
/**
* Gets current server status.
* @return the server status code
*/
public int getServerStatus()
{
return _status;
}
/**
* Gets server status as human-readable string.
* @return the status string representation
*/
public String getStatusString()
{
return ServerStatus.STATUS_STRING[_status];
}
/**
* Gets registered server name.
* @return the server name
*/
public String getServerName()
{
return _serverName;
}
/**
* Sets server status and notifies login server.
* @param status the new server status
*/
public void setServerStatus(int status)
{
switch (status)
{
case ServerStatus.STATUS_AUTO:
case ServerStatus.STATUS_DOWN:
case ServerStatus.STATUS_FULL:
case ServerStatus.STATUS_GM_ONLY:
case ServerStatus.STATUS_GOOD:
case ServerStatus.STATUS_NORMAL:
{
sendServerStatus(ServerStatus.SERVER_LIST_STATUS, status);
_status = status;
break;
}
default:
{
throw new IllegalArgumentException("Invalid server status: " + status);
}
}
}
/**
* Gets game client for specified account name.
* @param name the account name
* @return the game client or null if not found
*/
public GameClient getClient(String name)
{
return name != null ? _accountsInGameServer.get(name) : null;
}
/**
* Session key container for player authentication between login and game servers.
*/
public static class SessionKey
{
public int playOkID1;
public int playOkID2;
public int loginOkID1;
public int loginOkID2;
public SessionKey(int loginOK1, int loginOK2, int playOK1, int playOK2)
{
playOkID1 = playOK1;
playOkID2 = playOK2;
loginOkID1 = loginOK1;
loginOkID2 = loginOK2;
}
@Override
public String toString()
{
return "PlayOk: " + playOkID1 + " " + playOkID2 + " LoginOk:" + loginOkID1 + " " + loginOkID2;
}
}
/**
* Represents a client waiting for authentication response from login server.
*/
private static class WaitingClient
{
public String account;
public GameClient gameClient;
public SessionKey sessionKey;
public WaitingClient(String acc, GameClient client, SessionKey key)
{
account = acc;
gameClient = client;
sessionKey = key;
}
}
public static LoginServerThread getInstance()
{
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder
{
protected static final LoginServerThread INSTANCE = new LoginServerThread();
}
}
@@ -0,0 +1,601 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.l2jmobius.commons.config.DatabaseConfig;
import org.l2jmobius.commons.database.DatabaseBackup;
import org.l2jmobius.commons.database.DatabaseFactory;
import org.l2jmobius.commons.threads.ThreadPool;
import org.l2jmobius.gameserver.config.GeneralConfig;
import org.l2jmobius.gameserver.config.ServerConfig;
import org.l2jmobius.gameserver.config.custom.AutoPlayConfig;
import org.l2jmobius.gameserver.config.custom.OfflinePlayConfig;
import org.l2jmobius.gameserver.config.custom.OfflineTradeConfig;
import org.l2jmobius.gameserver.data.SchemeBufferTable;
import org.l2jmobius.gameserver.data.sql.ClanTable;
import org.l2jmobius.gameserver.data.sql.OfflinePlayTable;
import org.l2jmobius.gameserver.data.sql.OfflineTraderTable;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.managers.CHSiegeManager;
import org.l2jmobius.gameserver.managers.CastleManorManager;
import org.l2jmobius.gameserver.managers.CursedWeaponsManager;
import org.l2jmobius.gameserver.managers.FishingChampionshipManager;
import org.l2jmobius.gameserver.managers.GlobalVariablesManager;
import org.l2jmobius.gameserver.managers.GrandBossManager;
import org.l2jmobius.gameserver.managers.ItemsOnGroundManager;
import org.l2jmobius.gameserver.managers.PrecautionaryRestartManager;
import org.l2jmobius.gameserver.managers.RaidBossSpawnManager;
import org.l2jmobius.gameserver.managers.ScriptManager;
import org.l2jmobius.gameserver.mechanics.olympiad.Hero;
import org.l2jmobius.gameserver.mechanics.olympiad.Olympiad;
import org.l2jmobius.gameserver.mechanics.sevensigns.SevenSigns;
import org.l2jmobius.gameserver.mechanics.sevensigns.SevenSignsFestival;
import org.l2jmobius.gameserver.network.Disconnection;
import org.l2jmobius.gameserver.network.SystemMessageId;
import org.l2jmobius.gameserver.network.loginserverpackets.send.ServerStatus;
import org.l2jmobius.gameserver.network.serverpackets.ServerClose;
import org.l2jmobius.gameserver.network.serverpackets.SystemMessage;
import org.l2jmobius.gameserver.taskmanagers.GameTimeTaskManager;
/**
* This class provides the functions for shutting down and restarting the server.<br>
* It closes all open client connections and saves all data.
* @version $Revision: 1.2.4.5 $ $Date: 2005/03/27 15:29:09 $
*/
public class Shutdown extends Thread
{
private static final Logger LOGGER = Logger.getLogger(Shutdown.class.getName());
private static final int SIGTERM = 0;
private static final int GM_SHUTDOWN = 1;
private static final int GM_RESTART = 2;
private static final int ABORT = 3;
private static final String[] MODE_TEXT =
{
"SIGTERM",
"shutting down",
"restarting",
"aborting"
};
private static Shutdown _counterInstance;
private static boolean _countdownFinished;
private static volatile int _secondsShut;
private static volatile int _shutdownMode;
/**
* This function starts a shutdown count down (Copied from Function startShutdown())
* @param seconds seconds until shutdown
*/
private void sendServerQuit(int seconds)
{
final SystemMessage sysm = new SystemMessage(SystemMessageId.THE_SERVER_WILL_BE_COMING_DOWN_IN_S1_SECOND_S_PLEASE_FIND_A_SAFE_PLACE_TO_LOG_OUT);
sysm.addInt(seconds);
World.broadcastToAllOnlinePlayers(sysm);
}
/**
* Default constructor is only used internal to create the shutdown-hook instance
*/
protected Shutdown()
{
_secondsShut = -1;
_shutdownMode = SIGTERM;
}
/**
* This creates a countdown instance of Shutdown.
* @param seconds how many seconds until shutdown
* @param restart true is the server shall restart after shutdown
*/
public Shutdown(int seconds, boolean restart)
{
_secondsShut = Math.max(0, seconds);
_shutdownMode = restart ? GM_RESTART : GM_SHUTDOWN;
}
/**
* This function is called, when a new thread starts if this thread is the thread of getInstance, then this is the shutdown hook and we save all data and disconnect all clients.<br>
* After this thread ends, the server will completely exit if this is not the thread of getInstance, then this is a countdown thread.<br>
* We start the countdown, and when we finished it, and it was not aborted, we tell the shutdown-hook why we call exit, and then call exit when the exit status of the server is 1, startServer.sh / startServer.bat will restart the server.
*/
@Override
public void run()
{
if (this == getInstance())
{
startShutdownActions();
return;
}
if (_countdownFinished)
{
return;
}
// Send warnings and then call exit to start shutdown sequence.
countdown();
// Last point where logging is operational.
LOGGER.warning("GM shutdown countdown is over. " + MODE_TEXT[_shutdownMode] + " NOW!");
switch (_shutdownMode)
{
case GM_SHUTDOWN:
{
getInstance().setMode(GM_SHUTDOWN);
startShutdownActions();
System.exit(0);
break;
}
case GM_RESTART:
{
getInstance().setMode(GM_RESTART);
startShutdownActions();
System.exit(2);
break;
}
case ABORT:
{
LoginServerThread.getInstance().setServerStatus(ServerStatus.STATUS_AUTO);
break;
}
}
}
/**
* This functions starts a shutdown countdown.
* @param player GM who issued the shutdown command
* @param seconds seconds until shutdown
* @param restart true if the server will restart after shutdown
*/
public void startShutdown(Player player, int seconds, boolean restart)
{
_shutdownMode = restart ? GM_RESTART : GM_SHUTDOWN;
if (player != null)
{
LOGGER.warning("GM: " + player.getName() + "(" + player.getObjectId() + ") issued shutdown command. " + MODE_TEXT[_shutdownMode] + " in " + seconds + " seconds!");
}
else
{
LOGGER.warning("Server scheduled restart issued shutdown command. " + (restart ? "Restart" : "Shutdown") + " in " + seconds + " seconds!");
}
if (_shutdownMode > 0)
{
switch (seconds)
{
case 540:
case 480:
case 420:
case 360:
case 300:
case 240:
case 180:
case 120:
case 60:
case 30:
case 10:
case 5:
case 4:
case 3:
case 2:
case 1:
{
break;
}
default:
{
sendServerQuit(seconds);
}
}
}
if (_counterInstance != null)
{
_counterInstance.abort();
}
if (ServerConfig.PRECAUTIONARY_RESTART_ENABLED)
{
PrecautionaryRestartManager.getInstance().restartEnabled();
}
// The main instance should only run for shutdown hook, so we start a new instance.
_counterInstance = new Shutdown(seconds, restart);
_counterInstance.start();
}
/**
* This function aborts a running countdown.
* @param player GM who issued the abort command
*/
public void abort(Player player)
{
if (_countdownFinished)
{
LOGGER.warning("GM: " + (player != null ? player.getName() + "(" + player.getObjectId() + ") " : "") + "shutdown ABORT failed because countdown has finished.");
return;
}
LOGGER.warning("GM: " + (player != null ? player.getName() + "(" + player.getObjectId() + ") " : "") + "issued shutdown ABORT. " + MODE_TEXT[_shutdownMode] + " has been stopped!");
if (_counterInstance != null)
{
_counterInstance.abort();
if (ServerConfig.PRECAUTIONARY_RESTART_ENABLED)
{
PrecautionaryRestartManager.getInstance().restartAborted();
}
World.broadcastToAllOnlinePlayers("Server aborts " + MODE_TEXT[_shutdownMode] + " and continues normal operation!", false);
}
}
/**
* Set the shutdown mode.
* @param mode what mode shall be set
*/
private void setMode(int mode)
{
_shutdownMode = mode;
}
/**
* Set shutdown mode to ABORT.
*/
private void abort()
{
_shutdownMode = ABORT;
}
/**
* This counts the countdown and reports it to all players countdown is aborted if mode changes to ABORT.
*/
private void countdown()
{
try
{
while (_secondsShut > 0)
{
// Rehabilitate previous server status if shutdown is aborted.
if (_shutdownMode == ABORT)
{
if (LoginServerThread.getInstance().getServerStatus() == ServerStatus.STATUS_DOWN)
{
LoginServerThread.getInstance().setServerStatus((GeneralConfig.SERVER_GMONLY) ? ServerStatus.STATUS_GM_ONLY : ServerStatus.STATUS_AUTO);
}
break;
}
switch (_secondsShut)
{
case 540:
case 480:
case 420:
case 360:
case 300:
case 240:
case 180:
case 120:
case 60:
case 30:
case 10:
case 5:
case 4:
case 3:
case 2:
case 1:
{
sendServerQuit(_secondsShut);
}
}
// Prevent players from logging in.
if ((_secondsShut <= 60) && (LoginServerThread.getInstance().getServerStatus() != ServerStatus.STATUS_DOWN))
{
LoginServerThread.getInstance().setServerStatus(ServerStatus.STATUS_DOWN);
}
_secondsShut--;
Thread.sleep(1000);
}
}
catch (Exception e)
{
// This will never happen.
}
}
/**
* Actions performed when shutdown countdown completes.
*/
private void startShutdownActions()
{
if (_countdownFinished)
{
return;
}
_countdownFinished = true;
final TimeCounter tc = new TimeCounter();
final TimeCounter tc1 = new TimeCounter();
try
{
if ((OfflineTradeConfig.OFFLINE_TRADE_ENABLE || OfflineTradeConfig.OFFLINE_CRAFT_ENABLE) && OfflineTradeConfig.RESTORE_OFFLINERS && !OfflineTradeConfig.STORE_OFFLINE_TRADE_IN_REALTIME)
{
OfflineTraderTable.getInstance().storeOffliners();
LOGGER.info("Offline Traders Table: Offline shops stored(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
}
}
catch (Throwable t)
{
LOGGER.log(Level.WARNING, "Error saving offline shops.", t);
}
try
{
if (OfflinePlayConfig.RESTORE_AUTO_PLAY_OFFLINERS && AutoPlayConfig.ENABLE_AUTO_ASSIST)
{
OfflinePlayTable.getInstance().storeOfflineGroups();
LOGGER.info("Offline Play Table: Offline play groups stored(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
}
}
catch (Throwable t)
{
LOGGER.log(Level.WARNING, "Error saving offline play groups.", t);
}
try
{
disconnectAllCharacters();
LOGGER.info("All players disconnected and saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
}
catch (Throwable t)
{
// ignore
}
// ensure all services are stopped
try
{
GameTimeTaskManager.getInstance().interrupt();
LOGGER.info("Game Time Task Manager: Thread interruped(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
}
catch (Throwable t)
{
// ignore
}
// stop all thread pools
try
{
ThreadPool.shutdown();
LOGGER.info("Thread Pool Manager: Manager has been shut down(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
}
catch (Throwable t)
{
// ignore
}
try
{
LoginServerThread.getInstance().interrupt();
LOGGER.info("Login Server Thread: Thread interruped(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
}
catch (Throwable t)
{
// ignore
}
// last byebye, save all data and quit this server
saveData();
tc.restartCounter();
// commit data, last chance
try
{
DatabaseFactory.close();
LOGGER.info("Database Factory: Database connection has been shut down(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
}
catch (Throwable t)
{
// ignore
}
// Backup database.
if (DatabaseConfig.BACKUP_DATABASE)
{
DatabaseBackup.performBackup("game");
}
LOGGER.info("The server has been successfully shut down in " + (tc1.getEstimatedTime() / 1000) + "seconds.");
}
/**
* This sends a last byebye, disconnects all players and saves data.
*/
private void saveData()
{
switch (_shutdownMode)
{
case SIGTERM:
{
LOGGER.info("SIGTERM received. Shutting down NOW!");
break;
}
case GM_SHUTDOWN:
{
LOGGER.info("GM shutdown received. Shutting down NOW!");
break;
}
case GM_RESTART:
{
LOGGER.info("GM restart received. Restarting NOW!");
break;
}
}
final TimeCounter tc = new TimeCounter();
// Seven Signs data is now saved along with Festival data.
if (!SevenSigns.getInstance().isSealValidationPeriod())
{
SevenSignsFestival.getInstance().saveFestivalData(false);
LOGGER.info("SevenSignsFestival: Festival data saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
}
// Save Seven Signs data before closing. :)
SevenSigns.getInstance().saveSevenSignsData();
LOGGER.info("SevenSigns: Seven Signs data saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
SevenSigns.getInstance().saveSevenSignsStatus();
LOGGER.info("SevenSigns: Seven Signs status saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
// Save all raidboss and GrandBoss status ^_^
RaidBossSpawnManager.getInstance().cleanUp();
LOGGER.info("RaidBossSpawnManager: All raidboss info saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
GrandBossManager.getInstance().cleanUp();
LOGGER.info("GrandBossManager: All Grand Boss info saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
Olympiad.getInstance().saveOlympiadStatus();
LOGGER.info("Olympiad System: Data saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
Hero.getInstance().shutdown();
LOGGER.info("Hero System: Data saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
ClanTable.getInstance().shutdown();
LOGGER.info("Clan System: Data saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
// Save Cursed Weapons data before closing.
CursedWeaponsManager.getInstance().saveData();
LOGGER.info("Cursed Weapons Manager: Data saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
// Save all manor data.
if (!GeneralConfig.ALT_MANOR_SAVE_ALL_ACTIONS)
{
CastleManorManager.getInstance().storeMe();
LOGGER.info("Castle Manor Manager: Data saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
}
CHSiegeManager.getInstance().onServerShutDown();
LOGGER.info("CHSiegeManager: Siegable hall attacker lists saved!");
// Save all global (non-player specific) Quest data that needs to persist after reboot.
ScriptManager.getInstance().save();
LOGGER.info("Script Manager: Data saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
// Save all global variables data.
GlobalVariablesManager.getInstance().storeMe();
LOGGER.info("Global Variables Manager: Variables saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
// Save Fishing tournament data.
if (GeneralConfig.ALT_FISH_CHAMPIONSHIP_ENABLED)
{
FishingChampionshipManager.getInstance().shutdown();
LOGGER.info("Fishing Championship Manager: Data saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
}
// Schemes save.
SchemeBufferTable.getInstance().saveSchemes();
LOGGER.info("SchemeBufferTable: Data saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
// Save items on ground before closing.
if (GeneralConfig.SAVE_DROPPED_ITEM)
{
ItemsOnGroundManager.getInstance().saveInDb();
LOGGER.info("Items On Ground Manager: Data saved(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
ItemsOnGroundManager.getInstance().cleanUp();
LOGGER.info("Items On Ground Manager: Cleaned up(" + tc.getEstimatedTimeAndRestartCounter() + "ms).");
}
try
{
Thread.sleep(5000);
}
catch (Exception e)
{
// This will never happen.
}
}
/**
* This disconnects all clients from the server.
*/
private void disconnectAllCharacters()
{
for (Player player : World.getPlayers())
{
Disconnection.of(player).storeAndDeleteWith(ServerClose.STATIC_PACKET);
}
}
/**
* A simple class used to track down the estimated time of method executions.<br>
* Once this class is created, it saves the start time, and when you want to get the estimated time, use the getEstimatedTime() method.
*/
private static class TimeCounter
{
private long _startTime;
protected TimeCounter()
{
restartCounter();
}
public void restartCounter()
{
_startTime = System.currentTimeMillis();
}
public long getEstimatedTimeAndRestartCounter()
{
final long toReturn = System.currentTimeMillis() - _startTime;
restartCounter();
return toReturn;
}
public long getEstimatedTime()
{
return System.currentTimeMillis() - _startTime;
}
}
/**
* Get the shutdown-hook instance the shutdown-hook instance is created by the first call of this function, but it has to be registered externally.
* @return instance of Shutdown, to be used as shutdown hook
*/
public static Shutdown getInstance()
{
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder
{
protected static final Shutdown INSTANCE = new Shutdown();
}
}
@@ -0,0 +1,568 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.ai;
import org.l2jmobius.gameserver.entity.Location;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.WorldRegion;
import org.l2jmobius.gameserver.entity.actor.Creature;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.Summon;
import org.l2jmobius.gameserver.interfaces.ILocational;
import org.l2jmobius.gameserver.mechanics.skill.Skill;
import org.l2jmobius.gameserver.network.serverpackets.ActionFailed;
import org.l2jmobius.gameserver.network.serverpackets.AutoAttackStart;
import org.l2jmobius.gameserver.network.serverpackets.AutoAttackStop;
import org.l2jmobius.gameserver.network.serverpackets.Die;
import org.l2jmobius.gameserver.network.serverpackets.MoveToLocation;
import org.l2jmobius.gameserver.network.serverpackets.MoveToPawn;
import org.l2jmobius.gameserver.network.serverpackets.StopMove;
import org.l2jmobius.gameserver.taskmanagers.AttackStanceTaskManager;
import org.l2jmobius.gameserver.taskmanagers.CreatureFollowTaskManager;
import org.l2jmobius.gameserver.taskmanagers.GameTimeTaskManager;
/**
* Mother class of all objects AI in the world.<br>
* AbastractAI:<br>
* <li>CreatureAI</li>
*/
public abstract class AbstractAI
{
/** The creature that this AI manages. */
protected final Creature _actor;
/** Current long-term intention. */
protected Intention _intention = Intention.IDLE;
/** Flags about client's state, in order to know which messages to send. */
protected volatile boolean _clientAutoAttacking;
/** Flags about client's state, in order to know which messages to send. */
protected int _clientMovingToPawnOffset;
/** Different targets this AI maintains. */
private WorldObject _target;
private Creature _castTarget;
protected Creature _attackTarget;
protected Creature _followTarget;
/** The skill we are currently casting by INTENTION_CAST. */
protected Skill _skill;
/** Different internal state flags. */
private int _moveToPawnTimeout;
private NextAction _nextAction;
/**
* @return the _nextAction
*/
public NextAction getNextAction()
{
return _nextAction;
}
/**
* @param nextAction the next action to set.
*/
public void setNextAction(NextAction nextAction)
{
_nextAction = nextAction;
}
protected AbstractAI(Creature creature)
{
_actor = creature;
}
/**
* @return the Creature managed by this Accessor AI.
*/
public Creature getActor()
{
return _actor;
}
/**
* @return the current Intention.
*/
public Intention getIntention()
{
return _intention;
}
/**
* @return the saved Intention pending replay (e.g. after a CAST finishes), or {@code null} if none.<br>
* <b><u>Overridden in</u>:</b>
* <ul>
* <li><b>PlayerAI</b> : returns the intention that was interrupted by the current CAST</li>
* <li><b>SummonAI</b> : returns {@link Intention#ATTACK} if a pending attack is saved</li>
* </ul>
*/
public Intention getNextIntention()
{
return null;
}
public abstract void setIntentionIdle();
public abstract void setIntentionActive();
public abstract void setIntentionRest();
public abstract void setIntentionAttack(WorldObject target);
public abstract void setIntentionCast(Skill skill, WorldObject target);
public abstract void setIntentionMoveTo(ILocational destination);
public abstract void setIntentionFollow(WorldObject target);
public abstract void setIntentionPickUp(WorldObject item);
public abstract void setIntentionInteract(WorldObject object);
public abstract void notifyActionThink();
public abstract void notifyActionAttacked(WorldObject attacker);
public abstract void notifyActionAggression(WorldObject target, int aggro);
public abstract void notifyActionStunned();
public abstract void notifyActionParalyzed();
public abstract void notifyActionSleeping();
public abstract void notifyActionRooted();
public abstract void notifyActionConfused();
public abstract void notifyActionMuted();
public abstract void notifyActionEvaded(WorldObject attacker);
public abstract void notifyActionReadyToAct();
public abstract void notifyActionUserCmd(Object arg0, Object arg1);
public abstract void notifyActionArrived();
public abstract void notifyActionArrivedRevalidate();
public abstract void notifyActionArrivedBlocked(Location location);
public abstract void notifyActionForgetObject(WorldObject object);
public abstract void notifyActionCancel();
public abstract void notifyActionDeath();
public abstract void notifyActionFakeDeath();
public abstract void notifyActionAfraid(WorldObject effector, boolean start);
public abstract void notifyActionFinishCasting();
/**
* Cancel action client side by sending Server->Client packet ActionFailed to the Player actor.<br>
* <font color=#FF0000><b><u>Caution</u>: Low level function, used by AI subclasses</b></font>
*/
protected void clientActionFailed()
{
if (_actor.isPlayer())
{
_actor.sendPacket(ActionFailed.STATIC_PACKET);
}
}
/**
* Move the actor to Pawn server side AND client side by sending Server->Client packet MoveToPawn <i>(broadcast)</i>.<br>
* <font color=#FF0000><b><u>Caution</u>: Low level function, used by AI subclasses</b></font>
* @param pawn
* @param offsetValue
*/
public void moveToPawn(WorldObject pawn, int offsetValue)
{
// Check if actor can move.
if (!_actor.isMovementDisabled() && !_actor.isAttackingNow() && !_actor.isCastingNow())
{
int offset = offsetValue;
if (offset < 10)
{
offset = 10;
}
// Prevent possible extra calls to this function (there is none?), also don't send movetopawn packets too often.
final int gameTime = GameTimeTaskManager.getInstance().getGameTicks();
if (_actor.isMoving() && (_target == pawn))
{
if (_clientMovingToPawnOffset == offset)
{
if (gameTime < _moveToPawnTimeout)
{
return;
}
}
// Minimum time to calculate new route is 2 seconds.
else if (_actor.isOnGeodataPath() && (gameTime < (_moveToPawnTimeout + 10)))
{
return;
}
}
// Set AI movement data.
_clientMovingToPawnOffset = offset;
_target = pawn;
_moveToPawnTimeout = gameTime;
_moveToPawnTimeout += 1000 / GameTimeTaskManager.MILLIS_IN_TICK;
if (pawn == null)
{
return;
}
// Calculate movement data for a move to location action and add the actor to movingObjects of GameTimeTaskManager.
_actor.moveToLocation(pawn.getX(), pawn.getY(), pawn.getZ(), offset);
// May result to make monsters stop moving.
// if (!_actor.isMoving())
// {
// clientActionFailed();
// return;
// }
// Send a Server->Client packet MoveToPawn/MoveToLocation to the actor and all Player in its known players.
if (pawn.isCreature())
{
if (_actor.isOnGeodataPath())
{
_actor.broadcastMoveToLocation();
_clientMovingToPawnOffset = 0;
}
else
{
final WorldRegion region = _actor.getWorldRegion();
if ((region != null) && region.isActive())
{
_actor.broadcastPacket(new MoveToPawn(_actor, pawn, offset));
}
}
}
else
{
_actor.broadcastMoveToLocation();
}
}
else
{
clientActionFailed();
}
}
public void moveTo(ILocational loc)
{
moveTo(loc.getX(), loc.getY(), loc.getZ());
}
/**
* Move the actor to Location (x,y,z) server side AND client side by sending Server->Client packet MoveToLocation <i>(broadcast)</i>.<br>
* <font color=#FF0000><b><u>Caution</u>: Low level function, used by AI subclasses</b></font>
* @param x
* @param y
* @param z
*/
protected void moveTo(int x, int y, int z)
{
// Check if actor can move.
if (!_actor.isMovementDisabled())
{
// Set AI movement data.
_clientMovingToPawnOffset = 0;
// Calculate movement data for a move to location action and add the actor to movingObjects of GameTimeTaskManager.
_actor.moveToLocation(x, y, z, 0);
// Send a Server->Client packet MoveToLocation to the actor and all Player in its known players.
_actor.broadcastMoveToLocation();
}
else
{
clientActionFailed();
}
}
/**
* Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation <i>(broadcast)</i>.<br>
* <font color=#FF0000><b><u>Caution</u>: Low level function, used by AI subclasses</b></font>
* @param loc
*/
public void clientStopMoving(Location loc)
{
// Stop movement of the Creature.
if (_actor.isMoving())
{
_actor.stopMove(loc);
}
_clientMovingToPawnOffset = 0;
}
/**
* Client has already arrived to target, no need to force StopMove packet.
*/
protected void clientStoppedMoving()
{
if (_clientMovingToPawnOffset > 0) // movetoPawn needs to be stopped.
{
_clientMovingToPawnOffset = 0;
_actor.broadcastPacket(new StopMove(_actor));
}
}
public boolean isAutoAttacking()
{
return _clientAutoAttacking;
}
public void setAutoAttacking(boolean isAutoAttacking)
{
if (_actor.isSummon())
{
final Summon summon = _actor.asSummon();
if (summon.getOwner() != null)
{
summon.getOwner().getAI().setAutoAttacking(isAutoAttacking);
}
return;
}
_clientAutoAttacking = isAutoAttacking;
}
/**
* Start the actor Auto Attack client side by sending Server->Client packet AutoAttackStart <i>(broadcast)</i>.<br>
* <font color=#FF0000><b><u>Caution</u>: Low level function, used by AI subclasses</b></font>
*/
public void clientStartAutoAttack()
{
// Non attackable NPCs should not get in combat.
if (_actor.isNpc() && (!_actor.isAttackable() || _actor.isCoreAIDisabled()))
{
return;
}
if (_actor.isSummon())
{
final Summon summon = _actor.asSummon();
if (summon.getOwner() != null)
{
summon.getOwner().getAI().clientStartAutoAttack();
}
return;
}
if (!_clientAutoAttacking)
{
if (_actor.isPlayer())
{
final Player player = _actor.asPlayer();
if (player.hasSummon())
{
final Summon summon = player.getSummon();
summon.broadcastPacket(new AutoAttackStart(summon.getObjectId()));
}
}
// Send a Server->Client packet AutoAttackStart to the actor and all Player in its known players.
_actor.broadcastPacket(new AutoAttackStart(_actor.getObjectId()));
setAutoAttacking(true);
}
AttackStanceTaskManager.getInstance().addAttackStanceTask(_actor);
}
/**
* Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop <i>(broadcast)</i>.<br>
* <font color=#FF0000><b><u>Caution</u>: Low level function, used by AI subclasses</b></font>
*/
public void clientStopAutoAttack()
{
if (_actor.isSummon())
{
final Summon summon = _actor.asSummon();
if (summon.getOwner() != null)
{
summon.getOwner().getAI().clientStopAutoAttack();
}
return;
}
if (_actor.isPlayer())
{
if (!AttackStanceTaskManager.getInstance().hasAttackStanceTask(_actor) && isAutoAttacking())
{
AttackStanceTaskManager.getInstance().addAttackStanceTask(_actor);
}
}
else if (_clientAutoAttacking)
{
_actor.broadcastPacket(new AutoAttackStop(_actor.getObjectId()));
setAutoAttacking(false);
}
}
public int getClientMovingToPawnOffset()
{
return _clientMovingToPawnOffset;
}
/**
* Kill the actor client side by sending Server->Client packet AutoAttackStop, StopMove/StopRotation, Die <i>(broadcast)</i>.<br>
* <font color=#FF0000><b><u>Caution</u>: Low level function, used by AI subclasses</b></font>
*/
protected void clientNotifyDead()
{
// Send a Server->Client packet Die to the actor and all Player in its known players.
_actor.broadcastPacket(new Die(_actor));
// Init AI
_intention = Intention.IDLE;
_target = null;
_castTarget = null;
_attackTarget = null;
// Cancel the follow task if necessary.
stopFollow();
}
/**
* Update the state of this actor client side by sending Server->Client packet MoveToPawn/MoveToLocation and AutoAttackStart to the Player player.<br>
* <font color=#FF0000><b><u>Caution</u>: Low level function, used by AI subclasses</b></font>
* @param player The PlayerIstance to notify with state of this Creature
*/
public void describeStateToPlayer(Player player)
{
if (_actor.isVisibleFor(player) && _actor.isMoving())
{
if ((_clientMovingToPawnOffset != 0) && isFollowing())
{
// Send a Server->Client packet MoveToPawn to the actor and all Player in its known players.
player.sendPacket(new MoveToPawn(_actor, _followTarget, _clientMovingToPawnOffset));
}
else
{
// Send a Server->Client packet MoveToLocation to the actor and all Player in its known players.
player.sendPacket(new MoveToLocation(_actor));
}
}
}
public boolean isFollowing()
{
return (_followTarget != null) && _followTarget.isCreature() && ((_intention == Intention.FOLLOW) || CreatureFollowTaskManager.getInstance().isFollowing(_actor));
}
/**
* Create and Launch an AI Follow Task to execute every 1s.
* @param target The Creature to follow
*/
public void startFollow(Creature target)
{
startFollow(target, -1);
}
/**
* Create and Launch an AI Follow Task to execute every 0.5s, following at specified range.
* @param target The Creature to follow
* @param range
*/
public void startFollow(Creature target, int range)
{
stopFollow();
_followTarget = target;
if (range == -1)
{
CreatureFollowTaskManager.getInstance().addNormalFollow(_actor, range);
}
else
{
CreatureFollowTaskManager.getInstance().addAttackFollow(_actor, range);
}
}
/**
* Stop an AI Follow Task.
*/
public void stopFollow()
{
CreatureFollowTaskManager.getInstance().remove(_actor);
_followTarget = null;
}
public Creature getFollowTarget()
{
return _followTarget;
}
protected WorldObject getTarget()
{
return _target;
}
protected void setTarget(WorldObject target)
{
_target = target;
}
protected void setCastTarget(Creature target)
{
_castTarget = target;
}
public Creature getCastTarget()
{
return _castTarget;
}
protected void setAttackTarget(Creature target)
{
_attackTarget = target;
}
public Creature getAttackTarget()
{
return _attackTarget;
}
/**
* Stop all Ai tasks and futures.
*/
public void stopAITask()
{
stopFollow();
}
@Override
public String toString()
{
return "Actor: " + _actor;
}
}
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.ai;
/**
* Enum representing possible actions that can occur for an AI character.
*/
public enum Action
{
/** AI must decide the next action after a change. */
THINK,
/** Actor was attacked, may trigger a response. */
ATTACKED,
/** Increase/decrease aggression towards a target or reduce global aggression. */
AGGRESSION,
/** Actor is stunned and cannot act. */
STUNNED,
/** Actor is paralyzed or petrified and cannot move or act. */
PARALYZED,
/** Actor starts or stops sleeping. */
SLEEPING,
/** Actor is rooted and cannot move. */
ROOTED,
/** Actor evaded an attack. */
EVADED,
/** Previous action was completed, ready for the next. */
READY_TO_ACT,
/** User's command, such as using combat magic or changing weapons. */
USER_CMD,
/** Actor arrived at the assigned location. */
ARRIVED,
/** Actor arrived at an intermediate point and needs to revalidate destination. */
ARRIVED_REVALIDATE,
/** Actor cannot move further. */
ARRIVED_BLOCKED,
/** Actor forgets a specific object. */
FORGET_OBJECT,
/** Attempt to cancel the current step without changing intention. */
CANCEL,
/** Actor is dead. */
DEATH,
/** Actor appears to be dead but isn't. */
FAKE_DEATH,
/** Actor attacks randomly. */
CONFUSED,
/** Actor cannot cast spells. */
MUTED,
/** Actor flees in random directions. */
AFRAID,
/** Actor finishes casting a spell. */
FINISH_CASTING,
/** Actor betrays its master. */
BETRAYED
}
@@ -0,0 +1,183 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.ai;
import org.l2jmobius.gameserver.entity.Location;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.instance.Boat;
import org.l2jmobius.gameserver.mechanics.skill.Skill;
import org.l2jmobius.gameserver.network.serverpackets.VehicleDeparture;
import org.l2jmobius.gameserver.network.serverpackets.VehicleInfo;
import org.l2jmobius.gameserver.network.serverpackets.VehicleStarted;
/**
* @author DS, Mobius
*/
public class BoatAI extends CreatureAI
{
public BoatAI(Boat boat)
{
super(boat);
}
@Override
protected void moveTo(int x, int y, int z)
{
if (_actor.isMovementDisabled())
{
return;
}
if (!_actor.isMoving())
{
_actor.broadcastPacket(new VehicleStarted(getActor(), 1));
}
_actor.moveToLocation(x, y, z, 0);
_actor.broadcastPacket(new VehicleDeparture(getActor()));
}
@Override
public void clientStopMoving(Location loc)
{
if (_actor.isMoving())
{
_actor.stopMove(loc);
_actor.broadcastPacket(new VehicleStarted(getActor(), 0));
_actor.broadcastPacket(new VehicleInfo(getActor()));
return;
}
if (loc != null)
{
_actor.broadcastPacket(new VehicleStarted(getActor(), 0));
_actor.broadcastPacket(new VehicleInfo(getActor()));
}
}
@Override
public void describeStateToPlayer(Player player)
{
if (!_actor.isMoving())
{
return;
}
player.sendPacket(new VehicleDeparture(getActor()));
}
@Override
public void setIntentionAttack(WorldObject target)
{
}
@Override
public void setIntentionCast(Skill skill, WorldObject target)
{
}
@Override
public void setIntentionFollow(WorldObject target)
{
}
@Override
public void setIntentionPickUp(WorldObject item)
{
}
@Override
public void setIntentionInteract(WorldObject object)
{
}
@Override
public void notifyActionAttacked(WorldObject attacker)
{
}
@Override
public void notifyActionAggression(WorldObject target, int aggro)
{
}
@Override
public void notifyActionStunned()
{
}
@Override
public void notifyActionSleeping()
{
}
@Override
public void notifyActionRooted()
{
}
@Override
public void notifyActionForgetObject(WorldObject object)
{
}
@Override
public void notifyActionCancel()
{
}
@Override
public void notifyActionDeath()
{
}
@Override
public void notifyActionFakeDeath()
{
}
@Override
public void notifyActionFinishCasting()
{
}
@Override
protected void clientActionFailed()
{
}
@Override
public void moveToPawn(WorldObject pawn, int offset)
{
}
@Override
protected void clientStoppedMoving()
{
}
@Override
public Boat getActor()
{
return (Boat) _actor;
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.ai;
import org.l2jmobius.gameserver.entity.actor.Attackable;
import org.l2jmobius.gameserver.entity.actor.Creature;
import org.l2jmobius.gameserver.geoengine.GeoEngine;
/**
* @author Naker
*/
public class DistrustAI extends AttackableAI
{
private final Creature _forcedTarget;
public DistrustAI(Attackable actor, Creature forcedTarget)
{
super(actor);
_forcedTarget = forcedTarget;
}
@Override
public void thinkAttack()
{
if ((_forcedTarget == null) || _forcedTarget.isDead())
{
_actor.setTarget(null);
setIntentionActive();
return;
}
_actor.setTarget(_forcedTarget);
setIntentionAttack(_forcedTarget);
final int range = _actor.getPhysicalAttackRange() + _forcedTarget.getTemplate().getCollisionRadius();
if (_actor.calculateDistance3D(_forcedTarget) > range)
{
moveToPawn(_forcedTarget, range);
return;
}
if (!GeoEngine.getInstance().canSeeTarget(_actor, _forcedTarget))
{
return;
}
_actor.doAttack(_forcedTarget);
}
}
@@ -0,0 +1,175 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.ai;
import org.l2jmobius.commons.threads.ThreadPool;
import org.l2jmobius.gameserver.entity.Location;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.actor.Creature;
import org.l2jmobius.gameserver.entity.actor.instance.Defender;
import org.l2jmobius.gameserver.entity.actor.instance.Door;
import org.l2jmobius.gameserver.interfaces.ILocational;
import org.l2jmobius.gameserver.mechanics.skill.Skill;
/**
* @author mkizub, Mobius
*/
public class DoorAI extends CreatureAI
{
public DoorAI(Door door)
{
super(door);
}
@Override
public void setIntentionIdle()
{
}
@Override
public void setIntentionActive()
{
}
@Override
public void setIntentionRest()
{
}
@Override
public void setIntentionAttack(WorldObject target)
{
}
@Override
public void setIntentionCast(Skill skill, WorldObject target)
{
}
@Override
public void setIntentionMoveTo(ILocational destination)
{
}
@Override
public void setIntentionFollow(WorldObject target)
{
}
@Override
public void setIntentionPickUp(WorldObject item)
{
}
@Override
public void setIntentionInteract(WorldObject object)
{
}
@Override
public void notifyActionThink()
{
}
@Override
public void notifyActionAttacked(WorldObject attacker)
{
if (attacker == null)
{
return;
}
final Creature attackerCreature = attacker.asCreature();
if (attackerCreature == null)
{
return;
}
ThreadPool.execute(() -> World.forEachVisibleObject(_actor.asDoor(), Defender.class, guard ->
{
if (_actor.isInsideRadius3D(guard, guard.getTemplate().getClanHelpRange()))
{
guard.getAI().notifyActionAggression(attackerCreature, 15);
}
}));
}
@Override
public void notifyActionAggression(WorldObject target, int aggro)
{
}
@Override
public void notifyActionStunned()
{
}
@Override
public void notifyActionSleeping()
{
}
@Override
public void notifyActionRooted()
{
}
@Override
public void notifyActionReadyToAct()
{
}
@Override
public void notifyActionUserCmd(Object arg0, Object arg1)
{
}
@Override
public void notifyActionArrived()
{
}
@Override
public void notifyActionArrivedRevalidate()
{
}
@Override
public void notifyActionArrivedBlocked(Location blockedAtLoc)
{
}
@Override
public void notifyActionForgetObject(WorldObject object)
{
}
@Override
public void notifyActionCancel()
{
}
@Override
public void notifyActionDeath()
{
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.ai;
/**
* Enumeration of generic intentions of an NPC/PC, an intention may require several steps to be completed.
*/
public enum Intention
{
/** Do nothing; disconnect AI if no players are around. */
IDLE,
/** Alerted state without a goal: scan targets, random walk, etc. */
ACTIVE,
/** Rest (sit until attacked). */
REST,
/** Attack target (cast combat magic, go to target, combat). */
ATTACK,
/** Cast a spell; may start or stop attacking depending on the spell. */
CAST,
/** Move to another location. */
MOVE_TO,
/** Follow a target, adjusting movement based on the target's actions. */
FOLLOW,
/** Pick up an item (go to item, pick it up, then become idle). */
PICK_UP,
/** Move to target and then interact with it. */
INTERACT;
}
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.ai;
/**
* Represents a queued action that can be triggered by a specific {@link Action} and removed by a specific {@link Intention}.<br>
* When triggered, it executes a callback provided by the {@link Callback} interface.
* @author Mobius
*/
public class NextAction
{
/**
* A callback interface that defines the behavior to execute when the next action is triggered.
*/
public interface Callback
{
void doAction();
}
private final Action _action;
private final Intention _intention;
private final Callback _callback;
/**
* Constructs a new NextAction with the specified action, intention and callback.
* @param action The {@link Action} that will trigger this next action.
* @param intention The {@link Intention} that can remove this next action.
* @param callback The {@link Callback} that will be executed when this next action is triggered.
*/
public NextAction(Action action, Intention intention, Callback callback)
{
_action = action;
_intention = intention;
_callback = callback;
}
/**
* Checks if this next action can be triggered by the specified {@link Action}.
* @param action The {@link Action} to check.
* @return if the provided action matches the action associated with this next action.
*/
public boolean isTriggeredBy(Action action)
{
return _action == action;
}
/**
* Checks if this next action can be removed by the specified {@link Intention}.
* @param intention The {@link Intention} to check.
* @return if the provided intention matches the intention associated with this next action.
*/
public boolean isRemovedBy(Intention intention)
{
return _intention == intention;
}
/**
* Executes the next action by invoking the {@link Callback#doAction()} method.
*/
public void doAction()
{
_callback.doAction();
}
}
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.ai;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.actor.Playable;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.zone.ZoneId;
import org.l2jmobius.gameserver.mechanics.skill.Skill;
import org.l2jmobius.gameserver.network.SystemMessageId;
/**
* This class manages AI of Playable.<br>
* PlayableAI:
* <li>SummonAI</li>
* <li>PlayerAI</li>
* @author JIV, Mobius
*/
public abstract class PlayableAI extends CreatureAI
{
protected PlayableAI(Playable playable)
{
super(playable);
}
@Override
public void setIntentionAttack(WorldObject target)
{
if ((target != null) && target.isPlayable())
{
final Player player = _actor.asPlayer();
final Player targetPlayer = target.asPlayer();
if ((player != null) && (targetPlayer != null))
{
if (targetPlayer.isProtectionBlessingAffected() && ((player.getLevel() - targetPlayer.getLevel()) >= 10) && (player.getKarma() > 0) && !(target.isInsideZone(ZoneId.PVP)))
{
// If attacker have karma and have level >= 10 than his target and target have Newbie Protection Buff.
player.sendPacket(SystemMessageId.THAT_IS_THE_INCORRECT_TARGET);
clientActionFailed();
return;
}
if (player.isProtectionBlessingAffected() && ((targetPlayer.getLevel() - player.getLevel()) >= 10) && (targetPlayer.getKarma() > 0) && !(target.isInsideZone(ZoneId.PVP)))
{
// If target have karma and have level >= 10 than his target and actor have Newbie Protection Buff.
player.sendPacket(SystemMessageId.THAT_IS_THE_INCORRECT_TARGET);
clientActionFailed();
return;
}
if (targetPlayer.isCursedWeaponEquipped() && (player.getLevel() <= 20))
{
player.sendPacket(SystemMessageId.THAT_IS_THE_INCORRECT_TARGET);
clientActionFailed();
return;
}
if (player.isCursedWeaponEquipped() && (targetPlayer.getLevel() <= 20))
{
player.sendPacket(SystemMessageId.THAT_IS_THE_INCORRECT_TARGET);
clientActionFailed();
return;
}
}
}
super.setIntentionAttack(target);
}
@Override
public void setIntentionCast(Skill skill, WorldObject target)
{
if ((target != null) && (target.isPlayable()) && (skill != null) && skill.hasNegativeEffect())
{
final Player player = _actor.asPlayer();
final Player targetPlayer = target.asPlayer();
if ((player != null) && (targetPlayer != null))
{
if (targetPlayer.isProtectionBlessingAffected() && ((player.getLevel() - targetPlayer.getLevel()) >= 10) && (player.getKarma() > 0) && !target.isInsideZone(ZoneId.PVP))
{
// If attacker have karma and have level >= 10 than his target and target have Newbie Protection Buff.
player.sendPacket(SystemMessageId.THAT_IS_THE_INCORRECT_TARGET);
clientActionFailed();
return;
}
if (player.isProtectionBlessingAffected() && ((targetPlayer.getLevel() - player.getLevel()) >= 10) && (targetPlayer.getKarma() > 0) && !target.isInsideZone(ZoneId.PVP))
{
// If target have karma and have level >= 10 than his target and actor have Newbie Protection Buff.
player.sendPacket(SystemMessageId.THAT_IS_THE_INCORRECT_TARGET);
clientActionFailed();
return;
}
if (targetPlayer.isCursedWeaponEquipped() && ((player.getLevel() <= 20) || (targetPlayer.getLevel() <= 20)))
{
player.sendPacket(SystemMessageId.THAT_IS_THE_INCORRECT_TARGET);
clientActionFailed();
return;
}
}
}
super.setIntentionCast(skill, target);
}
}
@@ -0,0 +1,486 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.ai;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.actor.Creature;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.holders.player.Duel;
import org.l2jmobius.gameserver.entity.actor.instance.StaticObject;
import org.l2jmobius.gameserver.interfaces.ILocational;
import org.l2jmobius.gameserver.mechanics.skill.Skill;
import org.l2jmobius.gameserver.mechanics.skill.targets.TargetType;
import org.l2jmobius.gameserver.network.SystemMessageId;
import org.l2jmobius.gameserver.network.serverpackets.SystemMessage;
public class PlayerAI extends PlayableAI
{
private boolean _thinking; // To prevent recursive thinking.
// Saved intention to replay after a CAST completes (typed fields — no IntentionCommand).
private Intention _savedIntention = null;
private WorldObject _savedAttackTarget = null;
private ILocational _savedMoveTo = null;
private WorldObject _savedFollowTarget = null;
private WorldObject _savedPickUpTarget = null;
private WorldObject _savedInteractTarget = null;
private Skill _savedCastSkill = null;
private WorldObject _savedCastTarget = null;
public PlayerAI(Player player)
{
super(player);
}
@Override
public Intention getNextIntention()
{
return _savedIntention;
}
private void clearSavedIntention()
{
_savedIntention = null;
_savedAttackTarget = null;
_savedMoveTo = null;
_savedFollowTarget = null;
_savedPickUpTarget = null;
_savedInteractTarget = null;
_savedCastSkill = null;
_savedCastTarget = null;
}
/**
* Saves the current intention so it can be replayed once the upcoming CAST resolves.
*/
private void saveCurrentIntentionForCast()
{
_savedIntention = _intention;
_savedAttackTarget = getAttackTarget();
_savedCastSkill = _skill;
_savedCastTarget = getCastTarget();
// Other typed targets are restored on replay via the same getters when possible.
_savedMoveTo = null;
_savedFollowTarget = getFollowTarget();
_savedPickUpTarget = null;
_savedInteractTarget = null;
}
private void replaySavedIntention()
{
if (_savedIntention == null)
{
return;
}
final Intention intention = _savedIntention;
final WorldObject attackTarget = _savedAttackTarget;
final ILocational moveToLoc = _savedMoveTo;
final WorldObject followTarget = _savedFollowTarget;
final WorldObject pickUpTarget = _savedPickUpTarget;
final WorldObject interactTarget = _savedInteractTarget;
final Skill castSkill = _savedCastSkill;
final WorldObject castTarget = _savedCastTarget;
clearSavedIntention();
switch (intention)
{
case IDLE:
{
setIntentionIdle();
break;
}
case ACTIVE:
{
setIntentionActive();
break;
}
case REST:
{
setIntentionRest();
break;
}
case ATTACK:
{
setIntentionAttack(attackTarget);
break;
}
case CAST:
{
if (castSkill != null)
{
setIntentionCast(castSkill, castTarget);
}
break;
}
case MOVE_TO:
{
if (moveToLoc != null)
{
setIntentionMoveTo(moveToLoc);
}
break;
}
case FOLLOW:
{
setIntentionFollow(followTarget);
break;
}
case PICK_UP:
{
setIntentionPickUp(pickUpTarget);
break;
}
case INTERACT:
{
setIntentionInteract(interactTarget);
break;
}
}
}
/**
* Launch actions corresponding to the Action ReadyToAct.<br>
* <br>
* <b><u>Actions</u>:</b>
* <ul>
* <li>Launch actions corresponding to the Action Think</li>
* </ul>
*/
@Override
public void notifyActionReadyToAct()
{
// Replay any saved intention from before a CAST.
if (_savedIntention != null)
{
replaySavedIntention();
}
super.notifyActionReadyToAct();
}
@Override
public void notifyActionForgetObject(WorldObject object)
{
if ((object != null) && object.isPlayer())
{
getActor().getKnownRelations().remove(object.getObjectId());
}
super.notifyActionForgetObject(object);
}
/**
* Launch actions corresponding to the Action Cancel.<br>
* <br>
* <b><u>Actions</u>:</b>
* <ul>
* <li>Stop an AI Follow Task</li>
* <li>Launch actions corresponding to the Action Think</li>
* </ul>
*/
@Override
public void notifyActionCancel()
{
clearSavedIntention();
super.notifyActionCancel();
}
/**
* Finalize the casting of a skill. This method overrides CreatureAI method.<br>
* <b>What it does:</b><br>
* Check if actual intention is set to CAST and, if so, retrieves latest intention before the actual CAST and set it as the current intention for the player.
*/
@Override
public void notifyActionFinishCasting()
{
if (getIntention() == Intention.CAST)
{
// Run interrupted or next intention.
if (_savedIntention != null)
{
if (_savedIntention != Intention.CAST)
{
replaySavedIntention();
}
else
{
clearSavedIntention();
setIntentionIdle();
}
}
else
{
// Set intention to idle if skill doesn't change intention.
setIntentionIdle();
}
}
super.notifyActionFinishCasting();
}
@Override
public void setIntentionRest()
{
if (getIntention() == Intention.REST)
{
return;
}
clearSavedIntention();
_intention = Intention.REST;
setTarget(null);
if (getAttackTarget() != null)
{
setAttackTarget(null);
}
clientStopMoving(null);
}
@Override
public void setIntentionActive()
{
setIntentionIdle();
}
/**
* Manage the Move To Intention : Stop current Attack and Launch a Move to Location Task.<br>
* <br>
* <b><u>Actions</u> : </b>
* <ul>
* <li>Stop the actor auto-attack server side AND client side by sending Server->Client packet AutoAttackStop (broadcast)</li>
* <li>Set the Intention of this AI to MOVE_TO</li>
* <li>Move the actor to Location (x,y,z) server side AND client side by sending Server->Client packet MoveToLocation (broadcast)</li>
* </ul>
*/
@Override
public void setIntentionMoveTo(ILocational loc)
{
if (getIntention() == Intention.REST)
{
// Cancel action client side by sending Server->Client packet ActionFailed to the Player actor.
clientActionFailed();
return;
}
final Player player = _actor.asPlayer();
if (player.getDuelState() == Duel.DUELSTATE_DEAD)
{
clientActionFailed();
player.sendPacket(new SystemMessage(SystemMessageId.YOU_CANNOT_MOVE_WHILE_FROZEN_PLEASE_WAIT));
return;
}
if (_actor.isAllSkillsDisabled() || _actor.isCastingNow() || _actor.isAttackingNow())
{
clientActionFailed();
// Save the move-to as the next intention to replay once ready.
_savedIntention = Intention.MOVE_TO;
_savedMoveTo = loc;
return;
}
stopFollow();
// Set the Intention of this AbstractAI to MOVE_TO.
clearSavedIntention();
_intention = Intention.MOVE_TO;
// Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast).
clientStopAutoAttack();
// Abort the attack of the Creature and send Server->Client ActionFailed packet.
_actor.abortAttack();
// Move the actor to Location (x,y,z) server side AND client side by sending Server->Client packet MoveToLocation (broadcast).
moveTo(loc.getX(), loc.getY(), loc.getZ());
}
@Override
public void setIntentionCast(Skill skill, WorldObject target)
{
// Forget next if it's not cast or it's cast and skill is toggle.
if ((skill == null) || !skill.isToggle())
{
// New non-toggle cast: clear any stale saved intention and remember the current one if it differs.
if (_intention != Intention.CAST)
{
saveCurrentIntentionForCast();
}
}
super.setIntentionCast(skill, target);
}
@Override
protected void clientNotifyDead()
{
_clientMovingToPawnOffset = 0;
super.clientNotifyDead();
}
private void thinkAttack()
{
final Creature target = getAttackTarget();
if (target == null)
{
return;
}
if (checkTargetLostOrDead(target))
{
// Notify the target
setAttackTarget(null);
return;
}
if (maybeMoveToPawn(target, _actor.getPhysicalAttackRange()))
{
return;
}
clientStopMoving(null);
_actor.doAttack(target);
}
private void thinkCast()
{
final Creature target = getCastTarget();
if ((_skill.getTargetType() == TargetType.GROUND) && _actor.isPlayer())
{
if (maybeMoveToPosition(_actor.asPlayer().getCurrentSkillWorldPosition(), _actor.getMagicalAttackRange(_skill)))
{
_actor.setCastingNow(false);
return;
}
}
else
{
if (checkTargetLost(target))
{
if (_skill.hasNegativeEffect() && (getAttackTarget() != null))
{
// Notify the target
setCastTarget(null);
}
_actor.setCastingNow(false);
return;
}
if ((target != null) && maybeMoveToPawn(target, _actor.getMagicalAttackRange(_skill)))
{
_actor.setCastingNow(false);
return;
}
}
if ((_skill.getHitTime() > 50) && !_skill.isSimultaneousCast())
{
clientStopMoving(null);
}
// Check if target has changed.
final WorldObject currentTarget = _actor.getTarget();
if ((currentTarget != target) && (currentTarget != null) && (target != null))
{
_actor.setTarget(target);
_actor.doCast(_skill);
_actor.setTarget(currentTarget);
return;
}
_actor.doCast(_skill);
}
private void thinkPickUp()
{
if (_actor.isAllSkillsDisabled() || _actor.isCastingNow())
{
return;
}
final WorldObject target = getTarget();
if (checkTargetLost(target) || maybeMoveToPawn(target, 36))
{
return;
}
setIntentionIdle();
_actor.asPlayer().doPickupItem(target);
}
private void thinkInteract()
{
if (_actor.isAllSkillsDisabled() || _actor.isCastingNow())
{
return;
}
final WorldObject target = getTarget();
if (checkTargetLost(target) || maybeMoveToPawn(target, 36))
{
return;
}
if (!(target instanceof StaticObject))
{
_actor.asPlayer().doInteract(target.asCreature());
}
setIntentionIdle();
}
@Override
public void notifyActionThink()
{
if (_thinking && (getIntention() != Intention.CAST))
{
return;
}
_thinking = true;
try
{
if (getIntention() == Intention.ATTACK)
{
thinkAttack();
}
else if (getIntention() == Intention.CAST)
{
thinkCast();
}
else if (getIntention() == Intention.PICK_UP)
{
thinkPickUp();
}
else if (getIntention() == Intention.INTERACT)
{
thinkInteract();
}
}
finally
{
_thinking = false;
}
}
}
@@ -0,0 +1,866 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.ai;
import java.util.Collection;
import java.util.concurrent.Future;
import org.l2jmobius.commons.threads.ThreadPool;
import org.l2jmobius.commons.util.Rnd;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.actor.Attackable;
import org.l2jmobius.gameserver.entity.actor.Creature;
import org.l2jmobius.gameserver.entity.actor.Npc;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.instance.Defender;
import org.l2jmobius.gameserver.geoengine.GeoEngine;
import org.l2jmobius.gameserver.interfaces.ILocational;
import org.l2jmobius.gameserver.mechanics.effects.EffectType;
import org.l2jmobius.gameserver.mechanics.skill.Skill;
import org.l2jmobius.gameserver.taskmanagers.GameTimeTaskManager;
import org.l2jmobius.gameserver.util.LocationUtil;
/**
* This class manages AI of Attackable.
*/
public class SiegeGuardAI extends CreatureAI implements Runnable
{
private static final int MAX_ATTACK_TIMEOUT = 300; // int ticks, i.e. 30 seconds
/** The Attackable AI task executed every 1s (call onActionThink method) */
private Future<?> _aiTask;
/** For attack AI, analysis of mob and its targets */
private final SelfAnalysis _selfAnalysis = new SelfAnalysis();
// private TargetAnalysis _mostHatedAnalysis = new TargetAnalysis();
/** The delay after which the attacked is stopped */
private int _attackTimeout;
/** The Attackable aggro counter */
private int _globalAggro;
/** The flag used to indicate that a thinking action is in progress */
private boolean _thinking; // to prevent recursive thinking
private final int _attackRange;
/**
* Constructor of AttackableAI.
* @param creature the creature
*/
public SiegeGuardAI(Defender creature)
{
super(creature);
_selfAnalysis.init();
_attackTimeout = Integer.MAX_VALUE;
_globalAggro = -10; // 10 seconds timeout of ATTACK after respawn.
_attackRange = _actor.getPhysicalAttackRange();
}
@Override
public void run()
{
// Launch actions corresponding to the Action Think.
notifyActionThink();
}
/**
* <b><u>Actor is a GuardInstance</u>:</b>
* <ul>
* <li>The target isn't a Folk or a Door</li>
* <li>The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)</li>
* <li>The target is in the actor Aggro range and is at the same height</li>
* <li>The Player target has karma (=PK)</li>
* <li>The Monster target is aggressive</li>
* </ul>
* <br>
* <b><u>Actor is a SiegeGuard</u>:</b>
* <ul>
* <li>The target isn't a Folk or a Door</li>
* <li>The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)</li>
* <li>The target is in the actor Aggro range and is at the same height</li>
* <li>A siege is in progress</li>
* <li>The Player target isn't a Defender</li>
* </ul>
* <br>
* <b><u>Actor is a FriendlyMob</u>:</b>
* <ul>
* <li>The target isn't a Folk, a Door or another Npc</li>
* <li>The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)</li>
* <li>The target is in the actor Aggro range and is at the same height</li>
* <li>The Player target has karma (=PK)</li>
* </ul>
* <br>
* <b><u>Actor is a Monster</u>:</b>
* <ul>
* <li>The target isn't a Folk, a Door or another Npc</li>
* <li>The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)</li>
* <li>The target is in the actor Aggro range and is at the same height</li>
* <li>The actor is Aggressive</li>
* </ul>
* @param target The targeted WorldObject
* @return True if the target is autoattackable (depends on the actor type).
*/
protected boolean autoAttackCondition(Creature target)
{
// Check if the target isn't another guard, folk or a door.
if ((target == null) || (target instanceof Defender) || target.isNpc() || target.isDoor() || target.isAlikeDead())
{
return false;
}
// Check if the target isn't invulnerable.
if (target.isInvul() && ((target.isPlayer() && target.isGM()) || (target.isSummon() && target.asSummon().getOwner().isGM())))
{
return false;
}
// Get the owner if the target is a summon.
Creature currentTarget = target;
if (currentTarget.isSummon())
{
final Player owner = currentTarget.asSummon().getOwner();
if (_actor.isInsideRadius3D(owner, 1000))
{
currentTarget = owner;
}
}
// Check if the target isn't in silent move mode AND too far (>100).
if (currentTarget.isPlayable() && currentTarget.asPlayable().isSilentMovingAffected() && !_actor.isInsideRadius2D(currentTarget, 250))
{
return false;
}
// Los Check Here
return (_actor.isAutoAttackable(currentTarget) && GeoEngine.getInstance().canSeeTarget(_actor, currentTarget));
}
private void startAITask()
{
if (_aiTask == null)
{
_aiTask = ThreadPool.scheduleAtFixedRate(this, 1000, 1000);
}
}
private boolean shouldPromoteIdleToActive()
{
if (_actor.isAlikeDead())
{
return false;
}
return World.getFirstVisibleObject(_actor.asAttackable(), Player.class) != null;
}
@Override
public synchronized void setIntentionIdle()
{
if (shouldPromoteIdleToActive())
{
setIntentionActive();
return;
}
super.setIntentionIdle();
// Stop AI task and detach AI from NPC.
if (_aiTask != null)
{
_aiTask.cancel(true);
_aiTask = null;
}
// Cancel the AI
_actor.detachAI();
}
@Override
public synchronized void setIntentionActive()
{
super.setIntentionActive();
startAITask();
}
@Override
public synchronized void setIntentionRest()
{
super.setIntentionRest();
startAITask();
}
/**
* Manage the Attack Intention : Stop current Attack (if necessary), Calculate attack timeout, Start a new Attack and Launch Think Action.
* @param target The WorldObject to attack
*/
@Override
public synchronized void setIntentionAttack(WorldObject target)
{
// Calculate the attack timeout.
_attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeTaskManager.getInstance().getGameTicks();
// Manage the Attack Intention : Stop current Attack (if necessary), Start a new Attack and Launch Think Action.
super.setIntentionAttack(target);
startAITask();
}
@Override
public synchronized void setIntentionCast(Skill skill, WorldObject target)
{
super.setIntentionCast(skill, target);
startAITask();
}
@Override
public synchronized void setIntentionMoveTo(ILocational destination)
{
super.setIntentionMoveTo(destination);
startAITask();
}
@Override
public synchronized void setIntentionFollow(WorldObject target)
{
super.setIntentionFollow(target);
startAITask();
}
@Override
public synchronized void setIntentionPickUp(WorldObject item)
{
super.setIntentionPickUp(item);
startAITask();
}
@Override
public synchronized void setIntentionInteract(WorldObject object)
{
super.setIntentionInteract(object);
startAITask();
}
/**
* Manage AI standard thinks of a Attackable (called by onActionThink).<br>
* <br>
* <b><u>Actions</u>:</b>
* <ul>
* <li>Update every 1s the _globalAggro counter to come close to 0</li>
* <li>If the actor is Aggressive and can attack, add all autoAttackable Creature in its Aggro Range to its _aggroList, chose a target and order to attack it</li>
* <li>If the actor can't attack, order to it to return to its home location</li>
* </ul>
*/
private void thinkActive()
{
final Attackable npc = _actor.asAttackable();
// Update every 1s the _globalAggro counter to come close to 0.
if (_globalAggro != 0)
{
if (_globalAggro < 0)
{
_globalAggro++;
}
else
{
_globalAggro--;
}
}
// Add all autoAttackable Creature in Attackable Aggro Range to its _aggroList with 0 damage and 1 hate.
// A Attackable isn't aggressive during 10s after its spawn because _globalAggro is set to -10.
if (_globalAggro >= 0)
{
World.forEachVisibleObjectInRange(npc, Creature.class, _attackRange, target ->
{
if (autoAttackCondition(target) && (npc.getHating(target) == 0)) // check aggression
{
npc.addDamageHate(target, 0, 1);
}
});
// Chose a target from its aggroList.
final Creature hated = _actor.isConfused() ? getAttackTarget() : npc.getMostHated();
// Order to the Attackable to attack the target.
if (hated != null)
{
// Get the hate level of the Attackable against this Creature target contained in _aggroList.
final long aggro = npc.getHating(hated);
if ((aggro + _globalAggro) > 0)
{
// Set the Creature movement type to run and send Server->Client packet ChangeMoveType to all others Player.
if (!_actor.isRunning())
{
_actor.setRunning();
}
// Set the AI Intention to ATTACK.
setIntentionAttack(hated);
}
return;
}
}
// Order to the Defender to return to its home location because there's no target to attack.
((Defender) _actor).returnHome();
}
/**
* Manage AI attack thinks of a Attackable (called by onActionThink).<br>
* <br>
* <b><u>Actions</u>:</b>
* <ul>
* <li>Update the attack timeout if actor is running</li>
* <li>If target is dead or timeout is expired, stop this attack and set the Intention to ACTIVE</li>
* <li>Call all WorldObject of its Faction inside the Faction Range</li>
* <li>Chose a target and order to attack it with magic skill or physical attack</li>
* </ul>
* TODO: Manage casting rules to healer mobs (like Ant Nurses)
*/
private void thinkAttack()
{
if ((_attackTimeout < GameTimeTaskManager.getInstance().getGameTicks()) && _actor.isRunning())
{
// Set the actor movement type to walk and send Server->Client packet ChangeMoveType to all others Player.
_actor.setWalking();
// Calculate a new attack timeout.
_attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeTaskManager.getInstance().getGameTicks();
}
final Creature attackTarget = getAttackTarget();
// Check if target is dead or if timeout is expired to stop this attack.
if ((attackTarget == null) || attackTarget.isAlikeDead() || (_attackTimeout < GameTimeTaskManager.getInstance().getGameTicks()))
{
// Stop hating this target after the attack timeout or if target is dead.
if (attackTarget != null)
{
_actor.asAttackable().stopHating(attackTarget);
}
// Cancel target and timeout.
_attackTimeout = Integer.MAX_VALUE;
setAttackTarget(null);
// Set the AI Intention to ACTIVE.
setIntentionActive();
_actor.setWalking();
return;
}
factionNotifyAndSupport();
attackPrepare();
}
private void factionNotifyAndSupport()
{
final Creature target = getAttackTarget();
// Call all WorldObject of its Faction inside the Faction Range.
if ((_actor.asNpc().getTemplate().getClans() == null) || (target == null) || target.isInvul())
{
return;
}
// Go through all Creature that belong to its faction.
// for (Creature creature : _actor.getKnownList().getKnownCharactersInRadius(_actor.asNpc().getFactionRange()+_actor.getTemplate().collisionRadius))
for (Creature creature : World.getVisibleObjectsInRange(_actor, Creature.class, 1000))
{
if (!(creature instanceof Npc))
{
if (_selfAnalysis.hasHealOrResurrect && creature.isPlayer() && (_actor.asNpc().getCastle().getSiege().checkIsDefender(creature.asPlayer().getClan()))//
&& !_actor.isAttackDisabled() && (creature.getCurrentHp() < (creature.getMaxHp() * 0.6)) && (_actor.getCurrentHp() > (_actor.getMaxHp() / 2)) && (_actor.getCurrentMp() > (_actor.getMaxMp() / 2)) && creature.isInCombat())
{
for (Skill sk : _selfAnalysis.healSkills)
{
if ((_actor.getCurrentMp() < sk.getMpConsume()) || _actor.isSkillDisabled(sk) || !LocationUtil.checkIfInRange(sk.getCastRange(), _actor, creature, true))
{
continue;
}
final int chance = 5;
if (chance >= Rnd.get(100))
{
continue;
}
if (!GeoEngine.getInstance().canSeeTarget(_actor, creature))
{
break;
}
final WorldObject oldTarget = _actor.getTarget();
_actor.setTarget(creature);
clientStopMoving(null);
_actor.doCast(sk);
_actor.setTarget(oldTarget);
return;
}
}
continue;
}
final Npc npc = creature.asNpc();
if (!npc.isInMyClan(_actor.asNpc()))
{
continue;
}
if (npc.getAI() != null) // TODO: possibly check not needed
{
if (!npc.isDead() && (Math.abs(target.getZ() - npc.getZ()) < 600) && ((npc.getAI()._intention == Intention.IDLE) || (npc.getAI()._intention == Intention.ACTIVE)) && target.isInsideRadius3D(npc, 1500) && GeoEngine.getInstance().canSeeTarget(npc, target))
{
// Notify the WorldObject AI with AGGRESSION.
npc.getAI().notifyActionAggression(getAttackTarget(), 1);
return;
}
// heal friends
if (_selfAnalysis.hasHealOrResurrect && !_actor.isAttackDisabled() && (npc.getCurrentHp() < (npc.getMaxHp() * 0.6)) && (_actor.getCurrentHp() > (_actor.getMaxHp() / 2)) && (_actor.getCurrentMp() > (_actor.getMaxMp() / 2)) && npc.isInCombat())
{
for (Skill sk : _selfAnalysis.healSkills)
{
if ((_actor.getCurrentMp() < sk.getMpConsume()) || _actor.isSkillDisabled(sk) || !LocationUtil.checkIfInRange(sk.getCastRange(), _actor, npc, true))
{
continue;
}
final int chance = 4;
if (chance >= Rnd.get(100))
{
continue;
}
if (!GeoEngine.getInstance().canSeeTarget(_actor, npc))
{
break;
}
final WorldObject oldTarget = _actor.getTarget();
_actor.setTarget(npc);
clientStopMoving(null);
_actor.doCast(sk);
_actor.setTarget(oldTarget);
return;
}
}
}
}
}
private void attackPrepare()
{
// Get all information needed to choose between physical or magical attack.
Collection<Skill> skills = null;
double distance = 0;
int range = 0;
Creature attackTarget = getAttackTarget();
try
{
_actor.setTarget(attackTarget);
skills = _actor.getAllSkills();
distance = _actor.calculateDistance2D(attackTarget);
range = _actor.getPhysicalAttackRange() + _actor.getTemplate().getCollisionRadius() + attackTarget.getTemplate().getCollisionRadius();
if (attackTarget.isMoving())
{
range += 50;
}
}
catch (NullPointerException e)
{
_actor.setTarget(null);
setIntentionIdle();
return;
}
// never attack defenders
final Defender sGuard = (Defender) _actor;
if (attackTarget.isPlayer() && (sGuard.getConquerableHall() == null) && sGuard.getCastle().getSiege().checkIsDefender(attackTarget.asPlayer().getClan()))
{
// Cancel the target
sGuard.stopHating(attackTarget);
_actor.setTarget(null);
setIntentionIdle();
return;
}
if (!GeoEngine.getInstance().canSeeTarget(_actor, attackTarget))
{
// Siege guards differ from normal mobs currently:
// If target cannot seen, don't attack any more.
sGuard.stopHating(attackTarget);
_actor.setTarget(null);
setIntentionIdle();
return;
}
// Check if the actor isn't muted and if it is far from target.
if (!_actor.isMuted() && (distance > range))
{
// Check for long ranged skills and heal/buff skills.
for (Skill sk : skills)
{
final int castRange = sk.getCastRange();
if ((distance <= castRange) && (castRange > 70) && !_actor.isSkillDisabled(sk) && (_actor.getCurrentMp() >= _actor.getStat().getMpConsume(sk)) && !sk.isPassive())
{
final WorldObject oldTarget = _actor.getTarget();
if ((sk.isContinuous() && !sk.isDebuff()) || sk.hasEffectType(EffectType.HEAL))
{
if (sk.hasEffectType(EffectType.HEAL) && (_actor.getCurrentHp() > (int) (_actor.getMaxHp() / 1.5)))
{
break;
}
boolean useSkillSelf = true;
if (sk.isContinuous() && !sk.isDebuff() && _actor.isAffectedBySkill(sk.getId()))
{
useSkillSelf = false;
}
if (useSkillSelf)
{
_actor.setTarget(_actor);
}
}
clientStopMoving(null);
_actor.doCast(sk);
_actor.setTarget(oldTarget);
return;
}
}
// Check if the SiegeGuard is attacking, knows the target and can't run.
if (!(_actor.isAttackingNow()) && (_actor.getRunSpeed() == 0) && (_actor.isInSurroundingRegion(attackTarget)))
{
// Cancel the target
_actor.setTarget(null);
setIntentionIdle();
}
else if (sGuard.getSpawn() != null)
{
final double dx = _actor.getX() - attackTarget.getX();
final double dy = _actor.getY() - attackTarget.getY();
final double dz = _actor.getZ() - attackTarget.getZ();
final double homeX = attackTarget.getX() - sGuard.getSpawn().getX();
final double homeY = attackTarget.getY() - sGuard.getSpawn().getY();
// Check if the SiegeGuard isn't too far from it's home location.
if ((((dx * dx) + (dy * dy)) > 10000) && (((homeX * homeX) + (homeY * homeY)) > 3240000) // 1800 * 1800
&& (_actor.isInSurroundingRegion(attackTarget)))
{
// Cancel the target
_actor.setTarget(null);
setIntentionIdle();
}
// Temporary hack for preventing guards jumping off towers,
// before replacing this with effective geodata checks and AI modification
else if ((dz * dz) < (170 * 170)) // normally 130 if guard z coordinates correct
{
if (_selfAnalysis.isHealer)
{
return;
}
if (_selfAnalysis.isMage)
{
range = _selfAnalysis.maxCastRange - 50;
}
moveToPawn(attackTarget, attackTarget.isMoving() ? range - 70 : range);
}
}
}
else
{
if (_actor.isMuted() && (distance > range) && !_selfAnalysis.isHealer)
{
// Temporary hack for preventing guards jumping off towers,
// before replacing this with effective geodata checks and AI modification
final double dz = _actor.getZ() - attackTarget.getZ();
if ((dz * dz) < (170 * 170)) // normally 130 if guard z coordinates correct
{
if (_selfAnalysis.isMage)
{
range = _selfAnalysis.maxCastRange - 50;
}
moveToPawn(attackTarget, attackTarget.isMoving() ? range - 70 : range);
}
return;
}
if (distance <= range)
{
final Creature hated = _actor.isConfused() ? attackTarget : _actor.asAttackable().getMostHated();
if (hated == null)
{
setIntentionActive();
return;
}
if (hated != attackTarget)
{
attackTarget = hated;
}
_attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeTaskManager.getInstance().getGameTicks();
// check for close combat skills && heal/buff skills
if (!_actor.isMuted() && (Rnd.get(100) <= 5))
{
for (Skill sk : skills)
{
final int castRange = sk.getCastRange();
if ((castRange >= distance) && !sk.isPassive() && (_actor.getCurrentMp() >= _actor.getStat().getMpConsume(sk)) && !_actor.isSkillDisabled(sk))
{
final WorldObject oldTarget = _actor.getTarget();
if ((sk.isContinuous() && !sk.isDebuff()) || sk.hasEffectType(EffectType.HEAL))
{
if (sk.hasEffectType(EffectType.HEAL) && (_actor.getCurrentHp() > (int) (_actor.getMaxHp() / 1.5)))
{
break;
}
boolean useSkillSelf = true;
if (sk.isContinuous() && !sk.isDebuff() && _actor.isAffectedBySkill(sk.getId()))
{
useSkillSelf = false;
}
if (useSkillSelf)
{
_actor.setTarget(_actor);
}
}
clientStopMoving(null);
_actor.doCast(sk);
_actor.setTarget(oldTarget);
return;
}
}
}
// Finally, do the physical attack itself.
if (!_selfAnalysis.isHealer)
{
_actor.doAttack(attackTarget);
}
}
}
}
/**
* Manage AI thinking actions of a Attackable.
*/
@Override
public void notifyActionThink()
{
// if(getIntention() != Intention.IDLE && (!_actor.isSpawned() || !_actor.hasAI() || !_actor.isKnownPlayers()))
// setIntentionIdle();
// Check if the thinking action is already in progress.
if (_thinking || _actor.isCastingNow() || _actor.isAllSkillsDisabled())
{
return;
}
// Start thinking action
_thinking = true;
try
{
// Manage AI thinks of a Attackable.
if (getIntention() == Intention.ACTIVE)
{
thinkActive();
}
else if (getIntention() == Intention.ATTACK)
{
thinkAttack();
}
}
finally
{
// Stop thinking action
_thinking = false;
}
}
/**
* Launch actions corresponding to the Action Attacked.<br>
* <br>
* <b><u>Actions</u>:</b>
* <ul>
* <li>Init the attack : Calculate the attack timeout, Set the _globalAggro to 0, Add the attacker to the actor _aggroList</li>
* <li>Set the Creature movement type to run and send Server->Client packet ChangeMoveType to all others Player</li>
* <li>Set the Intention to ATTACK</li>
* </ul>
* @param attackerObj The WorldObject that attacks the actor
*/
@Override
public void notifyActionAttacked(WorldObject attackerObj)
{
if (attackerObj == null)
{
return;
}
final Creature attacker = attackerObj.asCreature();
if (attacker == null)
{
return;
}
// Calculate the attack timeout.
_attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeTaskManager.getInstance().getGameTicks();
// Set the _globalAggro to 0 to permit attack even just after spawn.
if (_globalAggro < 0)
{
_globalAggro = 0;
}
// Add the attacker to the _aggroList of the actor.
_actor.asAttackable().addDamageHate(attacker, 0, 1);
// Set the Creature movement type to run and send Server->Client packet ChangeMoveType to all others Player.
if (!_actor.isRunning())
{
_actor.setRunning();
}
// Set the Intention to ATTACK.
if (getIntention() != Intention.ATTACK)
{
setIntentionAttack(attacker);
}
super.notifyActionAttacked(attacker);
}
/**
* Launch actions corresponding to the Action Aggression.<br>
* <br>
* <b><u>Actions</u>:</b>
* <ul>
* <li>Add the target to the actor _aggroList or update hate if already present</li>
* <li>Set the actor Intention to ATTACK (if actor is GuardInstance check if it isn't too far from its home location)</li>
* </ul>
* @param aggro The value of hate to add to the actor against the target
*/
@Override
public void notifyActionAggression(WorldObject targetObj, int aggro)
{
if (_actor == null)
{
return;
}
final Creature target = targetObj == null ? null : targetObj.asCreature();
final Attackable me = _actor.asAttackable();
if (target != null)
{
// Add the target to the actor _aggroList or update hate if already present.
me.addDamageHate(target, 0, aggro);
// Get the hate of the actor against the target.
if (me.getHating(target) <= 0)
{
if (me.getMostHated() == null)
{
_globalAggro = -25;
me.clearAggroList();
setIntentionIdle();
}
return;
}
// Set the actor AI Intention to ATTACK.
if (getIntention() != Intention.ATTACK)
{
// Set the Creature movement type to run and send Server->Client packet ChangeMoveType to all others Player.
if (!_actor.isRunning())
{
_actor.setRunning();
}
final Defender sGuard = (Defender) _actor;
final double homeX = target.getX() - sGuard.getSpawn().getX();
final double homeY = target.getY() - sGuard.getSpawn().getY();
// Check if the SiegeGuard is not too far from its home location.
if (((homeX * homeX) + (homeY * homeY)) < 3240000)
{
setIntentionAttack(target);
}
}
}
else
{
// Currently only for setting lower general aggro.
if (aggro >= 0)
{
return;
}
final Creature mostHated = me.getMostHated();
if (mostHated == null)
{
_globalAggro = -25;
return;
}
for (Creature aggroed : me.getAggroList().keySet())
{
me.addDamageHate(aggroed, 0, aggro);
}
if (me.getHating(mostHated) <= 0)
{
_globalAggro = -25;
me.clearAggroList();
setIntentionIdle();
}
}
}
@Override
public void stopAITask()
{
if (_aiTask != null)
{
_aiTask.cancel(false);
_aiTask = null;
}
_actor.detachAI();
super.stopAITask();
}
}
@@ -0,0 +1,55 @@
/*
* This file is part of the L2J Mobius project.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.ai;
import java.util.ArrayList;
import java.util.List;
import org.l2jmobius.gameserver.entity.actor.Creature;
import org.l2jmobius.gameserver.entity.actor.instance.Defender;
/**
* @author BiggBoss
*/
public class SpecialSiegeGuardAI extends SiegeGuardAI
{
private final List<Integer> _allied = new ArrayList<>();
/**
* @param creature
*/
public SpecialSiegeGuardAI(Defender creature)
{
super(creature);
}
public List<Integer> getAlly()
{
return _allied;
}
@Override
protected boolean autoAttackCondition(Creature target)
{
if (_allied.contains(target.getObjectId()))
{
return false;
}
return super.autoAttackCondition(target);
}
}
@@ -0,0 +1,374 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.ai;
import java.util.concurrent.Future;
import org.l2jmobius.commons.threads.ThreadPool;
import org.l2jmobius.commons.util.Rnd;
import org.l2jmobius.gameserver.config.GeoEngineConfig;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.actor.Creature;
import org.l2jmobius.gameserver.entity.actor.Summon;
import org.l2jmobius.gameserver.geoengine.GeoEngine;
import org.l2jmobius.gameserver.geoengine.pathfinding.PathFinding;
import org.l2jmobius.gameserver.mechanics.skill.Skill;
public class SummonAI extends PlayableAI implements Runnable
{
private static final int AVOID_RADIUS = 70;
private volatile boolean _thinking; // To prevent recursive thinking.
private volatile boolean _startFollow = _actor.asSummon().getFollowStatus();
private Creature _lastAttack = null;
private volatile boolean _startAvoid = false;
private Future<?> _avoidTask = null;
public SummonAI(Summon creature)
{
super(creature);
}
@Override
public Intention getNextIntention()
{
return _lastAttack != null ? Intention.ATTACK : null;
}
@Override
public void setIntentionAttack(WorldObject target)
{
if (target == null)
{
return;
}
final Creature creatureTarget = target.asCreature();
if (creatureTarget == null)
{
return;
}
if ((GeoEngineConfig.PATHFINDING > 0) && (PathFinding.getInstance().findPath(_actor.getX(), _actor.getY(), _actor.getZ(), creatureTarget.getX(), creatureTarget.getY(), creatureTarget.getZ(), _actor.getInstanceId(), false) == null))
{
return;
}
stopAvoidTask();
super.setIntentionAttack(target);
}
@Override
public void setIntentionIdle()
{
stopFollow();
_startFollow = false;
stopAvoidTask();
setIntentionActive();
}
@Override
public void setIntentionActive()
{
if (_startFollow)
{
startAvoidTask();
setIntentionFollow(_actor.asSummon().getOwner());
}
else
{
startAvoidTask();
super.setIntentionActive();
}
}
@Override
public void setIntentionFollow(WorldObject target)
{
if (target == null)
{
clientActionFailed();
return;
}
final Creature creatureTarget = target.asCreature();
if (creatureTarget == null)
{
clientActionFailed();
return;
}
if ((GeoEngineConfig.PATHFINDING > 0) && (PathFinding.getInstance().findPath(_actor.getX(), _actor.getY(), _actor.getZ(), creatureTarget.getX(), creatureTarget.getY(), creatureTarget.getZ(), _actor.getInstanceId(), false) == null))
{
clientActionFailed();
return;
}
startAvoidTask();
super.setIntentionFollow(target);
}
private void thinkAttack()
{
if (checkTargetLostOrDead(getAttackTarget()))
{
setAttackTarget(null);
return;
}
if (maybeMoveToPawn(getAttackTarget(), _actor.getPhysicalAttackRange()))
{
return;
}
clientStopMoving(null);
_actor.doAttack(getAttackTarget());
}
private void thinkCast()
{
if (checkTargetLost(getCastTarget()))
{
setCastTarget(null);
return;
}
final boolean val = _startFollow;
if (maybeMoveToPawn(getCastTarget(), _actor.getMagicalAttackRange(_skill)))
{
return;
}
clientStopMoving(null);
final Summon summon = _actor.asSummon();
summon.setFollowStatus(false);
setIntentionIdle();
_startFollow = val;
_actor.doCast(_skill);
}
private void thinkPickUp()
{
if (checkTargetLost(getTarget()) || maybeMoveToPawn(getTarget(), 36))
{
return;
}
setIntentionIdle();
_actor.asSummon().doPickupItem(getTarget());
}
private void thinkInteract()
{
if (checkTargetLost(getTarget()) || maybeMoveToPawn(getTarget(), 36))
{
return;
}
setIntentionIdle();
}
@Override
public void notifyActionThink()
{
if (_thinking || _actor.isCastingNow() || _actor.isAllSkillsDisabled())
{
return;
}
_thinking = true;
try
{
switch (getIntention())
{
case ATTACK:
{
thinkAttack();
break;
}
case CAST:
{
thinkCast();
break;
}
case PICK_UP:
{
thinkPickUp();
break;
}
case INTERACT:
{
thinkInteract();
break;
}
}
}
finally
{
_thinking = false;
}
}
@Override
public void notifyActionFinishCasting()
{
if (_lastAttack == null)
{
_actor.asSummon().setFollowStatus(_startFollow);
}
else
{
final Creature replayTarget = _lastAttack;
_lastAttack = null;
setIntentionAttack(replayTarget);
}
super.notifyActionFinishCasting();
}
@Override
public void notifyActionAttacked(WorldObject attacker)
{
super.notifyActionAttacked(attacker);
if (attacker != null)
{
final Creature attackerCreature = attacker.asCreature();
if (attackerCreature != null)
{
avoidAttack(attackerCreature);
}
}
}
@Override
public void notifyActionEvaded(WorldObject attacker)
{
super.notifyActionEvaded(attacker);
if (attacker != null)
{
final Creature attackerCreature = attacker.asCreature();
if (attackerCreature != null)
{
avoidAttack(attackerCreature);
}
}
}
private void avoidAttack(Creature attacker)
{
// Trying to avoid if summon near owner.
if ((_actor.asSummon().getOwner() != null) && (_actor.asSummon().getOwner() != attacker) && _actor.asSummon().getOwner().isInsideRadius3D(_actor, 2 * AVOID_RADIUS))
{
_startAvoid = true;
}
}
@Override
public void run()
{
if (!_startAvoid)
{
return;
}
_startAvoid = false;
if (_actor.isMoving() || _actor.isDead() || _actor.isMovementDisabled())
{
return;
}
final int ownerX = _actor.asSummon().getOwner().getX();
final int ownerY = _actor.asSummon().getOwner().getY();
final double angle = Math.toRadians(Rnd.get(-90, 90)) + Math.atan2(ownerY - _actor.getY(), ownerX - _actor.getX());
final int targetX = ownerX + (int) (AVOID_RADIUS * Math.cos(angle));
final int targetY = ownerY + (int) (AVOID_RADIUS * Math.sin(angle));
if (GeoEngine.getInstance().canMoveToTarget(_actor.getX(), _actor.getY(), _actor.getZ(), targetX, targetY, _actor.getZ(), _actor.getInstanceId()))
{
moveTo(targetX, targetY, _actor.getZ());
}
}
public void notifyFollowStatusChange()
{
_startFollow = !_startFollow;
switch (getIntention())
{
case ACTIVE:
case FOLLOW:
case IDLE:
case MOVE_TO:
case PICK_UP:
{
_actor.asSummon().setFollowStatus(_startFollow);
}
}
}
public void setStartFollowController(boolean value)
{
_startFollow = value;
}
@Override
public void setIntentionCast(Skill skill, WorldObject target)
{
if (getIntention() == Intention.ATTACK)
{
_lastAttack = getAttackTarget();
}
else
{
_lastAttack = null;
}
stopAvoidTask();
super.setIntentionCast(skill, target);
}
private void startAvoidTask()
{
if (_avoidTask == null)
{
_avoidTask = ThreadPool.scheduleAtFixedRate(this, 100, 100);
}
}
private void stopAvoidTask()
{
if (_avoidTask != null)
{
_avoidTask.cancel(false);
_avoidTask = null;
}
}
@Override
public void stopAITask()
{
stopAvoidTask();
super.stopAITask();
}
}
@@ -0,0 +1,207 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.cache;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.l2jmobius.gameserver.config.GeneralConfig;
import org.l2jmobius.gameserver.config.ServerConfig;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.network.enums.ChatType;
import org.l2jmobius.gameserver.network.serverpackets.CreatureSay;
/**
* @author Layane, Mobius
*/
public class HtmCache
{
private static final Logger LOGGER = Logger.getLogger(HtmCache.class.getName());
private static final Map<String, String> HTML_CACHE = GeneralConfig.HTM_CACHE ? new HashMap<>() : new ConcurrentHashMap<>();
private int _loadedFiles;
private long _bytesBuffLen;
protected HtmCache()
{
reload();
}
public void reload()
{
reload(ServerConfig.DATAPACK_ROOT);
}
public void reload(File file)
{
if (GeneralConfig.HTM_CACHE)
{
LOGGER.info("Html cache start...");
parseDir(file);
LOGGER.info("Cache[HTML]: " + String.format("%.3f", getMemoryUsage()) + " megabytes on " + _loadedFiles + " files loaded.");
}
else
{
HTML_CACHE.clear();
_loadedFiles = 0;
_bytesBuffLen = 0;
LOGGER.info("Cache[HTML]: Running lazy cache.");
}
}
public void reloadPath(File file)
{
parseDir(file);
LOGGER.info("Cache[HTML]: Reloaded specified path.");
}
public double getMemoryUsage()
{
return (float) _bytesBuffLen / 1048576;
}
public int getLoadedFiles()
{
return _loadedFiles;
}
private void parseDir(File dir)
{
final File[] files = dir.listFiles();
if (files != null)
{
for (File file : files)
{
if (!file.isDirectory())
{
loadFile(file);
}
else
{
parseDir(file);
}
}
}
}
public String loadFile(File file)
{
if ((file == null) || !file.isFile())
{
return null;
}
final String lowerCaseName = file.getName().toLowerCase();
if (!(lowerCaseName.endsWith(".htm") || lowerCaseName.endsWith(".html")))
{
return null;
}
String filePath = null;
String content = null;
try (FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis))
{
final int bytes = bis.available();
final byte[] raw = new byte[bytes];
bis.read(raw);
content = new String(raw, StandardCharsets.UTF_8);
content = content.replaceAll("(?s)<!--.*?-->", ""); // Remove html comments.
content = content.replaceAll("[\\t\\n]", ""); // Remove tabs and new lines.
filePath = file.toURI().getPath().substring(ServerConfig.DATAPACK_ROOT.toURI().getPath().length());
if (GeneralConfig.CHECK_HTML_ENCODING && !filePath.startsWith("data/lang") && !StandardCharsets.US_ASCII.newEncoder().canEncode(content))
{
LOGGER.warning("HTML encoding check: File " + filePath + " contains non ASCII content.");
}
final String oldContent = HTML_CACHE.put(filePath, content);
if (oldContent == null)
{
_bytesBuffLen += bytes;
_loadedFiles++;
}
else
{
_bytesBuffLen = (_bytesBuffLen - oldContent.length()) + bytes;
}
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Problem with htm file:", e);
}
return content;
}
public String getHtm(Player player, String path)
{
final String prefix = player != null ? player.getHtmlPrefix() : "";
String newPath = prefix + path;
String content = HTML_CACHE.get(newPath);
if (!GeneralConfig.HTM_CACHE && (content == null))
{
content = loadFile(new File(ServerConfig.DATAPACK_ROOT, newPath));
if (content == null)
{
content = loadFile(new File(ServerConfig.SCRIPT_ROOT, newPath));
}
}
// In case localisation does not exist try the default path.
if ((content == null) && !prefix.contentEquals(""))
{
content = HTML_CACHE.get(path);
newPath = path;
}
if ((player != null) && player.isGM() && GeneralConfig.GM_DEBUG_HTML_PATHS)
{
player.sendPacket(new CreatureSay(null, ChatType.GENERAL, "HTML", newPath.substring(5)));
}
return content;
}
public boolean contains(String path)
{
return HTML_CACHE.containsKey(path);
}
public static HtmCache getInstance()
{
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder
{
protected static final HtmCache INSTANCE = new HtmCache();
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.cache;
/**
* @author Sahar
*/
public class RelationCache
{
private final int _relation;
private final boolean _isAutoAttackable;
public RelationCache(int relation, boolean isAutoAttackable)
{
_relation = relation;
_isAutoAttackable = isAutoAttackable;
}
public int getRelation()
{
return _relation;
}
public boolean isAutoAttackable()
{
return _isAutoAttackable;
}
}
@@ -0,0 +1,260 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.communitybbs.BB;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.l2jmobius.commons.database.DatabaseFactory;
import org.l2jmobius.gameserver.communitybbs.TopicConstructorType;
import org.l2jmobius.gameserver.communitybbs.Manager.ForumsBBSManager;
import org.l2jmobius.gameserver.communitybbs.Manager.TopicBBSManager;
public class Forum
{
private static final Logger LOGGER = Logger.getLogger(Forum.class.getName());
// type
public static final int ROOT = 0;
public static final int NORMAL = 1;
public static final int CLAN = 2;
public static final int MEMO = 3;
public static final int MAIL = 4;
// perm
public static final int INVISIBLE = 0;
public static final int ALL = 1;
public static final int CLANMEMBERONLY = 2;
public static final int OWNERONLY = 3;
private final Collection<Forum> _children;
private final Map<Integer, Topic> _topic = new ConcurrentHashMap<>();
private final int _forumId;
private String _forumName;
private int _forumType;
private int _forumPost;
private int _forumPerm;
private final Forum _fParent;
private int _ownerID;
private boolean _loaded = false;
/**
* Creates new instance of Forum. When you create new forum, use {@link org.l2jmobius.gameserver.communitybbs.Manager.ForumsBBSManager#addForum(org.l2jmobius.gameserver.communitybbs.BB.Forum)} to add forum to the forums manager.
* @param forumId
* @param fParent
*/
public Forum(int forumId, Forum fParent)
{
_forumId = forumId;
_fParent = fParent;
_children = ConcurrentHashMap.newKeySet();
}
/**
* @param name
* @param parent
* @param type
* @param perm
* @param ownerId
*/
public Forum(String name, Forum parent, int type, int perm, int ownerId)
{
_forumName = name;
_forumId = ForumsBBSManager.getInstance().getANewID();
_forumType = type;
_forumPost = 0;
_forumPerm = perm;
_fParent = parent;
_ownerID = ownerId;
_children = ConcurrentHashMap.newKeySet();
parent._children.add(this);
ForumsBBSManager.getInstance().addForum(this);
_loaded = true;
}
private void load()
{
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement ps = con.prepareStatement("SELECT * FROM forums WHERE forum_id=?"))
{
ps.setInt(1, _forumId);
try (ResultSet rs = ps.executeQuery())
{
if (rs.next())
{
_forumName = rs.getString("forum_name");
_forumPost = rs.getInt("forum_post");
_forumType = rs.getInt("forum_type");
_forumPerm = rs.getInt("forum_perm");
_ownerID = rs.getInt("forum_owner_id");
}
}
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Data error on Forum " + _forumId + " : " + e.getMessage(), e);
}
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement ps = con.prepareStatement("SELECT * FROM topic WHERE topic_forum_id=? ORDER BY topic_id DESC"))
{
ps.setInt(1, _forumId);
try (ResultSet rs = ps.executeQuery())
{
while (rs.next())
{
final Topic t = new Topic(TopicConstructorType.RESTORE, rs.getInt("topic_id"), rs.getInt("topic_forum_id"), rs.getString("topic_name"), rs.getLong("topic_date"), rs.getString("topic_ownername"), rs.getInt("topic_ownerid"), rs.getInt("topic_type"), rs.getInt("topic_reply"));
_topic.put(t.getID(), t);
if (t.getID() > TopicBBSManager.getInstance().getMaxID(this))
{
TopicBBSManager.getInstance().setMaxID(t.getID(), this);
}
}
}
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Data error on Forum " + _forumId + " : " + e.getMessage(), e);
}
}
private void getChildren()
{
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement ps = con.prepareStatement("SELECT forum_id FROM forums WHERE forum_parent=?"))
{
ps.setInt(1, _forumId);
try (ResultSet rs = ps.executeQuery())
{
while (rs.next())
{
final Forum f = new Forum(rs.getInt("forum_id"), this);
_children.add(f);
ForumsBBSManager.getInstance().addForum(f);
}
}
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Data error on Forum (children): " + e.getMessage(), e);
}
}
public int getTopicSize()
{
vload();
return _topic.size();
}
public Topic getTopic(int j)
{
vload();
return _topic.get(j);
}
public void addTopic(Topic t)
{
vload();
_topic.put(t.getID(), t);
}
/**
* @return the forum Id
*/
public int getID()
{
return _forumId;
}
public String getName()
{
vload();
return _forumName;
}
public int getType()
{
vload();
return _forumType;
}
/**
* @param name the forum name
* @return the forum for the given name
*/
public Forum getChildByName(String name)
{
vload();
for (Forum forum : _children)
{
if (forum.getName().equals(name))
{
return forum;
}
}
return null;
}
/**
* @param id
*/
public void rmTopicByID(int id)
{
_topic.remove(id);
}
public void insertIntoDb()
{
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT INTO forums (forum_id,forum_name,forum_parent,forum_post,forum_type,forum_perm,forum_owner_id) VALUES (?,?,?,?,?,?,?)"))
{
ps.setInt(1, _forumId);
ps.setString(2, _forumName);
ps.setInt(3, _fParent.getID());
ps.setInt(4, _forumPost);
ps.setInt(5, _forumType);
ps.setInt(6, _forumPerm);
ps.setInt(7, _ownerID);
ps.execute();
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Error while saving new Forum to db " + e.getMessage(), e);
}
}
public void vload()
{
if (!_loaded)
{
load();
getChildren();
_loaded = true;
}
}
}
@@ -0,0 +1,137 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.communitybbs.BB;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.format.DateTimeFormatter;
import org.l2jmobius.gameserver.network.enums.MailType;
public class Mail
{
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private final int _id;
private final int _receiverId;
private final int _senderId;
private final String _recipients;
public final String _subject;
private final String _message;
private final Timestamp _sentDate;
private final String _formattedSentDate;
private MailType _mailType;
private boolean _isUnread;
public Mail(ResultSet rs) throws SQLException
{
_id = rs.getInt("id");
_receiverId = rs.getInt("receiver_id");
_senderId = rs.getInt("sender_id");
_mailType = Enum.valueOf(MailType.class, rs.getString("location").toUpperCase());
_recipients = rs.getString("recipients");
_subject = rs.getString("subject");
_message = rs.getString("message");
_sentDate = rs.getTimestamp("sent_date");
_formattedSentDate = _sentDate.toLocalDateTime().format(DATE_FORMATTER);
_isUnread = rs.getInt("is_unread") != 0;
}
public Mail(int id, int receiverId, int senderId, MailType location, String recipients, String subject, String message, Timestamp sentDate, String formattedSentDate, boolean isUnread)
{
_id = id;
_receiverId = receiverId;
_senderId = senderId;
_mailType = location;
_recipients = recipients;
_subject = subject;
_message = message;
_sentDate = sentDate;
_formattedSentDate = formattedSentDate;
_isUnread = isUnread;
}
public int getId()
{
return _id;
}
public int getReceiverId()
{
return _receiverId;
}
public int getSenderId()
{
return _senderId;
}
public MailType getMailType()
{
return _mailType;
}
public void setMailType(MailType mailType)
{
_mailType = mailType;
}
public String getRecipients()
{
return _recipients;
}
public String getSubject()
{
return _subject;
}
public String getMessage()
{
return _message;
}
public Timestamp getSentDate()
{
return _sentDate;
}
public String getFormattedSentDate()
{
return _formattedSentDate;
}
public boolean isUnread()
{
return _isUnread;
}
public void setAsRead()
{
_isUnread = false;
}
}
@@ -0,0 +1,258 @@
/*
* This file is part of the L2J Mobius project.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.communitybbs.BB;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.l2jmobius.commons.database.DatabaseFactory;
import org.l2jmobius.gameserver.communitybbs.Manager.PostBBSManager;
/**
* @author Maktakien
*/
public class Post
{
private static final Logger LOGGER = Logger.getLogger(Post.class.getName());
public static class CPost
{
private int _postId;
private String _postOwner;
private int _postOwnerId;
private long _postDate;
private int _postTopicId;
private int _postForumId;
private String _postText;
public void setPostId(int postId)
{
_postId = postId;
}
public int getPostId()
{
return _postId;
}
public void setPostOwner(String postOwner)
{
_postOwner = postOwner;
}
public String getPostOwner()
{
return _postOwner;
}
public void setPostOwnerId(int postOwnerId)
{
_postOwnerId = postOwnerId;
}
public int getPostOwnerId()
{
return _postOwnerId;
}
public void setPostDate(long postDate)
{
_postDate = postDate;
}
public long getPostDate()
{
return _postDate;
}
public void setPostTopicId(int postTopicId)
{
_postTopicId = postTopicId;
}
public int getPostTopicId()
{
return _postTopicId;
}
public void setPostForumId(int postForumId)
{
_postForumId = postForumId;
}
public int getPostForumId()
{
return _postForumId;
}
public void setPostText(String postText)
{
_postText = postText;
}
public String getPostText()
{
if (_postText == null)
{
return "";
}
// Bypass exploit check.
final String text = _postText.toLowerCase();
if (text.contains("action") && text.contains("bypass"))
{
return "";
}
// Returns text without tags.
return _postText.replaceAll("<.*?>", "");
}
}
private final Collection<CPost> _post;
/**
* @param postOwner
* @param postOwnerId
* @param date
* @param tid
* @param postForumId
* @param txt
*/
public Post(String postOwner, int postOwnerId, long date, int tid, int postForumId, String txt)
{
_post = ConcurrentHashMap.newKeySet();
final CPost cp = new CPost();
cp.setPostId(0);
cp.setPostOwner(postOwner);
cp.setPostOwnerId(postOwnerId);
cp.setPostDate(date);
cp.setPostTopicId(tid);
cp.setPostForumId(postForumId);
cp.setPostText(txt);
_post.add(cp);
insertindb(cp);
}
private void insertindb(CPost cp)
{
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT INTO posts (post_id,post_owner_name,post_ownerid,post_date,post_topic_id,post_forum_id,post_txt) values (?,?,?,?,?,?,?)"))
{
ps.setInt(1, cp.getPostId());
ps.setString(2, cp.getPostOwner());
ps.setInt(3, cp.getPostOwnerId());
ps.setLong(4, cp.getPostDate());
ps.setInt(5, cp.getPostTopicId());
ps.setInt(6, cp.getPostForumId());
ps.setString(7, cp.getPostText());
ps.execute();
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Error while saving new Post to db " + e.getMessage(), e);
}
}
public Post(Topic t)
{
_post = ConcurrentHashMap.newKeySet();
load(t);
}
public CPost getCPost(int id)
{
int i = 0;
for (CPost cp : _post)
{
if (i++ == id)
{
return cp;
}
}
return null;
}
public void deleteMe(Topic t)
{
PostBBSManager.getInstance().delPostByTopic(t);
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement ps = con.prepareStatement("DELETE FROM posts WHERE post_forum_id=? AND post_topic_id=?"))
{
ps.setInt(1, t.getForumID());
ps.setInt(2, t.getID());
ps.execute();
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Error while deleting post: " + e.getMessage(), e);
}
}
private void load(Topic t)
{
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement ps = con.prepareStatement("SELECT * FROM posts WHERE post_forum_id=? AND post_topic_id=? ORDER BY post_id ASC"))
{
ps.setInt(1, t.getForumID());
ps.setInt(2, t.getID());
try (ResultSet rs = ps.executeQuery())
{
while (rs.next())
{
final CPost cp = new CPost();
cp.setPostId(rs.getInt("post_id"));
cp.setPostOwner(rs.getString("post_owner_name"));
cp.setPostOwnerId(rs.getInt("post_ownerid"));
cp.setPostDate(rs.getLong("post_date"));
cp.setPostTopicId(rs.getInt("post_topic_id"));
cp.setPostForumId(rs.getInt("post_forum_id"));
cp.setPostText(rs.getString("post_txt"));
_post.add(cp);
}
}
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Data error on Post " + t.getForumID() + "/" + t.getID() + " : " + e.getMessage(), e);
}
}
public void updateText(int i)
{
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement ps = con.prepareStatement("UPDATE posts SET post_txt=? WHERE post_id=? AND post_topic_id=? AND post_forum_id=?"))
{
final CPost cp = getCPost(i);
ps.setString(1, cp.getPostText());
ps.setInt(2, cp.getPostId());
ps.setInt(3, cp.getPostTopicId());
ps.setInt(4, cp.getPostForumId());
ps.execute();
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Error while saving new Post to db " + e.getMessage(), e);
}
}
}
@@ -0,0 +1,151 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.communitybbs.BB;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.l2jmobius.commons.database.DatabaseFactory;
import org.l2jmobius.gameserver.communitybbs.TopicConstructorType;
import org.l2jmobius.gameserver.communitybbs.Manager.TopicBBSManager;
public class Topic
{
private static final Logger LOGGER = Logger.getLogger(Topic.class.getName());
public static final int NORMAL = 0;
public static final int MEMO = 1;
private final int _id;
private final int _forumId;
private final String _topicName;
private final long _date;
private final String _ownerName;
private final int _ownerId;
private final int _type;
private final int _cReply;
/**
* @param ct
* @param id
* @param fid
* @param name
* @param date
* @param oname
* @param oid
* @param type
* @param cReply
*/
public Topic(TopicConstructorType ct, int id, int fid, String name, long date, String oname, int oid, int type, int cReply)
{
_id = id;
_forumId = fid;
_topicName = name;
_date = date;
_ownerName = oname;
_ownerId = oid;
_type = type;
_cReply = cReply;
TopicBBSManager.getInstance().addTopic(this);
if (ct == TopicConstructorType.CREATE)
{
insertindb();
}
}
private void insertindb()
{
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT INTO topic (topic_id,topic_forum_id,topic_name,topic_date,topic_ownername,topic_ownerid,topic_type,topic_reply) values (?,?,?,?,?,?,?,?)"))
{
ps.setInt(1, _id);
ps.setInt(2, _forumId);
ps.setString(3, _topicName);
ps.setLong(4, _date);
ps.setString(5, _ownerName);
ps.setInt(6, _ownerId);
ps.setInt(7, _type);
ps.setInt(8, _cReply);
ps.execute();
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Error while saving new Topic to db " + e.getMessage(), e);
}
}
/**
* @return the topic Id
*/
public int getID()
{
return _id;
}
public int getForumID()
{
return _forumId;
}
/**
* @return the topic name
*/
public String getName()
{
return _topicName;
}
public String getOwnerName()
{
return _ownerName;
}
/**
* @param f
*/
public void deleteme(Forum f)
{
TopicBBSManager.getInstance().delTopic(this);
f.rmTopicByID(_id);
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement ps = con.prepareStatement("DELETE FROM topic WHERE topic_id=? AND topic_forum_id=?"))
{
ps.setInt(1, _id);
ps.setInt(2, f.getID());
ps.execute();
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Error while deleting topic: " + e.getMessage(), e);
}
}
/**
* @return the topic date
*/
public long getDate()
{
return _date;
}
}
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.communitybbs.Manager;
import java.util.ArrayList;
import java.util.List;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.network.serverpackets.ShowBoard;
public abstract class BaseBBSManager
{
public abstract void parsecmd(String command, Player player);
public abstract void parsewrite(String ar1, String ar2, String ar3, String ar4, String ar5, Player player);
/**
* @param html
* @param acha
*/
protected void send1001(String html, Player acha)
{
if (html.length() < 8192)
{
acha.sendPacket(new ShowBoard(html, "1001"));
}
}
/**
* @param acha
*/
protected void send1002(Player acha)
{
send1002(acha, " ", " ", "0");
}
/**
* @param player
* @param string
* @param string2
* @param string3
*/
protected void send1002(Player player, String string, String string2, String string3)
{
final List<String> arg = new ArrayList<>(20);
arg.add("0");
arg.add("0");
arg.add("0");
arg.add("0");
arg.add("0");
arg.add("0");
arg.add(player.getName());
arg.add(Integer.toString(player.getObjectId()));
arg.add(player.getAccountName());
arg.add("9");
arg.add(string2); // subject?
arg.add(string2); // subject?
arg.add(string); // text
arg.add(string3); // date?
arg.add(string3); // date?
arg.add("0");
arg.add("0");
player.sendPacket(new ShowBoard(arg));
}
}
@@ -0,0 +1,174 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.communitybbs.Manager;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.l2jmobius.commons.database.DatabaseFactory;
import org.l2jmobius.gameserver.communitybbs.BB.Forum;
import org.l2jmobius.gameserver.entity.actor.Player;
public class ForumsBBSManager extends BaseBBSManager
{
private static final Logger LOGGER = Logger.getLogger(ForumsBBSManager.class.getName());
private final Collection<Forum> _table;
private int _lastid = 1;
/**
* Instantiates a new forums bbs manager.
*/
protected ForumsBBSManager()
{
_table = ConcurrentHashMap.newKeySet();
try (Connection con = DatabaseFactory.getConnection();
Statement s = con.createStatement();
ResultSet rs = s.executeQuery("SELECT forum_id FROM forums WHERE forum_type = 0"))
{
while (rs.next())
{
addForum(new Forum(rs.getInt("forum_id"), null));
}
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Data error on Forum (root): " + e.getMessage(), e);
}
}
/**
* Inits the root.
*/
public void initRoot()
{
_table.forEach(Forum::vload);
LOGGER.info(getClass().getSimpleName() + ": Loaded " + _table.size() + " forums. Last forum id used: " + _lastid);
}
/**
* Adds the forum.
* @param ff the forum
*/
public void addForum(Forum ff)
{
if (ff == null)
{
return;
}
_table.add(ff);
if (ff.getID() > _lastid)
{
_lastid = ff.getID();
}
}
@Override
public void parsecmd(String command, Player player)
{
}
/**
* Gets the forum by name.
* @param name the forum name
* @return the forum by name
*/
public Forum getForumByName(String name)
{
for (Forum forum : _table)
{
if (forum.getName().equals(name))
{
return forum;
}
}
return null;
}
/**
* Creates the new forum.
* @param name the forum name
* @param parent the parent forum
* @param type the forum type
* @param perm the perm
* @param oid the oid
* @return the new forum
*/
public Forum createNewForum(String name, Forum parent, int type, int perm, int oid)
{
final Forum forum = new Forum(name, parent, type, perm, oid);
forum.insertIntoDb();
return forum;
}
/**
* Gets the a new Id.
* @return the a new Id
*/
public int getANewID()
{
return ++_lastid;
}
/**
* Gets the forum by Id.
* @param idf the the forum Id
* @return the forum by Id
*/
public Forum getForumByID(int idf)
{
for (Forum f : _table)
{
if (f.getID() == idf)
{
return f;
}
}
return null;
}
@Override
public void parsewrite(String ar1, String ar2, String ar3, String ar4, String ar5, Player player)
{
}
/**
* Gets the single instance of ForumsBBSManager.
* @return single instance of ForumsBBSManager
*/
public static ForumsBBSManager getInstance()
{
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder
{
protected static final ForumsBBSManager INSTANCE = new ForumsBBSManager();
}
}
@@ -0,0 +1,191 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.communitybbs.Manager;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import org.l2jmobius.gameserver.communitybbs.BB.Forum;
import org.l2jmobius.gameserver.communitybbs.BB.Post;
import org.l2jmobius.gameserver.communitybbs.BB.Topic;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.handler.CommunityBoardHandler;
public class PostBBSManager extends BaseBBSManager
{
private final Map<Topic, Post> _postByTopic = new ConcurrentHashMap<>();
public Post getGPosttByTopic(Topic t)
{
Post post = _postByTopic.get(t);
if (post == null)
{
post = new Post(t);
_postByTopic.put(t, post);
}
return post;
}
public void delPostByTopic(Topic t)
{
_postByTopic.remove(t);
}
public void addPostByTopic(Post p, Topic t)
{
if (_postByTopic.get(t) == null)
{
_postByTopic.put(t, p);
}
}
@Override
public void parsecmd(String command, Player player)
{
if (command.startsWith("_bbsposts;read;"))
{
final StringTokenizer st = new StringTokenizer(command, ";");
st.nextToken();
st.nextToken();
final int idf = Integer.parseInt(st.nextToken());
final int idp = Integer.parseInt(st.nextToken());
final String index = st.hasMoreTokens() ? st.nextToken() : null;
final int ind = index == null ? 1 : Integer.parseInt(index);
showPost(TopicBBSManager.getInstance().getTopicByID(idp), ForumsBBSManager.getInstance().getForumByID(idf), player, ind);
}
else if (command.startsWith("_bbsposts;edit;"))
{
final StringTokenizer st = new StringTokenizer(command, ";");
st.nextToken();
st.nextToken();
final int idf = Integer.parseInt(st.nextToken());
final int idt = Integer.parseInt(st.nextToken());
final int idp = Integer.parseInt(st.nextToken());
showEditPost(TopicBBSManager.getInstance().getTopicByID(idt), ForumsBBSManager.getInstance().getForumByID(idf), player, idp);
}
else
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the command: " + command + " is not implemented yet</center><br><br></body></html>", player);
}
}
private void showEditPost(Topic topic, Forum forum, Player player, int idp)
{
final Post p = getGPosttByTopic(topic);
if ((forum == null) || (topic == null) || (p == null))
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>Error, this forum, topic or post does not exist!</center><br><br></body></html>", player);
}
else
{
showHtmlEditPost(topic, player, forum, p);
}
}
private void showPost(Topic topic, Forum forum, Player player, int ind)
{
if ((forum == null) || (topic == null))
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>Error: This forum is not implemented yet!</center></body></html>", player);
}
else if (forum.getType() == Forum.MEMO)
{
showMemoPost(topic, player, forum);
}
else
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>The forum: " + forum.getName() + " is not implemented yet!</center></body></html>", player);
}
}
private void showHtmlEditPost(Topic topic, Player player, Forum forum, Post p)
{
final String html = "<html><body><br><br><table border=0 width=610><tr><td width=10></td><td width=600 align=left><a action=\"bypass _bbshome\">HOME</a>&nbsp;>&nbsp;<a action=\"bypass _bbsmemo\">" + forum.getName() + " Form</a></td></tr></table><img src=\"L2UI.squareblank\" width=\"1\" height=\"10\"><center><table border=0 cellspacing=0 cellpadding=0><tr><td width=610><img src=\"sek.cbui355\" width=\"610\" height=\"1\"><br1><img src=\"sek.cbui355\" width=\"610\" height=\"1\"></td></tr></table><table fixwidth=610 border=0 cellspacing=0 cellpadding=0><tr><td><img src=\"l2ui.mini_logo\" width=5 height=20></td></tr><tr><td><img src=\"l2ui.mini_logo\" width=5 height=1></td><td align=center FIXWIDTH=60 height=29>&$413;</td><td FIXWIDTH=540>" + topic.getName() + "</td><td><img src=\"l2ui.mini_logo\" width=5 height=1></td></tr></table><table fixwidth=610 border=0 cellspacing=0 cellpadding=0><tr><td><img src=\"l2ui.mini_logo\" width=5 height=10></td></tr><tr><td><img src=\"l2ui.mini_logo\" width=5 height=1></td><td align=center FIXWIDTH=60 height=29 valign=top>&$427;</td><td align=center FIXWIDTH=540><MultiEdit var =\"Content\" width=535 height=313></td><td><img src=\"l2ui.mini_logo\" width=5 height=1></td></tr><tr><td><img src=\"l2ui.mini_logo\" width=5 height=10></td></tr></table><table fixwidth=610 border=0 cellspacing=0 cellpadding=0><tr><td><img src=\"l2ui.mini_logo\" width=5 height=10></td></tr><tr><td><img src=\"l2ui.mini_logo\" width=5 height=1></td><td align=center FIXWIDTH=60 height=29>&nbsp;</td><td align=center FIXWIDTH=70><button value=\"&$140;\" action=\"Write Post " + forum.getID() + ";" + topic.getID() + ";0 _ Content Content Content\" back=\"l2ui_ch3.smallbutton2_down\" width=65 height=20 fore=\"l2ui_ch3.smallbutton2\" ></td><td align=center FIXWIDTH=70><button value = \"&$141;\" action=\"bypass _bbsmemo\" back=\"l2ui_ch3.smallbutton2_down\" width=65 height=20 fore=\"l2ui_ch3.smallbutton2\"> </td><td align=center FIXWIDTH=400>&nbsp;</td><td><img src=\"l2ui.mini_logo\" width=5 height=1></td></tr></table></center></body></html>";
send1001(html, player);
send1002(player, p.getCPost(0).getPostText(), topic.getName(), DateFormat.getInstance().format(new Date(topic.getDate())));
}
private void showMemoPost(Topic topic, Player player, Forum forum)
{
final Post p = getGPosttByTopic(topic);
final Locale locale = Locale.getDefault();
final DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.FULL, locale);
String mes = p.getCPost(0).getPostText().replace(">", "&gt;");
mes = mes.replace("<", "&lt;");
final String html = "<html><body><br><br><table border=0 width=610><tr><td width=10></td><td width=600 align=left><a action=\"bypass _bbshome\">HOME</a>&nbsp;>&nbsp;<a action=\"bypass _bbsmemo\">Memo Form</a></td></tr></table><img src=\"L2UI.squareblank\" width=\"1\" height=\"10\"><center><table border=0 cellspacing=0 cellpadding=0 bgcolor=333333><tr><td height=10></td></tr><tr><td fixWIDTH=55 align=right valign=top>&$413; : &nbsp;</td><td fixWIDTH=380 valign=top>" + topic.getName() + "</td><td fixwidth=5></td><td fixwidth=50></td><td fixWIDTH=120></td></tr><tr><td height=10></td></tr><tr><td align=right><font color=\"AAAAAA\" >&$417; : &nbsp;</font></td><td><font color=\"AAAAAA\">" + topic.getOwnerName() + "</font></td><td></td><td><font color=\"AAAAAA\">&$418; :</font></td><td><font color=\"AAAAAA\">" + dateFormat.format(p.getCPost(0).getPostDate()) + "</font></td></tr><tr><td height=10></td></tr></table><br><table border=0 cellspacing=0 cellpadding=0><tr><td fixwidth=5></td><td FIXWIDTH=600 align=left>" + mes + "</td><td fixqqwidth=5></td></tr></table><br><img src=\"L2UI.squareblank\" width=\"1\" height=\"5\"><img src=\"L2UI.squaregray\" width=\"610\" height=\"1\"><img src=\"L2UI.squareblank\" width=\"1\" height=\"5\"><table border=0 cellspacing=0 cellpadding=0 FIXWIDTH=610><tr><td width=50><button value=\"&$422;\" action=\"bypass _bbsmemo\" back=\"l2ui_ch3.smallbutton2_down\" width=65 height=20 fore=\"l2ui_ch3.smallbutton2\"></td><td width=560 align=right><table border=0 cellspacing=0><tr><td FIXWIDTH=300></td><td><button value = \"&$424;\" action=\"bypass _bbsposts;edit;" + forum.getID() + ";" + topic.getID() + ";0\" back=\"l2ui_ch3.smallbutton2_down\" width=65 height=20 fore=\"l2ui_ch3.smallbutton2\" ></td>&nbsp;<td><button value = \"&$425;\" action=\"bypass _bbstopics;del;" + forum.getID() + ";" + topic.getID() + "\" back=\"l2ui_ch3.smallbutton2_down\" width=65 height=20 fore=\"l2ui_ch3.smallbutton2\" ></td>&nbsp;<td><button value = \"&$421;\" action=\"bypass _bbstopics;crea;" + forum.getID() + "\" back=\"l2ui_ch3.smallbutton2_down\" width=65 height=20 fore=\"l2ui_ch3.smallbutton2\" ></td>&nbsp;</tr></table></td></tr></table><br><br><br></center></body></html>";
CommunityBoardHandler.separateAndSend(html, player);
}
@Override
public void parsewrite(String ar1, String ar2, String ar3, String ar4, String ar5, Player player)
{
final StringTokenizer st = new StringTokenizer(ar1, ";");
final int idf = Integer.parseInt(st.nextToken());
final int idt = Integer.parseInt(st.nextToken());
final int idp = Integer.parseInt(st.nextToken());
final Forum f = ForumsBBSManager.getInstance().getForumByID(idf);
if (f == null)
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the forum: " + idf + " does not exist !</center><br><br></body></html>", player);
}
else
{
final Topic t = f.getTopic(idt);
if (t == null)
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the topic: " + idt + " does not exist !</center><br><br></body></html>", player);
}
else
{
final Post p = getGPosttByTopic(t);
if (p != null)
{
if (p.getCPost(idp) == null)
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the post: " + idp + " does not exist !</center><br><br></body></html>", player);
}
else
{
p.getCPost(idp).setPostText(ar4);
p.updateText(idp);
parsecmd("_bbsposts;read;" + f.getID() + ";" + t.getID(), player);
}
}
}
}
}
public static PostBBSManager getInstance()
{
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder
{
protected static final PostBBSManager INSTANCE = new PostBBSManager();
}
}
@@ -0,0 +1,311 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.communitybbs.Manager;
import java.text.DateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import org.l2jmobius.gameserver.communitybbs.TopicConstructorType;
import org.l2jmobius.gameserver.communitybbs.BB.Forum;
import org.l2jmobius.gameserver.communitybbs.BB.Post;
import org.l2jmobius.gameserver.communitybbs.BB.Topic;
import org.l2jmobius.gameserver.data.sql.ClanTable;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.handler.CommunityBoardHandler;
public class TopicBBSManager extends BaseBBSManager
{
private final Collection<Topic> _table = ConcurrentHashMap.newKeySet();
private final Map<Forum, Integer> _maxId = new ConcurrentHashMap<>();
protected TopicBBSManager()
{
// Prevent external initialization.
}
public void addTopic(Topic tt)
{
_table.add(tt);
}
public void delTopic(Topic topic)
{
_table.remove(topic);
}
public void setMaxID(int id, Forum f)
{
_maxId.put(f, id);
}
public int getMaxID(Forum f)
{
final Integer i = _maxId.get(f);
return i == null ? 0 : i;
}
public Topic getTopicByID(int idf)
{
for (Topic t : _table)
{
if (t.getID() == idf)
{
return t;
}
}
return null;
}
@Override
public void parsewrite(String ar1, String ar2, String ar3, String ar4, String ar5, Player player)
{
if (ar1.equals("crea"))
{
final Forum f = ForumsBBSManager.getInstance().getForumByID(Integer.parseInt(ar2));
if (f == null)
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the forum: " + ar2 + " is not implemented yet</center><br><br></body></html>", player);
}
else
{
final long currentTime = System.currentTimeMillis();
f.vload();
final Topic t = new Topic(TopicConstructorType.CREATE, getInstance().getMaxID(f) + 1, Integer.parseInt(ar2), ar5, currentTime, player.getName(), player.getObjectId(), Topic.MEMO, 0);
f.addTopic(t);
getInstance().setMaxID(t.getID(), f);
final Post p = new Post(player.getName(), player.getObjectId(), currentTime, t.getID(), f.getID(), ar4);
PostBBSManager.getInstance().addPostByTopic(p, t);
parsecmd("_bbsmemo", player);
}
}
else if (ar1.equals("del"))
{
final Forum f = ForumsBBSManager.getInstance().getForumByID(Integer.parseInt(ar2));
if (f == null)
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the forum: " + ar2 + " does not exist !</center><br><br></body></html>", player);
}
else
{
final Topic t = f.getTopic(Integer.parseInt(ar3));
if (t == null)
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the topic: " + ar3 + " does not exist !</center><br><br></body></html>", player);
}
else
{
// CPost cp = null;
final Post p = PostBBSManager.getInstance().getGPosttByTopic(t);
if (p != null)
{
p.deleteMe(t);
}
t.deleteme(f);
parsecmd("_bbsmemo", player);
}
}
}
else
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the command: " + ar1 + " is not implemented yet</center><br><br></body></html>", player);
}
}
@Override
public void parsecmd(String command, Player player)
{
if (command.equals("_bbsmemo"))
{
showTopics(player.getMemo(), player, 1, player.getMemo().getID());
}
else if (command.startsWith("_bbstopics;read"))
{
final StringTokenizer st = new StringTokenizer(command, ";");
st.nextToken();
st.nextToken();
final int idf = Integer.parseInt(st.nextToken());
final String index = st.hasMoreTokens() ? st.nextToken() : null;
final int ind = index == null ? 1 : Integer.parseInt(index);
showTopics(ForumsBBSManager.getInstance().getForumByID(idf), player, ind, idf);
}
else if (command.startsWith("_bbstopics;crea"))
{
final StringTokenizer st = new StringTokenizer(command, ";");
st.nextToken();
st.nextToken();
final int idf = Integer.parseInt(st.nextToken());
showNewTopic(ForumsBBSManager.getInstance().getForumByID(idf), player, idf);
}
else if (command.startsWith("_bbstopics;del"))
{
final StringTokenizer st = new StringTokenizer(command, ";");
st.nextToken();
st.nextToken();
final int idf = Integer.parseInt(st.nextToken());
final int idt = Integer.parseInt(st.nextToken());
final Forum f = ForumsBBSManager.getInstance().getForumByID(idf);
if (f == null)
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the forum: " + idf + " does not exist !</center><br><br></body></html>", player);
}
else
{
final Topic t = f.getTopic(idt);
if (t == null)
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the topic: " + idt + " does not exist !</center><br><br></body></html>", player);
}
else
{
// CPost cp = null;
final Post p = PostBBSManager.getInstance().getGPosttByTopic(t);
if (p != null)
{
p.deleteMe(t);
}
t.deleteme(f);
parsecmd("_bbsmemo", player);
}
}
}
else
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the command: " + command + " is not implemented yet</center><br><br></body></html>", player);
}
}
private void showNewTopic(Forum forum, Player player, int idf)
{
if (forum == null)
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the forum: " + idf + " is not implemented yet</center><br><br></body></html>", player);
}
else if (forum.getType() == Forum.MEMO)
{
showMemoNewTopics(forum, player);
}
else
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the forum: " + forum.getName() + " is not implemented yet</center><br><br></body></html>", player);
}
}
private void showMemoNewTopics(Forum forum, Player player)
{
final String html = "<html><body><br><br><table border=0 width=610><tr><td width=10></td><td width=600 align=left><a action=\"bypass _bbshome\">HOME</a>&nbsp;>&nbsp;<a action=\"bypass _bbsmemo\">Memo Form</a></td></tr></table><img src=\"L2UI.squareblank\" width=\"1\" height=\"10\"><center><table border=0 cellspacing=0 cellpadding=0><tr><td width=610><img src=\"sek.cbui355\" width=\"610\" height=\"1\"><br1><img src=\"sek.cbui355\" width=\"610\" height=\"1\"></td></tr></table><table fixwidth=610 border=0 cellspacing=0 cellpadding=0><tr><td><img src=\"l2ui.mini_logo\" width=5 height=20></td></tr><tr><td><img src=\"l2ui.mini_logo\" width=5 height=1></td><td align=center FIXWIDTH=60 height=29>&$413;</td><td FIXWIDTH=540><edit var = \"Title\" width=540 height=13></td><td><img src=\"l2ui.mini_logo\" width=5 height=1></td></tr></table><table fixwidth=610 border=0 cellspacing=0 cellpadding=0><tr><td><img src=\"l2ui.mini_logo\" width=5 height=10></td></tr><tr><td><img src=\"l2ui.mini_logo\" width=5 height=1></td><td align=center FIXWIDTH=60 height=29 valign=top>&$427;</td><td align=center FIXWIDTH=540><MultiEdit var =\"Content\" width=535 height=313></td><td><img src=\"l2ui.mini_logo\" width=5 height=1></td></tr><tr><td><img src=\"l2ui.mini_logo\" width=5 height=10></td></tr></table><table fixwidth=610 border=0 cellspacing=0 cellpadding=0><tr><td><img src=\"l2ui.mini_logo\" width=5 height=10></td></tr><tr><td><img src=\"l2ui.mini_logo\" width=5 height=1></td><td align=center FIXWIDTH=60 height=29>&nbsp;</td><td align=center FIXWIDTH=70><button value=\"&$140;\" action=\"Write Topic crea " + forum.getID() + " Title Content Title\" back=\"l2ui_ch3.smallbutton2_down\" width=65 height=20 fore=\"l2ui_ch3.smallbutton2\" ></td><td align=center FIXWIDTH=70><button value = \"&$141;\" action=\"bypass _bbsmemo\" back=\"l2ui_ch3.smallbutton2_down\" width=65 height=20 fore=\"l2ui_ch3.smallbutton2\"> </td><td align=center FIXWIDTH=400>&nbsp;</td><td><img src=\"l2ui.mini_logo\" width=5 height=1></td></tr></table></center></body></html>";
send1001(html, player);
send1002(player);
}
private void showTopics(Forum forum, Player player, int index, int idf)
{
if (forum == null)
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the forum: " + idf + " is not implemented yet</center><br><br></body></html>", player);
}
else if (forum.getType() == Forum.MEMO)
{
showMemoTopics(forum, player, index);
}
else
{
CommunityBoardHandler.separateAndSend("<html><body><br><br><center>the forum: " + forum.getName() + " is not implemented yet</center><br><br></body></html>", player);
}
}
private void showMemoTopics(Forum forum, Player player, int index)
{
forum.vload();
final StringBuilder html = new StringBuilder(2000);
html.append("<html><body><br><br><table border=0 width=610><tr><td width=10></td><td width=600 align=left><a action=\"bypass _bbshome\">HOME</a>&nbsp;>&nbsp;<a action=\"bypass _bbsmemo\">Memo Form</a></td></tr></table><img src=\"L2UI.squareblank\" width=\"1\" height=\"10\"><center><table border=0 cellspacing=0 cellpadding=2 bgcolor=888888 width=610><tr><td FIXWIDTH=5></td><td FIXWIDTH=415 align=center>&$413;</td><td FIXWIDTH=120 align=center></td><td FIXWIDTH=70 align=center>&$418;</td></tr></table>");
final DateFormat dateFormat = DateFormat.getInstance();
for (int i = 0, j = getMaxID(forum) + 1; i < (12 * index); j--)
{
if (j < 0)
{
break;
}
final Topic t = forum.getTopic(j);
if ((t != null) && (i++ >= (12 * (index - 1))))
{
html.append("<table border=0 cellspacing=0 cellpadding=5 WIDTH=610><tr><td FIXWIDTH=5></td><td FIXWIDTH=415><a action=\"bypass _bbsposts;read;" + forum.getID() + ";" + t.getID() + "\">" + t.getName() + "</a></td><td FIXWIDTH=120 align=center></td><td FIXWIDTH=70 align=center>" + dateFormat.format(new Date(t.getDate())) + "</td></tr></table><img src=\"L2UI.Squaregray\" width=\"610\" height=\"1\">");
}
}
html.append("<br><table width=610 cellspace=0 cellpadding=0><tr><td width=50><button value=\"&$422;\" action=\"bypass _bbsmemo\" back=\"l2ui_ch3.smallbutton2_down\" width=65 height=20 fore=\"l2ui_ch3.smallbutton2\"></td><td width=510 align=center><table border=0><tr>");
if (index == 1)
{
html.append("<td><button action=\"\" back=\"l2ui_ch3.prev1_down\" fore=\"l2ui_ch3.prev1\" width=16 height=16 ></td>");
}
else
{
html.append("<td><button action=\"bypass _bbstopics;read;" + forum.getID() + ";" + (index - 1) + "\" back=\"l2ui_ch3.prev1_down\" fore=\"l2ui_ch3.prev1\" width=16 height=16 ></td>");
}
int nbp = forum.getTopicSize() / 8;
if ((nbp * 8) != ClanTable.getInstance().getClanCount())
{
nbp++;
}
for (int i = 1; i <= nbp; i++)
{
if (i == index)
{
html.append("<td> " + i + " </td>");
}
else
{
html.append("<td><a action=\"bypass _bbstopics;read;" + forum.getID() + ";" + i + "\"> " + i + " </a></td>");
}
}
if (index == nbp)
{
html.append("<td><button action=\"\" back=\"l2ui_ch3.next1_down\" fore=\"l2ui_ch3.next1\" width=16 height=16 ></td>");
}
else
{
html.append("<td><button action=\"bypass _bbstopics;read;" + forum.getID() + ";" + (index + 1) + "\" back=\"l2ui_ch3.next1_down\" fore=\"l2ui_ch3.next1\" width=16 height=16 ></td>");
}
html.append("</tr></table> </td> <td align=right><button value = \"&$421;\" action=\"bypass _bbstopics;crea;" + forum.getID() + "\" back=\"l2ui_ch3.smallbutton2_down\" width=65 height=20 fore=\"l2ui_ch3.smallbutton2\" ></td></tr><tr><td><img src=\"l2ui.mini_logo\" width=5 height=10></td></tr><tr> <td></td><td align=center><table border=0><tr><td></td><td><edit var = \"Search\" width=130 height=11></td><td><button value=\"&$420;\" action=\"Write 5 -2 0 Search _ _\" back=\"l2ui_ch3.smallbutton2_down\" width=65 height=20 fore=\"l2ui_ch3.smallbutton2\"> </td> </tr></table> </td></tr></table><br><br><br></center></body></html>");
CommunityBoardHandler.separateAndSend(html.toString(), player);
}
public static TopicBBSManager getInstance()
{
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder
{
protected static final TopicBBSManager INSTANCE = new TopicBBSManager();
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.communitybbs;
public enum TopicConstructorType
{
RESTORE,
CREATE
}
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import org.l2jmobius.gameserver.config.custom.AllowedPlayerRacesConfig;
import org.l2jmobius.gameserver.config.custom.AutoPlayConfig;
import org.l2jmobius.gameserver.config.custom.AutoPotionsConfig;
import org.l2jmobius.gameserver.config.custom.BankingConfig;
import org.l2jmobius.gameserver.config.custom.BossAnnouncementsConfig;
import org.l2jmobius.gameserver.config.custom.CancelReturnConfig;
import org.l2jmobius.gameserver.config.custom.CaptchaConfig;
import org.l2jmobius.gameserver.config.custom.ChampionMonstersConfig;
import org.l2jmobius.gameserver.config.custom.ChatModerationConfig;
import org.l2jmobius.gameserver.config.custom.ClassBalanceConfig;
import org.l2jmobius.gameserver.config.custom.CommunityBoardConfig;
import org.l2jmobius.gameserver.config.custom.CustomMailManagerConfig;
import org.l2jmobius.gameserver.config.custom.DelevelManagerConfig;
import org.l2jmobius.gameserver.config.custom.DualboxCheckConfig;
import org.l2jmobius.gameserver.config.custom.FactionSystemConfig;
import org.l2jmobius.gameserver.config.custom.FakePlayersConfig;
import org.l2jmobius.gameserver.config.custom.FindPvpConfig;
import org.l2jmobius.gameserver.config.custom.FreeMountsConfig;
import org.l2jmobius.gameserver.config.custom.MerchantZeroSellPriceConfig;
import org.l2jmobius.gameserver.config.custom.MultilingualSupportConfig;
import org.l2jmobius.gameserver.config.custom.NoblessMasterConfig;
import org.l2jmobius.gameserver.config.custom.NpcStatMultipliersConfig;
import org.l2jmobius.gameserver.config.custom.OfflinePlayConfig;
import org.l2jmobius.gameserver.config.custom.OfflineTradeConfig;
import org.l2jmobius.gameserver.config.custom.OnlineInfoConfig;
import org.l2jmobius.gameserver.config.custom.PasswordChangeConfig;
import org.l2jmobius.gameserver.config.custom.PremiumSystemConfig;
import org.l2jmobius.gameserver.config.custom.PrivateStoreRangeConfig;
import org.l2jmobius.gameserver.config.custom.PvpAnnounceConfig;
import org.l2jmobius.gameserver.config.custom.PvpRewardItemConfig;
import org.l2jmobius.gameserver.config.custom.PvpTitleColorConfig;
import org.l2jmobius.gameserver.config.custom.RandomSpawnsConfig;
import org.l2jmobius.gameserver.config.custom.RebirthConfig;
import org.l2jmobius.gameserver.config.custom.SchemeBufferConfig;
import org.l2jmobius.gameserver.config.custom.ScreenWelcomeMessageConfig;
import org.l2jmobius.gameserver.config.custom.SellBuffsConfig;
import org.l2jmobius.gameserver.config.custom.ServerTimeConfig;
import org.l2jmobius.gameserver.config.custom.StartingLocationConfig;
import org.l2jmobius.gameserver.config.custom.StartingTitleConfig;
import org.l2jmobius.gameserver.config.custom.TransmogConfig;
import org.l2jmobius.gameserver.config.custom.WalkerBotProtectionConfig;
import org.l2jmobius.gameserver.config.custom.WarehouseSortingConfig;
import org.l2jmobius.gameserver.config.custom.WeddingConfig;
/**
* Central configuration loader for initializing all server configuration components.<br>
* This class serves as the entry point for loading all server configuration settings from various configuration files.<br>
* The configurations are typically located in the config directory within the server root folder.
* @author Mobius
*/
public class ConfigLoader
{
public static void init()
{
ServerConfig.load();
// Main configurations.
ConquerableHallSiegeConfig.load();
DevelopmentConfig.load();
FeatureConfig.load();
FloodProtectorConfig.load();
GeneralConfig.load();
GeoEngineConfig.load();
GrandBossConfig.load();
IdManagerConfig.load();
NpcConfig.load();
OlympiadConfig.load();
PlayerConfig.load();
PvpConfig.load();
RatesConfig.load();
// Custom configurations.
AllowedPlayerRacesConfig.load();
AutoPlayConfig.load();
AutoPotionsConfig.load();
BankingConfig.load();
BossAnnouncementsConfig.load();
CancelReturnConfig.load();
CaptchaConfig.load();
ChampionMonstersConfig.load();
ChatModerationConfig.load();
ClassBalanceConfig.load();
CommunityBoardConfig.load();
CustomMailManagerConfig.load();
DelevelManagerConfig.load();
DualboxCheckConfig.load();
FactionSystemConfig.load();
FakePlayersConfig.load();
FindPvpConfig.load();
FreeMountsConfig.load();
MerchantZeroSellPriceConfig.load();
MultilingualSupportConfig.load();
NoblessMasterConfig.load();
NpcStatMultipliersConfig.load();
OfflinePlayConfig.load();
OfflineTradeConfig.load();
OnlineInfoConfig.load();
PasswordChangeConfig.load();
PremiumSystemConfig.load();
PrivateStoreRangeConfig.load();
PvpAnnounceConfig.load();
PvpRewardItemConfig.load();
PvpTitleColorConfig.load();
RandomSpawnsConfig.load();
SchemeBufferConfig.load();
ScreenWelcomeMessageConfig.load();
SellBuffsConfig.load();
ServerTimeConfig.load();
StartingLocationConfig.load();
StartingTitleConfig.load();
TransmogConfig.load();
WalkerBotProtectionConfig.load();
WarehouseSortingConfig.load();
WeddingConfig.load();
RebirthConfig.load();
}
}
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the conquerable hall siege related configurations.
* @author Mobius
*/
public class ConquerableHallSiegeConfig
{
// File
private static final String CONQUERABLE_HALL_SIEGE_CONFIG_FILE = "./config/ConquerableHallSiege.ini";
// Constants
public static int CHS_MAX_ATTACKERS;
public static int CHS_CLAN_MINLEVEL;
public static int CHS_MAX_FLAGS_PER_CLAN;
public static boolean CHS_ENABLE_FAME;
public static int CHS_FAME_AMOUNT;
public static int CHS_FAME_FREQUENCY;
public static void load()
{
final ConfigReader config = new ConfigReader(CONQUERABLE_HALL_SIEGE_CONFIG_FILE);
CHS_MAX_ATTACKERS = config.getInt("MaxAttackers", 500);
CHS_CLAN_MINLEVEL = config.getInt("MinClanLevel", 4);
CHS_MAX_FLAGS_PER_CLAN = config.getInt("MaxFlagsPerClan", 1);
CHS_ENABLE_FAME = config.getBoolean("EnableFame", false);
CHS_FAME_AMOUNT = config.getInt("FameAmount", 0);
CHS_FAME_FREQUENCY = config.getInt("FameFrequency", 0);
}
}
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import java.util.HashSet;
import java.util.Set;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the development related configurations.
* @author Mobius
*/
public class DevelopmentConfig
{
// File
private static final String DEVELOPMENT_CONFIG_FILE = "./config/Development.ini";
// Constants
public static boolean LOG_SERVER_LOAD_TIMES;
public static boolean HTML_ACTION_CACHE_DEBUG;
public static boolean NO_QUESTS;
public static boolean NO_SPAWNS;
public static boolean SHOW_QUEST_LOAD_IN_LOGS;
public static boolean SHOW_SCRIPT_LOAD_IN_LOGS;
public static boolean DEBUG_CLIENT_PACKETS;
public static boolean DEBUG_EX_CLIENT_PACKETS;
public static boolean DEBUG_SERVER_PACKETS;
public static boolean DEBUG_UNKNOWN_PACKETS;
public static Set<String> EXCLUDED_DEBUG_PACKETS;
public static void load()
{
final ConfigReader config = new ConfigReader(DEVELOPMENT_CONFIG_FILE);
LOG_SERVER_LOAD_TIMES = config.getBoolean("LogServerLoadTimes", false);
HTML_ACTION_CACHE_DEBUG = config.getBoolean("HtmlActionCacheDebug", false);
NO_QUESTS = config.getBoolean("NoQuests", false);
NO_SPAWNS = config.getBoolean("NoSpawns", false);
SHOW_QUEST_LOAD_IN_LOGS = config.getBoolean("ShowQuestLoadInLogs", false);
SHOW_SCRIPT_LOAD_IN_LOGS = config.getBoolean("ShowScriptLoadInLogs", false);
DEBUG_CLIENT_PACKETS = config.getBoolean("DebugClientPackets", false);
DEBUG_EX_CLIENT_PACKETS = config.getBoolean("DebugExClientPackets", false);
DEBUG_SERVER_PACKETS = config.getBoolean("DebugServerPackets", false);
DEBUG_UNKNOWN_PACKETS = config.getBoolean("DebugUnknownPackets", true);
final String[] packets = config.getString("ExcludedPacketList", "").trim().split(",");
EXCLUDED_DEBUG_PACKETS = new HashSet<>(packets.length);
for (String packet : packets)
{
EXCLUDED_DEBUG_PACKETS.add(packet.trim());
}
}
}
@@ -0,0 +1,387 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import java.util.ArrayList;
import java.util.List;
import org.l2jmobius.commons.util.ConfigReader;
import org.l2jmobius.commons.util.StringUtil;
/**
* This class loads all the feature related configurations.
* @author Mobius
*/
public class FeatureConfig
{
// File
private static final String FEATURE_CONFIG_FILE = "./config/Feature.ini";
// Constants
public static long CH_TELE_FEE_RATIO;
public static int CH_TELE1_FEE;
public static int CH_TELE2_FEE;
public static long CH_SUPPORT_FEE_RATIO;
public static int CH_SUPPORT1_FEE;
public static int CH_SUPPORT2_FEE;
public static int CH_SUPPORT3_FEE;
public static int CH_SUPPORT4_FEE;
public static int CH_SUPPORT5_FEE;
public static int CH_SUPPORT6_FEE;
public static int CH_SUPPORT7_FEE;
public static int CH_SUPPORT8_FEE;
public static long CH_MPREG_FEE_RATIO;
public static int CH_MPREG1_FEE;
public static int CH_MPREG2_FEE;
public static int CH_MPREG3_FEE;
public static int CH_MPREG4_FEE;
public static int CH_MPREG5_FEE;
public static long CH_HPREG_FEE_RATIO;
public static int CH_HPREG1_FEE;
public static int CH_HPREG2_FEE;
public static int CH_HPREG3_FEE;
public static int CH_HPREG4_FEE;
public static int CH_HPREG5_FEE;
public static int CH_HPREG6_FEE;
public static int CH_HPREG7_FEE;
public static int CH_HPREG8_FEE;
public static int CH_HPREG9_FEE;
public static int CH_HPREG10_FEE;
public static int CH_HPREG11_FEE;
public static int CH_HPREG12_FEE;
public static int CH_HPREG13_FEE;
public static long CH_EXPREG_FEE_RATIO;
public static int CH_EXPREG1_FEE;
public static int CH_EXPREG2_FEE;
public static int CH_EXPREG3_FEE;
public static int CH_EXPREG4_FEE;
public static int CH_EXPREG5_FEE;
public static int CH_EXPREG6_FEE;
public static int CH_EXPREG7_FEE;
public static long CH_ITEM_FEE_RATIO;
public static int CH_ITEM1_FEE;
public static int CH_ITEM2_FEE;
public static int CH_ITEM3_FEE;
public static long CH_CURTAIN_FEE_RATIO;
public static int CH_CURTAIN1_FEE;
public static int CH_CURTAIN2_FEE;
public static long CH_FRONT_FEE_RATIO;
public static int CH_FRONT1_FEE;
public static int CH_FRONT2_FEE;
public static boolean CH_BUFF_FREE;
public static List<Integer> SIEGE_HOUR_LIST;
public static long CS_TELE_FEE_RATIO;
public static int CS_TELE1_FEE;
public static int CS_TELE2_FEE;
public static long CS_SUPPORT_FEE_RATIO;
public static int CS_SUPPORT1_FEE;
public static int CS_SUPPORT2_FEE;
public static long CS_MPREG_FEE_RATIO;
public static int CS_MPREG1_FEE;
public static int CS_MPREG2_FEE;
public static long CS_HPREG_FEE_RATIO;
public static int CS_HPREG1_FEE;
public static int CS_HPREG2_FEE;
public static long CS_EXPREG_FEE_RATIO;
public static int CS_EXPREG1_FEE;
public static int CS_EXPREG2_FEE;
public static int OUTER_DOOR_UPGRADE_PRICE2;
public static int OUTER_DOOR_UPGRADE_PRICE3;
public static int OUTER_DOOR_UPGRADE_PRICE5;
public static int INNER_DOOR_UPGRADE_PRICE2;
public static int INNER_DOOR_UPGRADE_PRICE3;
public static int INNER_DOOR_UPGRADE_PRICE5;
public static int WALL_UPGRADE_PRICE2;
public static int WALL_UPGRADE_PRICE3;
public static int WALL_UPGRADE_PRICE5;
public static int TRAP_UPGRADE_PRICE1;
public static int TRAP_UPGRADE_PRICE2;
public static int TRAP_UPGRADE_PRICE3;
public static int TRAP_UPGRADE_PRICE4;
public static long FS_TELE_FEE_RATIO;
public static int FS_TELE1_FEE;
public static int FS_TELE2_FEE;
public static long FS_SUPPORT_FEE_RATIO;
public static int FS_SUPPORT1_FEE;
public static int FS_SUPPORT2_FEE;
public static long FS_MPREG_FEE_RATIO;
public static int FS_MPREG1_FEE;
public static int FS_MPREG2_FEE;
public static long FS_HPREG_FEE_RATIO;
public static int FS_HPREG1_FEE;
public static int FS_HPREG2_FEE;
public static long FS_EXPREG_FEE_RATIO;
public static int FS_EXPREG1_FEE;
public static int FS_EXPREG2_FEE;
public static int FS_UPDATE_FRQ;
public static int FS_BLOOD_OATH_COUNT;
public static int FS_MAX_SUPPLY_LEVEL;
public static int FS_FEE_FOR_CASTLE;
public static int FS_MAX_OWN_TIME;
public static boolean ALT_SEVENSIGNS_OPEN_CATACUMBS;
public static boolean ALT_SEVENSIGNS_OPEN_NECROPOLIS;
public static boolean ALT_GAME_CASTLE_DAWN;
public static boolean ALT_GAME_CASTLE_DUSK;
public static boolean ALT_GAME_REQUIRE_CLAN_CASTLE;
public static int ALT_FESTIVAL_MIN_PLAYER;
public static int ALT_MAXIMUM_PLAYER_CONTRIB;
public static long ALT_FESTIVAL_MANAGER_START;
public static long ALT_FESTIVAL_LENGTH;
public static long ALT_FESTIVAL_CYCLE_LENGTH;
public static long ALT_FESTIVAL_FIRST_SPAWN;
public static long ALT_FESTIVAL_FIRST_SWARM;
public static long ALT_FESTIVAL_SECOND_SPAWN;
public static long ALT_FESTIVAL_SECOND_SWARM;
public static long ALT_FESTIVAL_CHEST_SPAWN;
public static double ALT_SIEGE_DAWN_GATES_PDEF_MULT;
public static double ALT_SIEGE_DUSK_GATES_PDEF_MULT;
public static double ALT_SIEGE_DAWN_GATES_MDEF_MULT;
public static double ALT_SIEGE_DUSK_GATES_MDEF_MULT;
public static boolean ALT_STRICT_SEVENSIGNS;
public static boolean ALT_SEVENSIGNS_LAZY_UPDATE;
public static int SSQ_DAWN_TICKET_QUANTITY;
public static int SSQ_DAWN_TICKET_PRICE;
public static int SSQ_DAWN_TICKET_BUNDLE;
public static int SSQ_MANORS_AGREEMENT_ID;
public static int SSQ_JOIN_DAWN_ADENA_FEE;
public static int TAKE_FORT_POINTS;
public static int LOOSE_FORT_POINTS;
public static int TAKE_CASTLE_POINTS;
public static int LOOSE_CASTLE_POINTS;
public static int CASTLE_DEFENDED_POINTS;
public static int FESTIVAL_WIN_POINTS;
public static int HERO_POINTS;
public static int ROYAL_GUARD_COST;
public static int KNIGHT_UNIT_COST;
public static int KNIGHT_REINFORCE_COST;
public static int BALLISTA_POINTS;
public static int BLOODALLIANCE_POINTS;
public static int BLOODOATH_POINTS;
public static int KNIGHTSEPAULETTE_POINTS;
public static int REPUTATION_SCORE_PER_KILL;
public static int JOIN_ACADEMY_MIN_REP_SCORE;
public static int JOIN_ACADEMY_MAX_REP_SCORE;
public static int RAID_RANKING_1ST;
public static int RAID_RANKING_2ND;
public static int RAID_RANKING_3RD;
public static int RAID_RANKING_4TH;
public static int RAID_RANKING_5TH;
public static int RAID_RANKING_6TH;
public static int RAID_RANKING_7TH;
public static int RAID_RANKING_8TH;
public static int RAID_RANKING_9TH;
public static int RAID_RANKING_10TH;
public static int RAID_RANKING_UP_TO_50TH;
public static int RAID_RANKING_UP_TO_100TH;
public static int CLAN_LEVEL_6_COST;
public static int CLAN_LEVEL_7_COST;
public static int CLAN_LEVEL_8_COST;
public static int CLAN_LEVEL_9_COST;
public static int CLAN_LEVEL_10_COST;
public static int CLAN_LEVEL_6_REQUIREMENT;
public static int CLAN_LEVEL_7_REQUIREMENT;
public static int CLAN_LEVEL_8_REQUIREMENT;
public static int CLAN_LEVEL_9_REQUIREMENT;
public static int CLAN_LEVEL_10_REQUIREMENT;
public static boolean ALLOW_WYVERN_ALWAYS;
public static boolean ALLOW_WYVERN_DURING_SIEGE;
public static boolean ALLOW_MOUNTS_DURING_SIEGE;
public static void load()
{
final ConfigReader config = new ConfigReader(FEATURE_CONFIG_FILE);
CH_TELE_FEE_RATIO = config.getLong("ClanHallTeleportFunctionFeeRatio", 604800000);
CH_TELE1_FEE = config.getInt("ClanHallTeleportFunctionFeeLvl1", 7000);
CH_TELE2_FEE = config.getInt("ClanHallTeleportFunctionFeeLvl2", 14000);
CH_SUPPORT_FEE_RATIO = config.getLong("ClanHallSupportFunctionFeeRatio", 86400000);
CH_SUPPORT1_FEE = config.getInt("ClanHallSupportFeeLvl1", 2500);
CH_SUPPORT2_FEE = config.getInt("ClanHallSupportFeeLvl2", 5000);
CH_SUPPORT3_FEE = config.getInt("ClanHallSupportFeeLvl3", 7000);
CH_SUPPORT4_FEE = config.getInt("ClanHallSupportFeeLvl4", 11000);
CH_SUPPORT5_FEE = config.getInt("ClanHallSupportFeeLvl5", 21000);
CH_SUPPORT6_FEE = config.getInt("ClanHallSupportFeeLvl6", 36000);
CH_SUPPORT7_FEE = config.getInt("ClanHallSupportFeeLvl7", 37000);
CH_SUPPORT8_FEE = config.getInt("ClanHallSupportFeeLvl8", 52000);
CH_MPREG_FEE_RATIO = config.getLong("ClanHallMpRegenerationFunctionFeeRatio", 86400000);
CH_MPREG1_FEE = config.getInt("ClanHallMpRegenerationFeeLvl1", 2000);
CH_MPREG2_FEE = config.getInt("ClanHallMpRegenerationFeeLvl2", 3750);
CH_MPREG3_FEE = config.getInt("ClanHallMpRegenerationFeeLvl3", 6500);
CH_MPREG4_FEE = config.getInt("ClanHallMpRegenerationFeeLvl4", 13750);
CH_MPREG5_FEE = config.getInt("ClanHallMpRegenerationFeeLvl5", 20000);
CH_HPREG_FEE_RATIO = config.getLong("ClanHallHpRegenerationFunctionFeeRatio", 86400000);
CH_HPREG1_FEE = config.getInt("ClanHallHpRegenerationFeeLvl1", 700);
CH_HPREG2_FEE = config.getInt("ClanHallHpRegenerationFeeLvl2", 800);
CH_HPREG3_FEE = config.getInt("ClanHallHpRegenerationFeeLvl3", 1000);
CH_HPREG4_FEE = config.getInt("ClanHallHpRegenerationFeeLvl4", 1166);
CH_HPREG5_FEE = config.getInt("ClanHallHpRegenerationFeeLvl5", 1500);
CH_HPREG6_FEE = config.getInt("ClanHallHpRegenerationFeeLvl6", 1750);
CH_HPREG7_FEE = config.getInt("ClanHallHpRegenerationFeeLvl7", 2000);
CH_HPREG8_FEE = config.getInt("ClanHallHpRegenerationFeeLvl8", 2250);
CH_HPREG9_FEE = config.getInt("ClanHallHpRegenerationFeeLvl9", 2500);
CH_HPREG10_FEE = config.getInt("ClanHallHpRegenerationFeeLvl10", 3250);
CH_HPREG11_FEE = config.getInt("ClanHallHpRegenerationFeeLvl11", 3270);
CH_HPREG12_FEE = config.getInt("ClanHallHpRegenerationFeeLvl12", 4250);
CH_HPREG13_FEE = config.getInt("ClanHallHpRegenerationFeeLvl13", 5166);
CH_EXPREG_FEE_RATIO = config.getLong("ClanHallExpRegenerationFunctionFeeRatio", 86400000);
CH_EXPREG1_FEE = config.getInt("ClanHallExpRegenerationFeeLvl1", 3000);
CH_EXPREG2_FEE = config.getInt("ClanHallExpRegenerationFeeLvl2", 6000);
CH_EXPREG3_FEE = config.getInt("ClanHallExpRegenerationFeeLvl3", 9000);
CH_EXPREG4_FEE = config.getInt("ClanHallExpRegenerationFeeLvl4", 15000);
CH_EXPREG5_FEE = config.getInt("ClanHallExpRegenerationFeeLvl5", 21000);
CH_EXPREG6_FEE = config.getInt("ClanHallExpRegenerationFeeLvl6", 23330);
CH_EXPREG7_FEE = config.getInt("ClanHallExpRegenerationFeeLvl7", 30000);
CH_ITEM_FEE_RATIO = config.getLong("ClanHallItemCreationFunctionFeeRatio", 86400000);
CH_ITEM1_FEE = config.getInt("ClanHallItemCreationFunctionFeeLvl1", 30000);
CH_ITEM2_FEE = config.getInt("ClanHallItemCreationFunctionFeeLvl2", 70000);
CH_ITEM3_FEE = config.getInt("ClanHallItemCreationFunctionFeeLvl3", 140000);
CH_CURTAIN_FEE_RATIO = config.getLong("ClanHallCurtainFunctionFeeRatio", 604800000);
CH_CURTAIN1_FEE = config.getInt("ClanHallCurtainFunctionFeeLvl1", 2000);
CH_CURTAIN2_FEE = config.getInt("ClanHallCurtainFunctionFeeLvl2", 2500);
CH_FRONT_FEE_RATIO = config.getLong("ClanHallFrontPlatformFunctionFeeRatio", 259200000);
CH_FRONT1_FEE = config.getInt("ClanHallFrontPlatformFunctionFeeLvl1", 1300);
CH_FRONT2_FEE = config.getInt("ClanHallFrontPlatformFunctionFeeLvl2", 4000);
CH_BUFF_FREE = config.getBoolean("AltClanHallMpBuffFree", false);
SIEGE_HOUR_LIST = new ArrayList<>();
for (String hour : config.getString("SiegeHourList", "").split(","))
{
if (StringUtil.isNumeric(hour))
{
SIEGE_HOUR_LIST.add(Integer.parseInt(hour));
}
}
CS_TELE_FEE_RATIO = config.getLong("CastleTeleportFunctionFeeRatio", 604800000);
CS_TELE1_FEE = config.getInt("CastleTeleportFunctionFeeLvl1", 1000);
CS_TELE2_FEE = config.getInt("CastleTeleportFunctionFeeLvl2", 10000);
CS_SUPPORT_FEE_RATIO = config.getLong("CastleSupportFunctionFeeRatio", 604800000);
CS_SUPPORT1_FEE = config.getInt("CastleSupportFeeLvl1", 49000);
CS_SUPPORT2_FEE = config.getInt("CastleSupportFeeLvl2", 120000);
CS_MPREG_FEE_RATIO = config.getLong("CastleMpRegenerationFunctionFeeRatio", 604800000);
CS_MPREG1_FEE = config.getInt("CastleMpRegenerationFeeLvl1", 45000);
CS_MPREG2_FEE = config.getInt("CastleMpRegenerationFeeLvl2", 65000);
CS_HPREG_FEE_RATIO = config.getLong("CastleHpRegenerationFunctionFeeRatio", 604800000);
CS_HPREG1_FEE = config.getInt("CastleHpRegenerationFeeLvl1", 12000);
CS_HPREG2_FEE = config.getInt("CastleHpRegenerationFeeLvl2", 20000);
CS_EXPREG_FEE_RATIO = config.getLong("CastleExpRegenerationFunctionFeeRatio", 604800000);
CS_EXPREG1_FEE = config.getInt("CastleExpRegenerationFeeLvl1", 63000);
CS_EXPREG2_FEE = config.getInt("CastleExpRegenerationFeeLvl2", 70000);
OUTER_DOOR_UPGRADE_PRICE2 = config.getInt("OuterDoorUpgradePriceLvl2", 3000000);
OUTER_DOOR_UPGRADE_PRICE3 = config.getInt("OuterDoorUpgradePriceLvl3", 4000000);
OUTER_DOOR_UPGRADE_PRICE5 = config.getInt("OuterDoorUpgradePriceLvl5", 5000000);
INNER_DOOR_UPGRADE_PRICE2 = config.getInt("InnerDoorUpgradePriceLvl2", 750000);
INNER_DOOR_UPGRADE_PRICE3 = config.getInt("InnerDoorUpgradePriceLvl3", 900000);
INNER_DOOR_UPGRADE_PRICE5 = config.getInt("InnerDoorUpgradePriceLvl5", 1000000);
WALL_UPGRADE_PRICE2 = config.getInt("WallUpgradePriceLvl2", 1600000);
WALL_UPGRADE_PRICE3 = config.getInt("WallUpgradePriceLvl3", 1800000);
WALL_UPGRADE_PRICE5 = config.getInt("WallUpgradePriceLvl5", 2000000);
TRAP_UPGRADE_PRICE1 = config.getInt("TrapUpgradePriceLvl1", 3000000);
TRAP_UPGRADE_PRICE2 = config.getInt("TrapUpgradePriceLvl2", 4000000);
TRAP_UPGRADE_PRICE3 = config.getInt("TrapUpgradePriceLvl3", 5000000);
TRAP_UPGRADE_PRICE4 = config.getInt("TrapUpgradePriceLvl4", 6000000);
FS_TELE_FEE_RATIO = config.getLong("FortressTeleportFunctionFeeRatio", 604800000);
FS_TELE1_FEE = config.getInt("FortressTeleportFunctionFeeLvl1", 1000);
FS_TELE2_FEE = config.getInt("FortressTeleportFunctionFeeLvl2", 10000);
FS_SUPPORT_FEE_RATIO = config.getLong("FortressSupportFunctionFeeRatio", 86400000);
FS_SUPPORT1_FEE = config.getInt("FortressSupportFeeLvl1", 7000);
FS_SUPPORT2_FEE = config.getInt("FortressSupportFeeLvl2", 17000);
FS_MPREG_FEE_RATIO = config.getLong("FortressMpRegenerationFunctionFeeRatio", 86400000);
FS_MPREG1_FEE = config.getInt("FortressMpRegenerationFeeLvl1", 6500);
FS_MPREG2_FEE = config.getInt("FortressMpRegenerationFeeLvl2", 9300);
FS_HPREG_FEE_RATIO = config.getLong("FortressHpRegenerationFunctionFeeRatio", 86400000);
FS_HPREG1_FEE = config.getInt("FortressHpRegenerationFeeLvl1", 2000);
FS_HPREG2_FEE = config.getInt("FortressHpRegenerationFeeLvl2", 3500);
FS_EXPREG_FEE_RATIO = config.getLong("FortressExpRegenerationFunctionFeeRatio", 86400000);
FS_EXPREG1_FEE = config.getInt("FortressExpRegenerationFeeLvl1", 9000);
FS_EXPREG2_FEE = config.getInt("FortressExpRegenerationFeeLvl2", 10000);
FS_UPDATE_FRQ = config.getInt("FortressPeriodicUpdateFrequency", 360);
FS_BLOOD_OATH_COUNT = config.getInt("FortressBloodOathCount", 1);
FS_MAX_SUPPLY_LEVEL = config.getInt("FortressMaxSupplyLevel", 6);
FS_FEE_FOR_CASTLE = config.getInt("FortressFeeForCastle", 25000);
FS_MAX_OWN_TIME = config.getInt("FortressMaximumOwnTime", 168);
ALT_SEVENSIGNS_OPEN_CATACUMBS = config.getBoolean("AltOpenCatacumbs", false);
ALT_SEVENSIGNS_OPEN_NECROPOLIS = config.getBoolean("AltOpenNecropolis", false);
ALT_GAME_CASTLE_DAWN = config.getBoolean("AltCastleForDawn", true);
ALT_GAME_CASTLE_DUSK = config.getBoolean("AltCastleForDusk", true);
ALT_GAME_REQUIRE_CLAN_CASTLE = config.getBoolean("AltRequireClanCastle", false);
ALT_FESTIVAL_MIN_PLAYER = config.getInt("AltFestivalMinPlayer", 5);
ALT_MAXIMUM_PLAYER_CONTRIB = config.getInt("AltMaxPlayerContrib", 1000000);
ALT_FESTIVAL_MANAGER_START = config.getLong("AltFestivalManagerStart", 120000);
ALT_FESTIVAL_LENGTH = config.getLong("AltFestivalLength", 1080000);
ALT_FESTIVAL_CYCLE_LENGTH = config.getLong("AltFestivalCycleLength", 2280000);
ALT_FESTIVAL_FIRST_SPAWN = config.getLong("AltFestivalFirstSpawn", 120000);
ALT_FESTIVAL_FIRST_SWARM = config.getLong("AltFestivalFirstSwarm", 300000);
ALT_FESTIVAL_SECOND_SPAWN = config.getLong("AltFestivalSecondSpawn", 540000);
ALT_FESTIVAL_SECOND_SWARM = config.getLong("AltFestivalSecondSwarm", 720000);
ALT_FESTIVAL_CHEST_SPAWN = config.getLong("AltFestivalChestSpawn", 900000);
ALT_SIEGE_DAWN_GATES_PDEF_MULT = config.getDouble("AltDawnGatesPdefMult", 1.1);
ALT_SIEGE_DUSK_GATES_PDEF_MULT = config.getDouble("AltDuskGatesPdefMult", 0.8);
ALT_SIEGE_DAWN_GATES_MDEF_MULT = config.getDouble("AltDawnGatesMdefMult", 1.1);
ALT_SIEGE_DUSK_GATES_MDEF_MULT = config.getDouble("AltDuskGatesMdefMult", 0.8);
ALT_STRICT_SEVENSIGNS = config.getBoolean("StrictSevenSigns", true);
ALT_SEVENSIGNS_LAZY_UPDATE = config.getBoolean("AltSevenSignsLazyUpdate", true);
SSQ_DAWN_TICKET_QUANTITY = config.getInt("SevenSignsDawnTicketQuantity", 300);
SSQ_DAWN_TICKET_PRICE = config.getInt("SevenSignsDawnTicketPrice", 1000);
SSQ_DAWN_TICKET_BUNDLE = config.getInt("SevenSignsDawnTicketBundle", 10);
SSQ_MANORS_AGREEMENT_ID = config.getInt("SevenSignsManorsAgreementId", 6388);
SSQ_JOIN_DAWN_ADENA_FEE = config.getInt("SevenSignsJoinDawnFee", 50000);
TAKE_FORT_POINTS = config.getInt("TakeFortPoints", 200);
LOOSE_FORT_POINTS = config.getInt("LooseFortPoints", 0);
TAKE_CASTLE_POINTS = config.getInt("TakeCastlePoints", 1500);
LOOSE_CASTLE_POINTS = config.getInt("LooseCastlePoints", 3000);
CASTLE_DEFENDED_POINTS = config.getInt("CastleDefendedPoints", 750);
FESTIVAL_WIN_POINTS = config.getInt("FestivalOfDarknessWin", 200);
HERO_POINTS = config.getInt("HeroPoints", 1000);
ROYAL_GUARD_COST = config.getInt("CreateRoyalGuardCost", 5000);
KNIGHT_UNIT_COST = config.getInt("CreateKnightUnitCost", 10000);
KNIGHT_REINFORCE_COST = config.getInt("ReinforceKnightUnitCost", 5000);
BALLISTA_POINTS = config.getInt("KillBallistaPoints", 30);
BLOODALLIANCE_POINTS = config.getInt("BloodAlliancePoints", 500);
BLOODOATH_POINTS = config.getInt("BloodOathPoints", 200);
KNIGHTSEPAULETTE_POINTS = config.getInt("KnightsEpaulettePoints", 20);
REPUTATION_SCORE_PER_KILL = config.getInt("ReputationScorePerKill", 1);
JOIN_ACADEMY_MIN_REP_SCORE = config.getInt("CompleteAcademyMinPoints", 190);
JOIN_ACADEMY_MAX_REP_SCORE = config.getInt("CompleteAcademyMaxPoints", 650);
RAID_RANKING_1ST = config.getInt("1stRaidRankingPoints", 1250);
RAID_RANKING_2ND = config.getInt("2ndRaidRankingPoints", 900);
RAID_RANKING_3RD = config.getInt("3rdRaidRankingPoints", 700);
RAID_RANKING_4TH = config.getInt("4thRaidRankingPoints", 600);
RAID_RANKING_5TH = config.getInt("5thRaidRankingPoints", 450);
RAID_RANKING_6TH = config.getInt("6thRaidRankingPoints", 350);
RAID_RANKING_7TH = config.getInt("7thRaidRankingPoints", 300);
RAID_RANKING_8TH = config.getInt("8thRaidRankingPoints", 200);
RAID_RANKING_9TH = config.getInt("9thRaidRankingPoints", 150);
RAID_RANKING_10TH = config.getInt("10thRaidRankingPoints", 100);
RAID_RANKING_UP_TO_50TH = config.getInt("UpTo50thRaidRankingPoints", 25);
RAID_RANKING_UP_TO_100TH = config.getInt("UpTo100thRaidRankingPoints", 12);
CLAN_LEVEL_6_COST = config.getInt("ClanLevel6Cost", 5000);
CLAN_LEVEL_7_COST = config.getInt("ClanLevel7Cost", 10000);
CLAN_LEVEL_8_COST = config.getInt("ClanLevel8Cost", 20000);
CLAN_LEVEL_9_COST = config.getInt("ClanLevel9Cost", 40000);
CLAN_LEVEL_10_COST = config.getInt("ClanLevel10Cost", 40000);
CLAN_LEVEL_6_REQUIREMENT = config.getInt("ClanLevel6Requirement", 30);
CLAN_LEVEL_7_REQUIREMENT = config.getInt("ClanLevel7Requirement", 50);
CLAN_LEVEL_8_REQUIREMENT = config.getInt("ClanLevel8Requirement", 80);
CLAN_LEVEL_9_REQUIREMENT = config.getInt("ClanLevel9Requirement", 120);
CLAN_LEVEL_10_REQUIREMENT = config.getInt("ClanLevel10Requirement", 140);
ALLOW_WYVERN_ALWAYS = config.getBoolean("AllowRideWyvernAlways", false);
ALLOW_WYVERN_DURING_SIEGE = config.getBoolean("AllowRideWyvernDuringSiege", true);
ALLOW_MOUNTS_DURING_SIEGE = config.getBoolean("AllowRideMountsDuringSiege", false);
}
}
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import org.l2jmobius.commons.util.ConfigReader;
import org.l2jmobius.gameserver.util.FloodProtectorSettings;
/**
* This class loads all the flood protector related configurations.
* @author Mobius
*/
public class FloodProtectorConfig
{
// File
private static final String FLOOD_PROTECTOR_CONFIG_FILE = "./config/FloodProtector.ini";
// Constants
public static FloodProtectorSettings FLOOD_PROTECTOR_USE_ITEM;
public static FloodProtectorSettings FLOOD_PROTECTOR_ROLL_DICE;
public static FloodProtectorSettings FLOOD_PROTECTOR_ITEM_PET_SUMMON;
public static FloodProtectorSettings FLOOD_PROTECTOR_HERO_VOICE;
public static FloodProtectorSettings FLOOD_PROTECTOR_GLOBAL_CHAT;
public static FloodProtectorSettings FLOOD_PROTECTOR_SUBCLASS;
public static FloodProtectorSettings FLOOD_PROTECTOR_DROP_ITEM;
public static FloodProtectorSettings FLOOD_PROTECTOR_ENCHANT_ITEM;
public static FloodProtectorSettings FLOOD_PROTECTOR_SERVER_BYPASS;
public static FloodProtectorSettings FLOOD_PROTECTOR_MULTISELL;
public static FloodProtectorSettings FLOOD_PROTECTOR_TRANSACTION;
public static FloodProtectorSettings FLOOD_PROTECTOR_MANUFACTURE;
public static FloodProtectorSettings FLOOD_PROTECTOR_SENDMAIL;
public static FloodProtectorSettings FLOOD_PROTECTOR_CHARACTER_SELECT;
public static FloodProtectorSettings FLOOD_PROTECTOR_ITEM_AUCTION;
public static FloodProtectorSettings FLOOD_PROTECTOR_PLAYER_ACTION;
public static void load()
{
final ConfigReader config = new ConfigReader(FLOOD_PROTECTOR_CONFIG_FILE);
FLOOD_PROTECTOR_USE_ITEM = new FloodProtectorSettings("UseItemFloodProtector");
FLOOD_PROTECTOR_ROLL_DICE = new FloodProtectorSettings("RollDiceFloodProtector");
FLOOD_PROTECTOR_ITEM_PET_SUMMON = new FloodProtectorSettings("ItemPetSummonFloodProtector");
FLOOD_PROTECTOR_HERO_VOICE = new FloodProtectorSettings("HeroVoiceFloodProtector");
FLOOD_PROTECTOR_GLOBAL_CHAT = new FloodProtectorSettings("GlobalChatFloodProtector");
FLOOD_PROTECTOR_SUBCLASS = new FloodProtectorSettings("SubclassFloodProtector");
FLOOD_PROTECTOR_DROP_ITEM = new FloodProtectorSettings("DropItemFloodProtector");
FLOOD_PROTECTOR_ENCHANT_ITEM = new FloodProtectorSettings("EnchantItemFloodProtector");
FLOOD_PROTECTOR_SERVER_BYPASS = new FloodProtectorSettings("ServerBypassFloodProtector");
FLOOD_PROTECTOR_MULTISELL = new FloodProtectorSettings("MultiSellFloodProtector");
FLOOD_PROTECTOR_TRANSACTION = new FloodProtectorSettings("TransactionFloodProtector");
FLOOD_PROTECTOR_MANUFACTURE = new FloodProtectorSettings("ManufactureFloodProtector");
FLOOD_PROTECTOR_SENDMAIL = new FloodProtectorSettings("SendMailFloodProtector");
FLOOD_PROTECTOR_CHARACTER_SELECT = new FloodProtectorSettings("CharacterSelectFloodProtector");
FLOOD_PROTECTOR_ITEM_AUCTION = new FloodProtectorSettings("ItemAuctionFloodProtector");
FLOOD_PROTECTOR_PLAYER_ACTION = new FloodProtectorSettings("PlayerActionFloodProtector");
loadFloodProtectorConfigs(config);
}
/**
* Loads flood protector configurations.
* @param configs the ConfigReader parser containing the actual values of the flood protector
*/
private static void loadFloodProtectorConfigs(ConfigReader configs)
{
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_USE_ITEM, "UseItem", 4);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_ROLL_DICE, "RollDice", 42);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_ITEM_PET_SUMMON, "ItemPetSummon", 16);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_HERO_VOICE, "HeroVoice", 100);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_GLOBAL_CHAT, "GlobalChat", 5);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_SUBCLASS, "Subclass", 20);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_DROP_ITEM, "DropItem", 10);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_SERVER_BYPASS, "ServerBypass", 5);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_MULTISELL, "MultiSell", 1);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_TRANSACTION, "Transaction", 10);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_MANUFACTURE, "Manufacture", 3);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_SENDMAIL, "SendMail", 100);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_CHARACTER_SELECT, "CharacterSelect", 30);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_ITEM_AUCTION, "ItemAuction", 9);
loadFloodProtectorConfig(configs, FLOOD_PROTECTOR_PLAYER_ACTION, "PlayerAction", 3);
}
/**
* Loads single flood protector configuration.
* @param configReader the ConfigReader parser
* @param floodConfig flood protector configuration instance
* @param configString flood protector configuration string that determines for which flood protector configuration should be read
* @param defaultInterval default flood protector interval
*/
private static void loadFloodProtectorConfig(ConfigReader configReader, FloodProtectorSettings floodConfig, String configString, int defaultInterval)
{
floodConfig.setProtectionInterval(configReader.getInt("FloodProtector" + configString + "Interval", defaultInterval));
floodConfig.setLogFlooding(configReader.getBoolean("FloodProtector" + configString + "LogFlooding", false));
floodConfig.setPunishmentLimit(configReader.getInt("FloodProtector" + configString + "PunishmentLimit", 0));
floodConfig.setPunishmentType(configReader.getString("FloodProtector" + configString + "PunishmentType", "none"));
floodConfig.setPunishmentTime(configReader.getInt("FloodProtector" + configString + "PunishmentTime", 0) * 60000);
}
}
@@ -0,0 +1,350 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.l2jmobius.commons.util.ConfigReader;
import org.l2jmobius.gameserver.entity.actor.enums.player.ChatBroadcastType;
import org.l2jmobius.gameserver.entity.actor.enums.player.IllegalActionPunishmentType;
import org.l2jmobius.gameserver.network.enums.ChatType;
/**
* This class loads all the general related configurations.
* @author Mobius
*/
public class GeneralConfig
{
private static final Logger LOGGER = Logger.getLogger(GeneralConfig.class.getName());
// File
private static final String GENERAL_CONFIG_FILE = "./config/General.ini";
// Constants
public static boolean EVERYBODY_HAS_ADMIN_RIGHTS;
public static boolean SERVER_GMONLY;
public static boolean GM_HERO_AURA;
public static boolean GM_STARTUP_BUILDER_HIDE;
public static boolean GM_STARTUP_INVULNERABLE;
public static boolean GM_STARTUP_INVISIBLE;
public static boolean GM_STARTUP_SILENCE;
public static boolean GM_STARTUP_AUTO_LIST;
public static boolean GM_STARTUP_DIET_MODE;
public static boolean GM_ITEM_RESTRICTION;
public static boolean GM_SKILL_RESTRICTION;
public static boolean GM_TRADE_RESTRICTED_ITEMS;
public static boolean GM_RESTART_FIGHTING;
public static boolean GM_ANNOUNCER_NAME;
public static boolean GM_CRITANNOUNCER_NAME;
public static boolean GM_GIVE_SPECIAL_SKILLS;
public static boolean GM_GIVE_SPECIAL_AURA_SKILLS;
public static boolean GM_DEBUG_HTML_PATHS;
public static boolean USE_SUPER_HASTE_AS_GM_SPEED;
public static boolean LOG_CHAT;
public static boolean LOG_ITEMS;
public static boolean LOG_ITEMS_SMALL_LOG;
public static boolean LOG_ITEMS_IDS_ONLY;
public static Set<Integer> LOG_ITEMS_IDS_LIST;
public static boolean LOG_ITEM_ENCHANTS;
public static boolean LOG_SKILL_ENCHANTS;
public static boolean GMAUDIT;
public static boolean SKILL_CHECK_ENABLE;
public static boolean SKILL_CHECK_REMOVE;
public static boolean SKILL_CHECK_GM;
public static boolean ALLOW_DISCARDITEM;
public static int AUTODESTROY_ITEM_AFTER;
public static int HERB_AUTO_DESTROY_TIME;
public static Set<Integer> LIST_PROTECTED_ITEMS;
public static int CHAR_DATA_STORE_INTERVAL;
public static boolean LAZY_ITEMS_UPDATE;
public static boolean UPDATE_ITEMS_ON_CHAR_STORE;
public static boolean DESTROY_DROPPED_PLAYER_ITEM;
public static boolean DESTROY_EQUIPABLE_PLAYER_ITEM;
public static boolean DESTROY_ALL_ITEMS;
public static boolean SAVE_DROPPED_ITEM;
public static boolean EMPTY_DROPPED_ITEM_TABLE_AFTER_LOAD;
public static int SAVE_DROPPED_ITEM_INTERVAL;
public static boolean CLEAR_DROPPED_ITEM_TABLE;
public static boolean AUTODELETE_INVALID_QUEST_DATA;
public static boolean MULTIPLE_ITEM_DROP;
public static boolean HTM_CACHE;
public static boolean CHECK_HTML_ENCODING;
public static int MIN_NPC_ANIMATION;
public static int MAX_NPC_ANIMATION;
public static int MIN_MONSTER_ANIMATION;
public static int MAX_MONSTER_ANIMATION;
public static boolean GRIDS_ALWAYS_ON;
public static int GRID_NEIGHBOR_TURNON_TIME;
public static int GRID_NEIGHBOR_TURNOFF_TIME;
public static int PEACE_ZONE_MODE;
public static ChatBroadcastType DEFAULT_GLOBAL_CHAT;
public static ChatBroadcastType DEFAULT_TRADE_CHAT;
public static int MINIMUM_CHAT_LEVEL;
public static boolean ALLOW_WAREHOUSE;
public static boolean ALLOW_REFUND;
public static boolean ALLOW_WEAR;
public static int WEAR_DELAY;
public static int WEAR_PRICE;
public static boolean ALT_VILLAGES_REPEATABLE_QUEST_REWARD;
public static int INSTANCE_FINISH_TIME;
public static boolean RESTORE_PLAYER_INSTANCE;
public static boolean ALLOW_SUMMON_IN_INSTANCE;
public static int EJECT_DEAD_PLAYER_TIME;
public static boolean ALLOW_LOTTERY;
public static boolean ALLOW_RACE;
public static boolean ALLOW_WATER;
public static boolean ALLOW_FISHING;
public static boolean ALLOW_MANOR;
public static boolean ALLOW_BOAT;
public static int BOAT_BROADCAST_RADIUS;
public static boolean ALLOW_CURSED_WEAPONS;
public static boolean ALLOW_PARTY_IN_SAME_EVENT;
public static boolean SERVER_NEWS;
public static boolean ENABLE_COMMUNITY_BOARD;
public static String BBS_DEFAULT;
public static boolean USE_SAY_FILTER;
public static String CHAT_FILTER_CHARS;
public static Set<ChatType> BAN_CHAT_CHANNELS;
public static int ALT_MANOR_REFRESH_TIME;
public static int ALT_MANOR_REFRESH_MIN;
public static int ALT_MANOR_APPROVE_TIME;
public static int ALT_MANOR_APPROVE_MIN;
public static int ALT_MANOR_MAINTENANCE_MIN;
public static boolean ALT_MANOR_SAVE_ALL_ACTIONS;
public static int ALT_MANOR_SAVE_PERIOD_RATE;
public static int ALT_LOTTERY_PRIZE;
public static int ALT_LOTTERY_TICKET_PRICE;
public static float ALT_LOTTERY_5_NUMBER_RATE;
public static float ALT_LOTTERY_4_NUMBER_RATE;
public static float ALT_LOTTERY_3_NUMBER_RATE;
public static int ALT_LOTTERY_2_AND_1_NUMBER_PRIZE;
public static boolean ALT_FISH_CHAMPIONSHIP_ENABLED;
public static int ALT_FISH_CHAMPIONSHIP_REWARD_ITEM;
public static int ALT_FISH_CHAMPIONSHIP_REWARD_1;
public static int ALT_FISH_CHAMPIONSHIP_REWARD_2;
public static int ALT_FISH_CHAMPIONSHIP_REWARD_3;
public static int ALT_FISH_CHAMPIONSHIP_REWARD_4;
public static int ALT_FISH_CHAMPIONSHIP_REWARD_5;
public static boolean ALT_ITEM_AUCTION_ENABLED;
public static int ALT_ITEM_AUCTION_EXPIRED_AFTER;
public static long ALT_ITEM_AUCTION_TIME_EXTENDS_ON_BID;
public static int RIFT_MIN_PARTY_SIZE;
public static int RIFT_MAX_JUMPS;
public static int RIFT_SPAWN_DELAY;
public static int RIFT_AUTO_JUMPS_TIME_MIN;
public static int RIFT_AUTO_JUMPS_TIME_MAX;
public static float RIFT_BOSS_ROOM_TIME_MUTIPLY;
public static int RIFT_ENTER_COST_RECRUIT;
public static int RIFT_ENTER_COST_SOLDIER;
public static int RIFT_ENTER_COST_OFFICER;
public static int RIFT_ENTER_COST_CAPTAIN;
public static int RIFT_ENTER_COST_COMMANDER;
public static int RIFT_ENTER_COST_HERO;
public static IllegalActionPunishmentType DEFAULT_PUNISH;
public static long DEFAULT_PUNISH_PARAM;
public static boolean ONLY_GM_ITEMS_FREE;
public static boolean JAIL_IS_PVP;
public static boolean JAIL_DISABLE_CHAT;
public static boolean JAIL_DISABLE_TRANSACTION;
public static boolean CUSTOM_NPC_DATA;
public static boolean CUSTOM_TELEPORT_TABLE;
public static boolean CUSTOM_SKILLS_LOAD;
public static boolean CUSTOM_ITEMS_LOAD;
public static boolean CUSTOM_MULTISELL_LOAD;
public static boolean CUSTOM_BUYLIST_LOAD;
public static int NORMAL_ENCHANT_COST_MULTIPLIER;
public static int SAFE_ENCHANT_COST_MULTIPLIER;
public static boolean CORRECT_PRICES;
public static long MULTISELL_AMOUNT_LIMIT;
public static boolean ENABLE_FALLING_DAMAGE;
public static boolean DEBUFF_DURATION_USES_RESISTS;
public static void load()
{
final ConfigReader config = new ConfigReader(GENERAL_CONFIG_FILE);
EVERYBODY_HAS_ADMIN_RIGHTS = config.getBoolean("EverybodyHasAdminRights", false);
SERVER_GMONLY = config.getBoolean("ServerGMOnly", false);
GM_HERO_AURA = config.getBoolean("GMHeroAura", false);
GM_STARTUP_BUILDER_HIDE = config.getBoolean("GMStartupBuilderHide", false);
GM_STARTUP_INVULNERABLE = config.getBoolean("GMStartupInvulnerable", false);
GM_STARTUP_INVISIBLE = config.getBoolean("GMStartupInvisible", false);
GM_STARTUP_SILENCE = config.getBoolean("GMStartupSilence", false);
GM_STARTUP_AUTO_LIST = config.getBoolean("GMStartupAutoList", false);
GM_STARTUP_DIET_MODE = config.getBoolean("GMStartupDietMode", false);
GM_ITEM_RESTRICTION = config.getBoolean("GMItemRestriction", true);
GM_SKILL_RESTRICTION = config.getBoolean("GMSkillRestriction", true);
GM_TRADE_RESTRICTED_ITEMS = config.getBoolean("GMTradeRestrictedItems", false);
GM_RESTART_FIGHTING = config.getBoolean("GMRestartFighting", true);
GM_ANNOUNCER_NAME = config.getBoolean("GMShowAnnouncerName", false);
GM_CRITANNOUNCER_NAME = config.getBoolean("GMShowCritAnnouncerName", false);
GM_GIVE_SPECIAL_SKILLS = config.getBoolean("GMGiveSpecialSkills", false);
GM_GIVE_SPECIAL_AURA_SKILLS = config.getBoolean("GMGiveSpecialAuraSkills", false);
GM_DEBUG_HTML_PATHS = config.getBoolean("GMDebugHtmlPaths", true);
USE_SUPER_HASTE_AS_GM_SPEED = config.getBoolean("UseSuperHasteAsGMSpeed", false);
LOG_CHAT = config.getBoolean("LogChat", false);
LOG_ITEMS = config.getBoolean("LogItems", false);
LOG_ITEMS_SMALL_LOG = config.getBoolean("LogItemsSmallLog", false);
LOG_ITEMS_IDS_ONLY = config.getBoolean("LogItemsIdsOnly", false);
final String[] splitItemIds = config.getString("LogItemsIdsList", "0").split(",");
LOG_ITEMS_IDS_LIST = new HashSet<>(splitItemIds.length);
for (String id : splitItemIds)
{
LOG_ITEMS_IDS_LIST.add(Integer.parseInt(id));
}
LOG_ITEM_ENCHANTS = config.getBoolean("LogItemEnchants", false);
LOG_SKILL_ENCHANTS = config.getBoolean("LogSkillEnchants", false);
GMAUDIT = config.getBoolean("GMAudit", false);
SKILL_CHECK_ENABLE = config.getBoolean("SkillCheckEnable", false);
SKILL_CHECK_REMOVE = config.getBoolean("SkillCheckRemove", false);
SKILL_CHECK_GM = config.getBoolean("SkillCheckGM", true);
ALLOW_DISCARDITEM = config.getBoolean("AllowDiscardItem", true);
AUTODESTROY_ITEM_AFTER = config.getInt("AutoDestroyDroppedItemAfter", 600);
HERB_AUTO_DESTROY_TIME = config.getInt("AutoDestroyHerbTime", 60) * 1000;
final String[] split = config.getString("ListOfProtectedItems", "0").split(",");
LIST_PROTECTED_ITEMS = new HashSet<>(split.length);
for (String id : split)
{
LIST_PROTECTED_ITEMS.add(Integer.parseInt(id));
}
CHAR_DATA_STORE_INTERVAL = config.getInt("CharacterDataStoreInterval", 15) * 60 * 1000;
LAZY_ITEMS_UPDATE = config.getBoolean("LazyItemsUpdate", false);
UPDATE_ITEMS_ON_CHAR_STORE = config.getBoolean("UpdateItemsOnCharStore", false);
DESTROY_DROPPED_PLAYER_ITEM = config.getBoolean("DestroyPlayerDroppedItem", false);
DESTROY_EQUIPABLE_PLAYER_ITEM = config.getBoolean("DestroyEquipableItem", false);
DESTROY_ALL_ITEMS = config.getBoolean("DestroyAllItems", false);
SAVE_DROPPED_ITEM = config.getBoolean("SaveDroppedItem", false);
EMPTY_DROPPED_ITEM_TABLE_AFTER_LOAD = config.getBoolean("EmptyDroppedItemTableAfterLoad", false);
SAVE_DROPPED_ITEM_INTERVAL = config.getInt("SaveDroppedItemInterval", 60) * 60000;
CLEAR_DROPPED_ITEM_TABLE = config.getBoolean("ClearDroppedItemTable", false);
AUTODELETE_INVALID_QUEST_DATA = config.getBoolean("AutoDeleteInvalidQuestData", false);
MULTIPLE_ITEM_DROP = config.getBoolean("MultipleItemDrop", true);
HTM_CACHE = config.getBoolean("HtmCache", true);
CHECK_HTML_ENCODING = config.getBoolean("CheckHtmlEncoding", true);
MIN_NPC_ANIMATION = config.getInt("MinNpcAnimation", 5);
MAX_NPC_ANIMATION = config.getInt("MaxNpcAnimation", 60);
MIN_MONSTER_ANIMATION = config.getInt("MinMonsterAnimation", 5);
MAX_MONSTER_ANIMATION = config.getInt("MaxMonsterAnimation", 60);
GRIDS_ALWAYS_ON = config.getBoolean("GridsAlwaysOn", false);
GRID_NEIGHBOR_TURNON_TIME = config.getInt("GridNeighborTurnOnTime", 1);
GRID_NEIGHBOR_TURNOFF_TIME = config.getInt("GridNeighborTurnOffTime", 90);
PEACE_ZONE_MODE = config.getInt("PeaceZoneMode", 0);
DEFAULT_GLOBAL_CHAT = Enum.valueOf(ChatBroadcastType.class, config.getString("GlobalChat", "ON"));
DEFAULT_TRADE_CHAT = Enum.valueOf(ChatBroadcastType.class, config.getString("TradeChat", "ON"));
MINIMUM_CHAT_LEVEL = config.getInt("MinimumChatLevel", 20);
ALLOW_WAREHOUSE = config.getBoolean("AllowWarehouse", true);
ALLOW_REFUND = config.getBoolean("AllowRefund", true);
ALLOW_WEAR = config.getBoolean("AllowWear", true);
WEAR_DELAY = config.getInt("WearDelay", 5);
WEAR_PRICE = config.getInt("WearPrice", 10);
ALT_VILLAGES_REPEATABLE_QUEST_REWARD = config.getBoolean("AltVillagesRepQuestReward", false);
INSTANCE_FINISH_TIME = config.getInt("DefaultFinishTime", 300) * 1000;
RESTORE_PLAYER_INSTANCE = config.getBoolean("RestorePlayerInstance", false);
ALLOW_SUMMON_IN_INSTANCE = config.getBoolean("AllowSummonInInstance", false);
EJECT_DEAD_PLAYER_TIME = config.getInt("EjectDeadPlayerTime", 60) * 1000;
ALLOW_LOTTERY = config.getBoolean("AllowLottery", true);
ALLOW_RACE = config.getBoolean("AllowRace", true);
ALLOW_WATER = config.getBoolean("AllowWater", true);
ALLOW_FISHING = config.getBoolean("AllowFishing", true);
ALLOW_MANOR = config.getBoolean("AllowManor", true);
ALLOW_BOAT = config.getBoolean("AllowBoat", true);
BOAT_BROADCAST_RADIUS = config.getInt("BoatBroadcastRadius", 20000);
ALLOW_CURSED_WEAPONS = config.getBoolean("AllowCursedWeapons", true);
ALLOW_PARTY_IN_SAME_EVENT = config.getBoolean("AllowPartyInSameEvent", true);
SERVER_NEWS = config.getBoolean("ShowServerNews", false);
ENABLE_COMMUNITY_BOARD = config.getBoolean("EnableCommunityBoard", true);
BBS_DEFAULT = config.getString("BBSDefault", "_bbshome");
USE_SAY_FILTER = config.getBoolean("UseChatFilter", false);
CHAT_FILTER_CHARS = config.getString("ChatFilterChars", "^_^");
final String[] propertySplit4 = config.getString("BanChatChannels", "GENERAL;SHOUT;WORLD;TRADE;HERO_VOICE").trim().split(";");
BAN_CHAT_CHANNELS = new HashSet<>();
try
{
for (String chatId : propertySplit4)
{
BAN_CHAT_CHANNELS.add(Enum.valueOf(ChatType.class, chatId));
}
}
catch (NumberFormatException nfe)
{
LOGGER.log(Level.WARNING, "There was an error while parsing ban chat channels: ", nfe);
}
ALT_MANOR_REFRESH_TIME = config.getInt("AltManorRefreshTime", 20);
ALT_MANOR_REFRESH_MIN = config.getInt("AltManorRefreshMin", 0);
ALT_MANOR_APPROVE_TIME = config.getInt("AltManorApproveTime", 4);
ALT_MANOR_APPROVE_MIN = config.getInt("AltManorApproveMin", 30);
ALT_MANOR_MAINTENANCE_MIN = config.getInt("AltManorMaintenanceMin", 6);
ALT_MANOR_SAVE_ALL_ACTIONS = config.getBoolean("AltManorSaveAllActions", false);
ALT_MANOR_SAVE_PERIOD_RATE = config.getInt("AltManorSavePeriodRate", 2);
ALT_LOTTERY_PRIZE = config.getInt("AltLotteryPrize", 50000);
ALT_LOTTERY_TICKET_PRICE = config.getInt("AltLotteryTicketPrice", 2000);
ALT_LOTTERY_5_NUMBER_RATE = config.getFloat("AltLottery5NumberRate", 0.6f);
ALT_LOTTERY_4_NUMBER_RATE = config.getFloat("AltLottery4NumberRate", 0.2f);
ALT_LOTTERY_3_NUMBER_RATE = config.getFloat("AltLottery3NumberRate", 0.2f);
ALT_LOTTERY_2_AND_1_NUMBER_PRIZE = config.getInt("AltLottery2and1NumberPrize", 200);
ALT_FISH_CHAMPIONSHIP_ENABLED = config.getBoolean("AltFishChampionshipEnabled", true);
ALT_FISH_CHAMPIONSHIP_REWARD_ITEM = config.getInt("AltFishChampionshipRewardItemId", 57);
ALT_FISH_CHAMPIONSHIP_REWARD_1 = config.getInt("AltFishChampionshipReward1", 800000);
ALT_FISH_CHAMPIONSHIP_REWARD_2 = config.getInt("AltFishChampionshipReward2", 500000);
ALT_FISH_CHAMPIONSHIP_REWARD_3 = config.getInt("AltFishChampionshipReward3", 300000);
ALT_FISH_CHAMPIONSHIP_REWARD_4 = config.getInt("AltFishChampionshipReward4", 200000);
ALT_FISH_CHAMPIONSHIP_REWARD_5 = config.getInt("AltFishChampionshipReward5", 100000);
ALT_ITEM_AUCTION_ENABLED = config.getBoolean("AltItemAuctionEnabled", true);
ALT_ITEM_AUCTION_EXPIRED_AFTER = config.getInt("AltItemAuctionExpiredAfter", 14);
ALT_ITEM_AUCTION_TIME_EXTENDS_ON_BID = config.getInt("AltItemAuctionTimeExtendsOnBid", 0) * 1000;
RIFT_MIN_PARTY_SIZE = config.getInt("RiftMinPartySize", 5);
RIFT_MAX_JUMPS = config.getInt("MaxRiftJumps", 4);
RIFT_SPAWN_DELAY = config.getInt("RiftSpawnDelay", 10000);
RIFT_AUTO_JUMPS_TIME_MIN = config.getInt("AutoJumpsDelayMin", 480);
RIFT_AUTO_JUMPS_TIME_MAX = config.getInt("AutoJumpsDelayMax", 600);
RIFT_BOSS_ROOM_TIME_MUTIPLY = config.getFloat("BossRoomTimeMultiply", 1.5f);
RIFT_ENTER_COST_RECRUIT = config.getInt("RecruitCost", 18);
RIFT_ENTER_COST_SOLDIER = config.getInt("SoldierCost", 21);
RIFT_ENTER_COST_OFFICER = config.getInt("OfficerCost", 24);
RIFT_ENTER_COST_CAPTAIN = config.getInt("CaptainCost", 27);
RIFT_ENTER_COST_COMMANDER = config.getInt("CommanderCost", 30);
RIFT_ENTER_COST_HERO = config.getInt("HeroCost", 33);
DEFAULT_PUNISH = IllegalActionPunishmentType.findByName(config.getString("DefaultPunish", "KICK"));
DEFAULT_PUNISH_PARAM = config.getLong("DefaultPunishParam", 0);
if (DEFAULT_PUNISH_PARAM == 0)
{
DEFAULT_PUNISH_PARAM = 3155695200L; // One hundred years in seconds.
}
ONLY_GM_ITEMS_FREE = config.getBoolean("OnlyGMItemsFree", true);
JAIL_IS_PVP = config.getBoolean("JailIsPvp", false);
JAIL_DISABLE_CHAT = config.getBoolean("JailDisableChat", true);
JAIL_DISABLE_TRANSACTION = config.getBoolean("JailDisableTransaction", false);
CUSTOM_NPC_DATA = config.getBoolean("CustomNpcData", false);
CUSTOM_TELEPORT_TABLE = config.getBoolean("CustomTeleportTable", false);
CUSTOM_SKILLS_LOAD = config.getBoolean("CustomSkillsLoad", false);
CUSTOM_ITEMS_LOAD = config.getBoolean("CustomItemsLoad", false);
CUSTOM_MULTISELL_LOAD = config.getBoolean("CustomMultisellLoad", false);
CUSTOM_BUYLIST_LOAD = config.getBoolean("CustomBuyListLoad", false);
NORMAL_ENCHANT_COST_MULTIPLIER = config.getInt("NormalEnchantCostMultipiler", 1);
SAFE_ENCHANT_COST_MULTIPLIER = config.getInt("SafeEnchantCostMultipiler", 5);
CORRECT_PRICES = config.getBoolean("CorrectPrices", true);
MULTISELL_AMOUNT_LIMIT = config.getLong("MultisellAmountLimit", 10000);
ENABLE_FALLING_DAMAGE = config.getBoolean("EnableFallingDamage", true);
DEBUFF_DURATION_USES_RESISTS = config.getBoolean("DebuffDurationUsesResists", false);
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the geo engine related configurations.
* @author Mobius
*/
public class GeoEngineConfig
{
// File
private static final String GEOENGINE_CONFIG_FILE = "./config/GeoEngine.ini";
// Constants
public static Path GEODATA_PATH;
public static Path PATHNODE_PATH;
public static Path GEOEDIT_PATH;
public static int PATHFINDING;
public static String PATHFIND_BUFFERS;
public static float LOW_WEIGHT;
public static float MEDIUM_WEIGHT;
public static float HIGH_WEIGHT;
public static boolean ADVANCED_DIAGONAL_STRATEGY;
public static boolean AVOID_OBSTRUCTED_PATH_NODES;
public static float DIAGONAL_WEIGHT;
public static int MAX_POSTFILTER_PASSES;
public static void load()
{
final ConfigReader config = new ConfigReader(GEOENGINE_CONFIG_FILE);
GEODATA_PATH = Paths.get(ServerConfig.DATAPACK_ROOT.getPath() + "/" + config.getString("GeoDataPath", "geodata"));
PATHNODE_PATH = Paths.get(ServerConfig.DATAPACK_ROOT.getPath() + "/" + config.getString("PathnodePath", "pathnode"));
GEOEDIT_PATH = Paths.get(ServerConfig.DATAPACK_ROOT.getPath() + "/" + config.getString("GeoEditPath", "saves"));
PATHFINDING = config.getInt("PathFinding", 0);
PATHFIND_BUFFERS = config.getString("PathFindBuffers", "100x6;128x6;192x6;256x4;320x4;384x4;500x2");
LOW_WEIGHT = config.getFloat("LowWeight", 0.5f);
MEDIUM_WEIGHT = config.getFloat("MediumWeight", 2);
HIGH_WEIGHT = config.getFloat("HighWeight", 3);
ADVANCED_DIAGONAL_STRATEGY = config.getBoolean("AdvancedDiagonalStrategy", true);
AVOID_OBSTRUCTED_PATH_NODES = config.getBoolean("AvoidObstructedPathNodes", true);
DIAGONAL_WEIGHT = config.getFloat("DiagonalWeight", 0.707f);
MAX_POSTFILTER_PASSES = config.getInt("MaxPostfilterPasses", 3);
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the grand boss related configurations.
* @author Mobius
*/
public class GrandBossConfig
{
// File
private static final String GRANDBOSS_CONFIG_FILE = "./config/GrandBoss.ini";
// Constants
public static int ANTHARAS_WAIT_TIME;
public static int ANTHARAS_SPAWN_INTERVAL;
public static int ANTHARAS_SPAWN_RANDOM;
public static boolean ANTHARAS_RECOGNIZE_HERO;
public static int VALAKAS_WAIT_TIME;
public static int VALAKAS_SPAWN_INTERVAL;
public static int VALAKAS_SPAWN_RANDOM;
public static boolean VALAKAS_RECOGNIZE_HERO;
public static int BAIUM_SPAWN_INTERVAL;
public static int BAIUM_SPAWN_RANDOM;
public static boolean BAIUM_RECOGNIZE_HERO;
public static int CORE_SPAWN_INTERVAL;
public static int CORE_SPAWN_RANDOM;
public static int ORFEN_SPAWN_INTERVAL;
public static int ORFEN_SPAWN_RANDOM;
public static int QUEEN_ANT_SPAWN_INTERVAL;
public static int QUEEN_ANT_SPAWN_RANDOM;
public static int ZAKEN_SPAWN_INTERVAL;
public static int ZAKEN_SPAWN_RANDOM;
public static int FRINTEZZA_SPAWN_INTERVAL;
public static int FRINTEZZA_SPAWN_RANDOM;
public static boolean DISABLE_RAIDBOSS_HEAL_FROM_PLAYERS;
public static boolean DISABLE_RAIDBOSS_BUFF_FROM_PLAYERS;
public static void load()
{
final ConfigReader config = new ConfigReader(GRANDBOSS_CONFIG_FILE);
ANTHARAS_WAIT_TIME = config.getInt("AntharasWaitTime", 30);
ANTHARAS_SPAWN_INTERVAL = config.getInt("IntervalOfAntharasSpawn", 264);
ANTHARAS_SPAWN_RANDOM = config.getInt("RandomOfAntharasSpawn", 72);
ANTHARAS_RECOGNIZE_HERO = config.getBoolean("AntharasRecognizeHero", true);
VALAKAS_WAIT_TIME = config.getInt("ValakasWaitTime", 30);
VALAKAS_SPAWN_INTERVAL = config.getInt("IntervalOfValakasSpawn", 264);
VALAKAS_SPAWN_RANDOM = config.getInt("RandomOfValakasSpawn", 72);
VALAKAS_RECOGNIZE_HERO = config.getBoolean("ValakasRecognizeHero", true);
BAIUM_SPAWN_INTERVAL = config.getInt("IntervalOfBaiumSpawn", 168);
BAIUM_SPAWN_RANDOM = config.getInt("RandomOfBaiumSpawn", 48);
BAIUM_RECOGNIZE_HERO = config.getBoolean("BaiumRecognizeHero", true);
CORE_SPAWN_INTERVAL = config.getInt("IntervalOfCoreSpawn", 60);
CORE_SPAWN_RANDOM = config.getInt("RandomOfCoreSpawn", 24);
ORFEN_SPAWN_INTERVAL = config.getInt("IntervalOfOrfenSpawn", 48);
ORFEN_SPAWN_RANDOM = config.getInt("RandomOfOrfenSpawn", 20);
QUEEN_ANT_SPAWN_INTERVAL = config.getInt("IntervalOfQueenAntSpawn", 36);
QUEEN_ANT_SPAWN_RANDOM = config.getInt("RandomOfQueenAntSpawn", 17);
ZAKEN_SPAWN_INTERVAL = config.getInt("IntervalOfZakenSpawn", 36);
ZAKEN_SPAWN_RANDOM = config.getInt("RandomOfZakenSpawn", 17);
FRINTEZZA_SPAWN_INTERVAL = config.getInt("IntervalOfFrintezzaSpawn", 48);
FRINTEZZA_SPAWN_RANDOM = config.getInt("RandomOfFrintezzaSpawn", 8);
DISABLE_RAIDBOSS_HEAL_FROM_PLAYERS = config.getBoolean("DisableRaidBossHealFromPlayers", false);
DISABLE_RAIDBOSS_BUFF_FROM_PLAYERS = config.getBoolean("DisableRaidBossBuffFromPlayers", false);
}
}
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the id manager related configurations.
* @author Mobius
*/
public class IdManagerConfig
{
// File
private static final String ID_MANAGER_CONFIG_FILE = "./config/IdManager.ini";
// Constants
public static boolean DATABASE_CLEAN_UP;
public static int FIRST_OBJECT_ID;
public static int LAST_OBJECT_ID;
public static int INITIAL_CAPACITY;
public static double RESIZE_THRESHOLD;
public static double RESIZE_MULTIPLIER;
public static void load()
{
final ConfigReader config = new ConfigReader(ID_MANAGER_CONFIG_FILE);
DATABASE_CLEAN_UP = config.getBoolean("DatabaseCleanUp", true);
FIRST_OBJECT_ID = config.getInt("FirstObjectId", 268435456);
LAST_OBJECT_ID = config.getInt("LastObjectId", 2147483647);
INITIAL_CAPACITY = Math.min(config.getInt("InitialCapacity", 100000), LAST_OBJECT_ID - 1);
RESIZE_THRESHOLD = config.getDouble("ResizeThreshold", 0.9);
RESIZE_MULTIPLIER = config.getDouble("ResizeMultiplier", 1.1);
}
}
@@ -0,0 +1,151 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import org.l2jmobius.commons.util.ConfigReader;
import org.l2jmobius.commons.util.StringUtil;
/**
* This class loads all the NPC related configurations.
* @author Mobius
*/
public class NpcConfig
{
private static final Logger LOGGER = Logger.getLogger(NpcConfig.class.getName());
// File
private static final String NPC_CONFIG_FILE = "./config/NPC.ini";
// Constants
public static boolean ANNOUNCE_MAMMON_SPAWN;
public static boolean ALT_MOB_AGRO_IN_PEACEZONE;
public static boolean ALT_ATTACKABLE_NPCS;
public static boolean ALT_GAME_VIEWNPC;
public static boolean SHOW_NPC_LEVEL;
public static boolean SHOW_NPC_AGGRESSION;
public static boolean ATTACKABLES_CAMP_PLAYER_CORPSES;
public static boolean SHOW_CREST_WITHOUT_QUEST;
public static boolean ENABLE_RANDOM_ENCHANT_EFFECT;
public static int DECAY_TIME_TASK;
public static int DEFAULT_CORPSE_TIME;
public static int SPOILED_CORPSE_EXTEND_TIME;
public static int CORPSE_CONSUME_SKILL_ALLOWED_TIME_BEFORE_DECAY;
public static int MAX_AGGRO_RANGE;
public static int MAX_DRIFT_RANGE;
public static boolean AGGRO_DISTANCE_CHECK_ENABLED;
public static int AGGRO_DISTANCE_CHECK_RANGE;
public static boolean AGGRO_DISTANCE_CHECK_RAIDS;
public static int AGGRO_DISTANCE_CHECK_RAID_RANGE;
public static boolean AGGRO_DISTANCE_CHECK_INSTANCES;
public static boolean AGGRO_DISTANCE_CHECK_RESTORE_LIFE;
public static boolean GUARD_ATTACK_AGGRO_MOB;
public static boolean ALLOW_WYVERN_UPGRADER;
public static double RAID_HP_REGEN_MULTIPLIER;
public static double RAID_MP_REGEN_MULTIPLIER;
public static double RAID_PDEFENCE_MULTIPLIER;
public static double RAID_MDEFENCE_MULTIPLIER;
public static double RAID_PATTACK_MULTIPLIER;
public static double RAID_MATTACK_MULTIPLIER;
public static float RAID_MIN_RESPAWN_MULTIPLIER;
public static float RAID_MAX_RESPAWN_MULTIPLIER;
public static double RAID_MINION_RESPAWN_TIMER;
public static Map<Integer, Integer> MINIONS_RESPAWN_TIME;
public static boolean FORCE_DELETE_MINIONS;
public static boolean RAID_DISABLE_CURSE;
public static int RAID_CHAOS_TIME;
public static int GRAND_CHAOS_TIME;
public static int MINION_CHAOS_TIME;
public static int INVENTORY_MAXIMUM_PET;
public static double PET_HP_REGEN_MULTIPLIER;
public static double PET_MP_REGEN_MULTIPLIER;
public static void load()
{
final ConfigReader config = new ConfigReader(NPC_CONFIG_FILE);
ANNOUNCE_MAMMON_SPAWN = config.getBoolean("AnnounceMammonSpawn", false);
ALT_MOB_AGRO_IN_PEACEZONE = config.getBoolean("AltMobAgroInPeaceZone", true);
ALT_ATTACKABLE_NPCS = config.getBoolean("AltAttackableNpcs", true);
ALT_GAME_VIEWNPC = config.getBoolean("AltGameViewNpc", false);
SHOW_NPC_LEVEL = config.getBoolean("ShowNpcLevel", false);
SHOW_NPC_AGGRESSION = config.getBoolean("ShowNpcAggression", false);
ATTACKABLES_CAMP_PLAYER_CORPSES = config.getBoolean("AttackablesCampPlayerCorpses", false);
SHOW_CREST_WITHOUT_QUEST = config.getBoolean("ShowCrestWithoutQuest", false);
ENABLE_RANDOM_ENCHANT_EFFECT = config.getBoolean("EnableRandomEnchantEffect", false);
DECAY_TIME_TASK = config.getInt("DecayTimeTask", 5000);
DEFAULT_CORPSE_TIME = config.getInt("DefaultCorpseTime", 7);
SPOILED_CORPSE_EXTEND_TIME = config.getInt("SpoiledCorpseExtendTime", 10);
CORPSE_CONSUME_SKILL_ALLOWED_TIME_BEFORE_DECAY = config.getInt("CorpseConsumeSkillAllowedTimeBeforeDecay", 2000);
MAX_AGGRO_RANGE = config.getInt("MaxAggroRange", 450);
MAX_DRIFT_RANGE = config.getInt("MaxDriftRange", 300);
AGGRO_DISTANCE_CHECK_ENABLED = config.getBoolean("AggroDistanceCheckEnabled", true);
AGGRO_DISTANCE_CHECK_RANGE = config.getInt("AggroDistanceCheckRange", 1500);
AGGRO_DISTANCE_CHECK_RAIDS = config.getBoolean("AggroDistanceCheckRaids", false);
AGGRO_DISTANCE_CHECK_RAID_RANGE = config.getInt("AggroDistanceCheckRaidRange", 3000);
AGGRO_DISTANCE_CHECK_INSTANCES = config.getBoolean("AggroDistanceCheckInstances", false);
AGGRO_DISTANCE_CHECK_RESTORE_LIFE = config.getBoolean("AggroDistanceCheckRestoreLife", true);
GUARD_ATTACK_AGGRO_MOB = config.getBoolean("GuardAttackAggroMob", false);
ALLOW_WYVERN_UPGRADER = config.getBoolean("AllowWyvernUpgrader", false);
RAID_HP_REGEN_MULTIPLIER = config.getDouble("RaidHpRegenMultiplier", 100) / 100;
RAID_MP_REGEN_MULTIPLIER = config.getDouble("RaidMpRegenMultiplier", 100) / 100;
RAID_PDEFENCE_MULTIPLIER = config.getDouble("RaidPDefenceMultiplier", 100) / 100;
RAID_MDEFENCE_MULTIPLIER = config.getDouble("RaidMDefenceMultiplier", 100) / 100;
RAID_PATTACK_MULTIPLIER = config.getDouble("RaidPAttackMultiplier", 100) / 100;
RAID_MATTACK_MULTIPLIER = config.getDouble("RaidMAttackMultiplier", 100) / 100;
RAID_MIN_RESPAWN_MULTIPLIER = config.getFloat("RaidMinRespawnMultiplier", 1.0f);
RAID_MAX_RESPAWN_MULTIPLIER = config.getFloat("RaidMaxRespawnMultiplier", 1.0f);
RAID_MINION_RESPAWN_TIMER = config.getInt("RaidMinionRespawnTime", 300000);
final String[] split = config.getString("CustomMinionsRespawnTime", "").split(";");
MINIONS_RESPAWN_TIME = new HashMap<>(split.length);
for (String prop : split)
{
final String[] propSplit = prop.split(",");
if (propSplit.length != 2)
{
LOGGER.warning(StringUtil.concat("[CustomMinionsRespawnTime]: invalid config property -> CustomMinionsRespawnTime \"", prop, "\""));
}
try
{
MINIONS_RESPAWN_TIME.put(Integer.parseInt(propSplit[0]), Integer.parseInt(propSplit[1]));
}
catch (NumberFormatException nfe)
{
if (!prop.isEmpty())
{
LOGGER.warning(StringUtil.concat("[CustomMinionsRespawnTime]: invalid config property -> CustomMinionsRespawnTime \"", propSplit[0], "\"", propSplit[1]));
}
}
}
FORCE_DELETE_MINIONS = config.getBoolean("ForceDeleteMinions", false);
RAID_DISABLE_CURSE = config.getBoolean("DisableRaidCurse", false);
RAID_CHAOS_TIME = config.getInt("RaidChaosTime", 10);
GRAND_CHAOS_TIME = config.getInt("GrandChaosTime", 10);
MINION_CHAOS_TIME = config.getInt("MinionChaosTime", 10);
INVENTORY_MAXIMUM_PET = config.getInt("MaximumSlotsForPet", 12);
PET_HP_REGEN_MULTIPLIER = config.getDouble("PetHpRegenMultiplier", 100) / 100;
PET_MP_REGEN_MULTIPLIER = config.getDouble("PetMpRegenMultiplier", 100) / 100;
}
}
@@ -0,0 +1,126 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the olympiad related configurations.
* @author Mobius
*/
public class OlympiadConfig
{
// File
private static final String OLYMPIAD_CONFIG_FILE = "./config/Olympiad.ini";
// Constants
public static boolean OLYMPIAD_ENABLED;
public static int OLYMPIAD_START_TIME;
public static int OLYMPIAD_MIN;
public static long OLYMPIAD_CPERIOD;
public static long OLYMPIAD_BATTLE;
public static long OLYMPIAD_WPERIOD;
public static long OLYMPIAD_VPERIOD;
public static int OLYMPIAD_START_POINTS;
public static int OLYMPIAD_WEEKLY_POINTS;
public static int OLYMPIAD_CLASSED;
public static int OLYMPIAD_NONCLASSED;
public static int OLYMPIAD_REG_DISPLAY;
public static int OLYMPIAD_BATTLE_REWARD_ITEM;
public static int OLYMPIAD_CLASSED_RITEM_C;
public static int OLYMPIAD_NONCLASSED_RITEM_C;
public static int OLYMPIAD_COMP_RITEM;
public static int OLYMPIAD_GP_PER_POINT;
public static int OLYMPIAD_HERO_POINTS;
public static int OLYMPIAD_MAX_POINTS;
public static boolean OLYMPIAD_LOG_FIGHTS;
public static boolean OLYMPIAD_SHOW_MONTHLY_WINNERS;
public static boolean OLYMPIAD_ANNOUNCE_GAMES;
public static Set<Integer> LIST_OLY_RESTRICTED_ITEMS = new HashSet<>();
public static boolean OLYMPIAD_DISABLE_BLESSED_SPIRITSHOTS;
public static int OLYMPIAD_ENCHANT_LIMIT;
public static int OLYMPIAD_WAIT_TIME;
public static boolean OLYMPIAD_USE_CUSTOM_PERIOD_SETTINGS;
public static String OLYMPIAD_PERIOD;
public static int OLYMPIAD_PERIOD_MULTIPLIER;
public static List<Integer> OLYMPIAD_COMPETITION_DAYS;
public static void load()
{
final ConfigReader config = new ConfigReader(OLYMPIAD_CONFIG_FILE);
OLYMPIAD_ENABLED = config.getBoolean("OlympiadEnabled", true);
OLYMPIAD_START_TIME = config.getInt("OlympiadStartTime", 18);
OLYMPIAD_MIN = config.getInt("OlympiadMin", 0);
OLYMPIAD_CPERIOD = config.getLong("OlympiadCPeriod", 21600000);
OLYMPIAD_BATTLE = config.getLong("OlympiadBattle", 360000);
OLYMPIAD_WPERIOD = config.getLong("OlympiadWPeriod", 604800000);
OLYMPIAD_VPERIOD = config.getLong("OlympiadVPeriod", 86400000);
OLYMPIAD_START_POINTS = config.getInt("OlympiadStartPoints", 18);
OLYMPIAD_WEEKLY_POINTS = config.getInt("OlympiadWeeklyPoints", 3);
OLYMPIAD_CLASSED = config.getInt("OlympiadClassedParticipants", 5);
OLYMPIAD_NONCLASSED = config.getInt("OlympiadNonClassedParticipants", 9);
OLYMPIAD_REG_DISPLAY = config.getInt("OlympiadRegistrationDisplayNumber", 0);
OLYMPIAD_BATTLE_REWARD_ITEM = config.getInt("OlympiadBattleRewItem", 6651);
OLYMPIAD_CLASSED_RITEM_C = config.getInt("OlympiadClassedRewItemCount", 50);
OLYMPIAD_NONCLASSED_RITEM_C = config.getInt("OlympiadNonClassedRewItemCount", 30);
OLYMPIAD_COMP_RITEM = config.getInt("OlympiadCompRewItem", 6651);
OLYMPIAD_GP_PER_POINT = config.getInt("OlympiadGPPerPoint", 1000);
OLYMPIAD_HERO_POINTS = config.getInt("OlympiadHeroPoints", 100);
OLYMPIAD_MAX_POINTS = config.getInt("OlympiadMaxPoints", 10);
OLYMPIAD_LOG_FIGHTS = config.getBoolean("OlympiadLogFights", false);
OLYMPIAD_SHOW_MONTHLY_WINNERS = config.getBoolean("OlympiadShowMonthlyWinners", true);
OLYMPIAD_ANNOUNCE_GAMES = config.getBoolean("OlympiadAnnounceGames", true);
final String olyRestrictedItems = config.getString("OlympiadRestrictedItems", "").trim();
if (!olyRestrictedItems.isEmpty())
{
final String[] olyRestrictedItemsSplit = olyRestrictedItems.split(",");
LIST_OLY_RESTRICTED_ITEMS = new HashSet<>(olyRestrictedItemsSplit.length);
for (String id : olyRestrictedItemsSplit)
{
LIST_OLY_RESTRICTED_ITEMS.add(Integer.parseInt(id));
}
}
else // In case of reload with removal of all items ids.
{
LIST_OLY_RESTRICTED_ITEMS.clear();
}
OLYMPIAD_DISABLE_BLESSED_SPIRITSHOTS = config.getBoolean("OlympiadDisableBlessedSpiritShots", true);
OLYMPIAD_ENCHANT_LIMIT = config.getInt("OlympiadEnchantLimit", -1);
OLYMPIAD_WAIT_TIME = config.getInt("OlympiadWaitTime", 120);
if ((OLYMPIAD_WAIT_TIME != 120) && (OLYMPIAD_WAIT_TIME != 60) && (OLYMPIAD_WAIT_TIME != 30) && (OLYMPIAD_WAIT_TIME != 15) && (OLYMPIAD_WAIT_TIME != 5))
{
OLYMPIAD_WAIT_TIME = 120;
}
OLYMPIAD_USE_CUSTOM_PERIOD_SETTINGS = config.getBoolean("OlympiadUseCustomPeriodSettings", false);
OLYMPIAD_PERIOD = config.getString("OlympiadPeriod", "MONTH");
OLYMPIAD_PERIOD_MULTIPLIER = config.getInt("OlympiadPeriodMultiplier", 1);
OLYMPIAD_COMPETITION_DAYS = new ArrayList<>();
for (String s : config.getString("OlympiadCompetitionDays", "1,2,3,4,5,6,7").split(","))
{
OLYMPIAD_COMPETITION_DAYS.add(Integer.parseInt(s));
}
}
}
@@ -0,0 +1,541 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.l2jmobius.commons.util.ConfigReader;
import org.l2jmobius.commons.util.StringUtil;
import org.l2jmobius.gameserver.entity.actor.enums.player.IllegalActionPunishmentType;
import org.l2jmobius.gameserver.entity.groups.PartyExpType;
/**
* This class loads all the player related configurations.
* @author Mobius
*/
public class PlayerConfig
{
private static final Logger LOGGER = Logger.getLogger(PlayerConfig.class.getName());
// File
private static final String PLAYER_CONFIG_FILE = "./config/Player.ini";
// Constants
public static boolean PLAYER_DELEVEL;
public static boolean DECREASE_SKILL_LEVEL;
public static double ALT_WEIGHT_LIMIT;
public static int RUN_SPD_BOOST;
public static int DEATH_PENALTY_CHANCE;
public static double RESPAWN_RESTORE_CP;
public static double RESPAWN_RESTORE_HP;
public static double RESPAWN_RESTORE_MP;
public static double HP_REGEN_MULTIPLIER;
public static double MP_REGEN_MULTIPLIER;
public static double CP_REGEN_MULTIPLIER;
public static boolean ENABLE_MODIFY_SKILL_DURATION;
public static Map<Integer, Integer> SKILL_DURATION_LIST;
public static boolean ENABLE_MODIFY_SKILL_REUSE;
public static Map<Integer, Integer> SKILL_REUSE_LIST;
public static boolean AUTO_LEARN_SKILLS;
public static boolean AUTO_LEARN_SKILLS_WITHOUT_ITEMS;
public static boolean AUTO_LEARN_FS_SKILLS;
public static boolean SHOW_EFFECT_MESSAGES_ON_LOGIN;
public static boolean AUTO_LOOT_HERBS;
public static byte BUFFS_MAX_AMOUNT;
public static byte DANCES_MAX_AMOUNT;
public static boolean DANCE_CANCEL_BUFF;
public static boolean DANCE_CONSUME_ADDITIONAL_MP;
public static boolean ALT_STORE_DANCES;
public static boolean ALT_STORE_TOGGLES;
public static boolean AUTO_LEARN_DIVINE_INSPIRATION;
public static boolean ALT_GAME_CANCEL_BOW;
public static boolean ALT_GAME_CANCEL_CAST;
public static boolean ALT_GAME_MAGICFAILURES;
public static int PLAYER_FAKEDEATH_UP_PROTECTION;
public static boolean STORE_SKILL_COOLTIME;
public static boolean SUBCLASS_STORE_SKILL_COOLTIME;
public static boolean SUMMON_STORE_SKILL_COOLTIME;
public static boolean ALT_GAME_SHIELD_BLOCKS;
public static int ALT_PERFECT_SHLD_BLOCK;
public static long EFFECT_TICK_RATIO;
public static boolean FAKE_DEATH_UNTARGET;
public static boolean FAKE_DEATH_DAMAGE_STAND;
public static boolean CALCULATE_MAGIC_SUCCESS_BY_SKILL_MAGIC_LEVEL;
public static boolean CALCULATE_DISTANCE_BOW_DAMAGE;
public static boolean LIFE_CRYSTAL_NEEDED;
public static boolean ES_SP_BOOK_NEEDED;
public static boolean DIVINE_SP_BOOK_NEEDED;
public static boolean ALT_GAME_SKILL_LEARN;
public static boolean ALT_GAME_SUBCLASS_WITHOUT_QUESTS;
public static boolean ALT_GAME_SUBCLASS_EVERYWHERE;
public static boolean RESTORE_SERVITOR_ON_RECONNECT;
public static boolean RESTORE_PET_ON_RECONNECT;
public static int FEE_DELETE_TRANSFER_SKILLS;
public static boolean ENABLE_VITALITY;
public static boolean RECOVER_VITALITY_ON_RECONNECT;
public static int STARTING_VITALITY_POINTS;
public static boolean RAIDBOSS_USE_VITALITY;
public static double MAX_BONUS_EXP;
public static double MAX_BONUS_SP;
public static int MAX_RUN_SPEED;
public static int MAX_PATK;
public static int MAX_MATK;
public static int MAX_PCRIT_RATE;
public static int MAX_MCRIT_RATE;
public static int MAX_PATK_SPEED;
public static int MAX_MATK_SPEED;
public static int MAX_EVASION;
public static int MIN_ABNORMAL_STATE_SUCCESS_RATE;
public static int MAX_ABNORMAL_STATE_SUCCESS_RATE;
public static long MAX_SP;
public static byte PLAYER_MAXIMUM_LEVEL;
public static byte MAX_SUBCLASS;
public static byte BASE_SUBCLASS_LEVEL;
public static byte MAX_SUBCLASS_LEVEL;
public static int MAX_PVTSTORESELL_SLOTS_DWARF;
public static int MAX_PVTSTORESELL_SLOTS_OTHER;
public static int MAX_PVTSTOREBUY_SLOTS_DWARF;
public static int MAX_PVTSTOREBUY_SLOTS_OTHER;
public static int INVENTORY_MAXIMUM_NO_DWARF;
public static int INVENTORY_MAXIMUM_DWARF;
public static int INVENTORY_MAXIMUM_GM;
public static int MAX_ITEM_IN_PACKET;
public static int WAREHOUSE_SLOTS_DWARF;
public static int WAREHOUSE_SLOTS_NO_DWARF;
public static int WAREHOUSE_SLOTS_CLAN;
public static int ALT_FREIGHT_SLOTS;
public static int ALT_FREIGHT_PRICE;
public static int[] ENCHANT_BLACKLIST;
public static boolean DISABLE_OVER_ENCHANTING;
public static boolean OVER_ENCHANT_PROTECTION;
public static IllegalActionPunishmentType OVER_ENCHANT_PUNISHMENT;
public static int AUGMENTATION_NG_SKILL_CHANCE;
public static int AUGMENTATION_NG_GLOW_CHANCE;
public static int AUGMENTATION_MID_SKILL_CHANCE;
public static int AUGMENTATION_MID_GLOW_CHANCE;
public static int AUGMENTATION_HIGH_SKILL_CHANCE;
public static int AUGMENTATION_HIGH_GLOW_CHANCE;
public static int AUGMENTATION_TOP_SKILL_CHANCE;
public static int AUGMENTATION_TOP_GLOW_CHANCE;
public static int AUGMENTATION_BASESTAT_CHANCE;
public static boolean RETAIL_LIKE_AUGMENTATION;
public static int[] RETAIL_LIKE_AUGMENTATION_NG_CHANCE;
public static int[] RETAIL_LIKE_AUGMENTATION_MID_CHANCE;
public static int[] RETAIL_LIKE_AUGMENTATION_HIGH_CHANCE;
public static int[] RETAIL_LIKE_AUGMENTATION_TOP_CHANCE;
public static int[] AUGMENTATION_BLACKLIST;
public static boolean ALT_ALLOW_AUGMENT_PVP_ITEMS;
public static boolean ALT_ALLOW_AUGMENT_TRADE;
public static boolean ALT_ALLOW_AUGMENT_DESTROY;
public static double SOUL_CRYSTAL_CHANCE_MULTIPLIER;
public static boolean ALT_GAME_KARMA_PLAYER_CAN_BE_KILLED_IN_PEACEZONE;
public static boolean ALT_GAME_KARMA_PLAYER_CAN_SHOP;
public static boolean ALT_GAME_KARMA_PLAYER_CAN_TELEPORT;
public static boolean ALT_GAME_KARMA_PLAYER_CAN_USE_GK;
public static boolean ALT_GAME_KARMA_PLAYER_CAN_TRADE;
public static boolean ALT_GAME_KARMA_PLAYER_CAN_USE_WAREHOUSE;
public static boolean FAME_SYSTEM_ENABLED;
public static int MAX_PERSONAL_FAME_POINTS;
public static int FORTRESS_ZONE_FAME_TASK_FREQUENCY;
public static int FORTRESS_ZONE_FAME_AQUIRE_POINTS;
public static int CASTLE_ZONE_FAME_TASK_FREQUENCY;
public static int CASTLE_ZONE_FAME_AQUIRE_POINTS;
public static boolean FAME_FOR_DEAD_PLAYERS;
public static boolean IS_CRAFTING_ENABLED;
public static int DWARF_RECIPE_LIMIT;
public static int COMMON_RECIPE_LIMIT;
public static boolean ALT_GAME_CREATION;
public static double ALT_GAME_CREATION_SPEED;
public static double ALT_GAME_CREATION_XP_RATE;
public static double ALT_GAME_CREATION_SP_RATE;
public static boolean ALT_BLACKSMITH_USE_RECIPES;
public static boolean ALT_CLAN_LEADER_INSTANT_ACTIVATION;
public static int ALT_CLAN_JOIN_DAYS;
public static int ALT_CLAN_CREATE_DAYS;
public static int ALT_CLAN_DISSOLVE_DAYS;
public static int ALT_ALLY_JOIN_DAYS_WHEN_LEAVED;
public static int ALT_ALLY_JOIN_DAYS_WHEN_DISMISSED;
public static int ALT_ACCEPT_CLAN_DAYS_WHEN_DISMISSED;
public static int ALT_CREATE_ALLY_DAYS_WHEN_DISSOLVED;
public static int ALT_MAX_NUM_OF_CLANS_IN_ALLY;
public static int ALT_CLAN_MEMBERS_FOR_WAR;
public static boolean ALT_GAME_NEW_CHAR_ALWAYS_IS_NEWBIE;
public static boolean ALT_MEMBERS_CAN_WITHDRAW_FROM_CLANWH;
public static boolean REMOVE_CASTLE_CIRCLETS;
public static int ALT_PARTY_RANGE;
public static boolean ALT_LEAVE_PARTY_LEADER;
public static int STARTING_ADENA;
public static byte STARTING_LEVEL;
public static int STARTING_SP;
public static int MAX_ADENA;
public static boolean AUTO_LOOT;
public static boolean AUTO_LOOT_RAIDS;
public static boolean AUTO_LOOT_SLOT_LIMIT;
public static int LOOT_RAIDS_PRIVILEGE_INTERVAL;
public static int LOOT_RAIDS_PRIVILEGE_CC_SIZE;
public static Set<Integer> AUTO_LOOT_ITEM_IDS;
public static boolean ENABLE_KEYBOARD_MOVEMENT;
public static int UNSTUCK_INTERVAL;
public static int TELEPORT_WATCHDOG_TIMEOUT;
public static int PLAYER_SPAWN_PROTECTION;
public static int PLAYER_TELEPORT_PROTECTION;
public static boolean RANDOM_RESPAWN_IN_TOWN_ENABLED;
public static boolean OFFSET_ON_TELEPORT_ENABLED;
public static int MAX_OFFSET_ON_TELEPORT;
public static boolean TELEPORT_WHILE_SIEGE_IN_PROGRESS;
public static boolean PETITIONING_ALLOWED;
public static int MAX_PETITIONS_PER_PLAYER;
public static int MAX_PETITIONS_PENDING;
public static int MAX_FREE_TELEPORT_LEVEL;
public static boolean ALT_RECOMMEND;
public static int DELETE_DAYS;
public static boolean DISCONNECT_AFTER_DEATH;
public static PartyExpType PARTY_XP_CUTOFF_METHOD;
public static double PARTY_XP_CUTOFF_PERCENT;
public static int PARTY_XP_CUTOFF_LEVEL;
public static int[][] PARTY_XP_CUTOFF_GAPS;
public static int[] PARTY_XP_CUTOFF_GAP_PERCENTS;
public static boolean DISABLE_TUTORIAL;
public static boolean EXPERTISE_PENALTY;
public static boolean STORE_RECIPE_SHOPLIST;
public static String[] FORBIDDEN_NAMES;
public static boolean SILENCE_MODE_EXCLUDE;
public static boolean ALT_VALIDATE_TRIGGER_SKILLS;
public static int PLAYER_MOVEMENT_BLOCK_TIME;
public static boolean RANDOMIZE_AUTO_ATTACK_DAMAGE;
public static boolean RANDOMIZE_PHYSICAL_SKILL_DAMAGE;
public static boolean RANDOMIZE_MAGICAL_SKILL_DAMAGE;
public static void load()
{
final ConfigReader config = new ConfigReader(PLAYER_CONFIG_FILE);
PLAYER_DELEVEL = config.getBoolean("Delevel", true);
DECREASE_SKILL_LEVEL = config.getBoolean("DecreaseSkillOnDelevel", true);
ALT_WEIGHT_LIMIT = config.getDouble("AltWeightLimit", 1);
RUN_SPD_BOOST = config.getInt("RunSpeedBoost", 0);
DEATH_PENALTY_CHANCE = config.getInt("DeathPenaltyChance", 20);
RESPAWN_RESTORE_CP = config.getDouble("RespawnRestoreCP", 0) / 100;
RESPAWN_RESTORE_HP = config.getDouble("RespawnRestoreHP", 65) / 100;
RESPAWN_RESTORE_MP = config.getDouble("RespawnRestoreMP", 0) / 100;
HP_REGEN_MULTIPLIER = config.getDouble("HpRegenMultiplier", 100) / 100;
MP_REGEN_MULTIPLIER = config.getDouble("MpRegenMultiplier", 100) / 100;
CP_REGEN_MULTIPLIER = config.getDouble("CpRegenMultiplier", 100) / 100;
ENABLE_MODIFY_SKILL_DURATION = config.getBoolean("EnableModifySkillDuration", false);
if (ENABLE_MODIFY_SKILL_DURATION)
{
final String[] propertySplit = config.getString("SkillDurationList", "").split(";");
SKILL_DURATION_LIST = new HashMap<>(propertySplit.length);
for (String skill : propertySplit)
{
final String[] skillSplit = skill.split(",");
if (skillSplit.length != 2)
{
LOGGER.warning("[SkillDurationList]: invalid config property -> SkillDurationList " + skill);
}
else
{
try
{
SKILL_DURATION_LIST.put(Integer.parseInt(skillSplit[0]), Integer.parseInt(skillSplit[1]));
}
catch (NumberFormatException nfe)
{
if (!skill.isEmpty())
{
LOGGER.warning(StringUtil.concat("[SkillDurationList]: invalid config property -> SkillList \"", skillSplit[0], "\"", skillSplit[1]));
}
}
}
}
}
ENABLE_MODIFY_SKILL_REUSE = config.getBoolean("EnableModifySkillReuse", false);
if (ENABLE_MODIFY_SKILL_REUSE)
{
final String[] propertySplit = config.getString("SkillReuseList", "").split(";");
SKILL_REUSE_LIST = new HashMap<>(propertySplit.length);
for (String skill : propertySplit)
{
final String[] skillSplit = skill.split(",");
if (skillSplit.length != 2)
{
LOGGER.warning(StringUtil.concat("[SkillReuseList]: invalid config property -> SkillReuseList \"", skill, "\""));
}
else
{
try
{
SKILL_REUSE_LIST.put(Integer.parseInt(skillSplit[0]), Integer.parseInt(skillSplit[1]));
}
catch (NumberFormatException nfe)
{
if (!skill.isEmpty())
{
LOGGER.warning(StringUtil.concat("[SkillReuseList]: invalid config property -> SkillList \"", skillSplit[0], "\"", skillSplit[1]));
}
}
}
}
}
AUTO_LEARN_SKILLS = config.getBoolean("AutoLearnSkills", false);
AUTO_LEARN_SKILLS_WITHOUT_ITEMS = config.getBoolean("AutoLearnSkillsWithoutItems", false);
AUTO_LEARN_FS_SKILLS = config.getBoolean("AutoLearnForgottenScrollSkills", false);
SHOW_EFFECT_MESSAGES_ON_LOGIN = config.getBoolean("ShowEffectMessagesOnLogin", false);
AUTO_LOOT_HERBS = config.getBoolean("AutoLootHerbs", false);
BUFFS_MAX_AMOUNT = config.getByte("MaxBuffAmount", (byte) 20);
DANCES_MAX_AMOUNT = config.getByte("MaxDanceAmount", (byte) 12);
DANCE_CANCEL_BUFF = config.getBoolean("DanceCancelBuff", false);
DANCE_CONSUME_ADDITIONAL_MP = config.getBoolean("DanceConsumeAdditionalMP", true);
ALT_STORE_DANCES = config.getBoolean("AltStoreDances", false);
ALT_STORE_TOGGLES = config.getBoolean("AltStoreToggles", false);
AUTO_LEARN_DIVINE_INSPIRATION = config.getBoolean("AutoLearnDivineInspiration", false);
ALT_GAME_CANCEL_BOW = config.getString("AltGameCancelByHit", "Cast").equalsIgnoreCase("bow") || config.getString("AltGameCancelByHit", "Cast").equalsIgnoreCase("all");
ALT_GAME_CANCEL_CAST = config.getString("AltGameCancelByHit", "Cast").equalsIgnoreCase("cast") || config.getString("AltGameCancelByHit", "Cast").equalsIgnoreCase("all");
ALT_GAME_MAGICFAILURES = config.getBoolean("MagicFailures", true);
PLAYER_FAKEDEATH_UP_PROTECTION = config.getInt("PlayerFakeDeathUpProtection", 0);
STORE_SKILL_COOLTIME = config.getBoolean("StoreSkillCooltime", true);
SUBCLASS_STORE_SKILL_COOLTIME = config.getBoolean("SubclassStoreSkillCooltime", false);
SUMMON_STORE_SKILL_COOLTIME = config.getBoolean("SummonStoreSkillCooltime", true);
ALT_GAME_SHIELD_BLOCKS = config.getBoolean("AltShieldBlocks", false);
ALT_PERFECT_SHLD_BLOCK = config.getInt("AltPerfectShieldBlockRate", 10);
EFFECT_TICK_RATIO = config.getLong("EffectTickRatio", 666);
FAKE_DEATH_UNTARGET = config.getBoolean("FakeDeathUntarget", false);
FAKE_DEATH_DAMAGE_STAND = config.getBoolean("FakeDeathDamageStand", true);
CALCULATE_MAGIC_SUCCESS_BY_SKILL_MAGIC_LEVEL = config.getBoolean("CalculateMagicSuccessBySkillMagicLevel", true);
CALCULATE_DISTANCE_BOW_DAMAGE = config.getBoolean("DistanceBowDamage", false);
LIFE_CRYSTAL_NEEDED = config.getBoolean("LifeCrystalNeeded", true);
ES_SP_BOOK_NEEDED = config.getBoolean("EnchantSkillSpBookNeeded", true);
DIVINE_SP_BOOK_NEEDED = config.getBoolean("DivineInspirationSpBookNeeded", true);
ALT_GAME_SKILL_LEARN = config.getBoolean("AltGameSkillLearn", false);
ALT_GAME_SUBCLASS_WITHOUT_QUESTS = config.getBoolean("AltSubClassWithoutQuests", false);
ALT_GAME_SUBCLASS_EVERYWHERE = config.getBoolean("AltSubclassEverywhere", false);
RESTORE_SERVITOR_ON_RECONNECT = config.getBoolean("RestoreServitorOnReconnect", true);
RESTORE_PET_ON_RECONNECT = config.getBoolean("RestorePetOnReconnect", true);
FEE_DELETE_TRANSFER_SKILLS = config.getInt("FeeDeleteTransferSkills", 10000000);
ENABLE_VITALITY = config.getBoolean("EnableVitality", false);
RECOVER_VITALITY_ON_RECONNECT = config.getBoolean("RecoverVitalityOnReconnect", true);
STARTING_VITALITY_POINTS = config.getInt("StartingVitalityPoints", 20000);
RAIDBOSS_USE_VITALITY = config.getBoolean("RaidbossUseVitality", true);
MAX_BONUS_EXP = config.getDouble("MaxExpBonus", 3.5);
MAX_BONUS_SP = config.getDouble("MaxSpBonus", 3.5);
MAX_RUN_SPEED = config.getInt("MaxRunSpeed", 250);
MAX_PATK = config.getInt("MaxPAtk", 999999);
MAX_MATK = config.getInt("MaxMAtk", 999999);
MAX_PCRIT_RATE = config.getInt("MaxPCritRate", 500);
MAX_MCRIT_RATE = config.getInt("MaxMCritRate", 200);
MAX_PATK_SPEED = config.getInt("MaxPAtkSpeed", 1500);
MAX_MATK_SPEED = config.getInt("MaxMAtkSpeed", 1999);
MAX_EVASION = config.getInt("MaxEvasion", 250);
MIN_ABNORMAL_STATE_SUCCESS_RATE = config.getInt("MinAbnormalStateSuccessRate", 10);
MAX_ABNORMAL_STATE_SUCCESS_RATE = config.getInt("MaxAbnormalStateSuccessRate", 90);
MAX_SP = config.getLong("MaxSp", 50000000000L) >= 0 ? config.getLong("MaxSp", 50000000000L) : Long.MAX_VALUE;
PLAYER_MAXIMUM_LEVEL = config.getByte("MaximumPlayerLevel", (byte) 80);
PLAYER_MAXIMUM_LEVEL++;
MAX_SUBCLASS = config.getByte("MaxSubclass", (byte) 3);
BASE_SUBCLASS_LEVEL = config.getByte("BaseSubclassLevel", (byte) 40);
MAX_SUBCLASS_LEVEL = config.getByte("MaxSubclassLevel", (byte) 80);
MAX_PVTSTORESELL_SLOTS_DWARF = config.getInt("MaxPvtStoreSellSlotsDwarf", 4);
MAX_PVTSTORESELL_SLOTS_OTHER = config.getInt("MaxPvtStoreSellSlotsOther", 3);
MAX_PVTSTOREBUY_SLOTS_DWARF = config.getInt("MaxPvtStoreBuySlotsDwarf", 5);
MAX_PVTSTOREBUY_SLOTS_OTHER = config.getInt("MaxPvtStoreBuySlotsOther", 4);
INVENTORY_MAXIMUM_NO_DWARF = config.getInt("MaximumSlotsForNoDwarf", 80);
INVENTORY_MAXIMUM_DWARF = config.getInt("MaximumSlotsForDwarf", 100);
INVENTORY_MAXIMUM_GM = config.getInt("MaximumSlotsForGMPlayer", 250);
MAX_ITEM_IN_PACKET = Math.max(INVENTORY_MAXIMUM_NO_DWARF, Math.max(INVENTORY_MAXIMUM_DWARF, INVENTORY_MAXIMUM_GM));
WAREHOUSE_SLOTS_DWARF = config.getInt("MaximumWarehouseSlotsForDwarf", 120);
WAREHOUSE_SLOTS_NO_DWARF = config.getInt("MaximumWarehouseSlotsForNoDwarf", 100);
WAREHOUSE_SLOTS_CLAN = config.getInt("MaximumWarehouseSlotsForClan", 150);
ALT_FREIGHT_SLOTS = config.getInt("MaximumFreightSlots", 200);
ALT_FREIGHT_PRICE = config.getInt("FreightPrice", 1000);
final String[] notenchantable = config.getString("EnchantBlackList", "7816,7817,7818,7819,7820,7821,7822,7823,7824,7825,7826,7827,7828,7829,7830,7831,13293,13294,13296").split(",");
ENCHANT_BLACKLIST = new int[notenchantable.length];
for (int i = 0; i < notenchantable.length; i++)
{
ENCHANT_BLACKLIST[i] = Integer.parseInt(notenchantable[i]);
}
Arrays.sort(ENCHANT_BLACKLIST);
DISABLE_OVER_ENCHANTING = config.getBoolean("DisableOverEnchanting", true);
OVER_ENCHANT_PROTECTION = config.getBoolean("OverEnchantProtection", true);
OVER_ENCHANT_PUNISHMENT = IllegalActionPunishmentType.findByName(config.getString("OverEnchantPunishment", "JAIL"));
AUGMENTATION_NG_SKILL_CHANCE = config.getInt("AugmentationNGSkillChance", 15);
AUGMENTATION_NG_GLOW_CHANCE = config.getInt("AugmentationNGGlowChance", 0);
AUGMENTATION_MID_SKILL_CHANCE = config.getInt("AugmentationMidSkillChance", 30);
AUGMENTATION_MID_GLOW_CHANCE = config.getInt("AugmentationMidGlowChance", 40);
AUGMENTATION_HIGH_SKILL_CHANCE = config.getInt("AugmentationHighSkillChance", 45);
AUGMENTATION_HIGH_GLOW_CHANCE = config.getInt("AugmentationHighGlowChance", 70);
AUGMENTATION_TOP_SKILL_CHANCE = config.getInt("AugmentationTopSkillChance", 60);
AUGMENTATION_TOP_GLOW_CHANCE = config.getInt("AugmentationTopGlowChance", 100);
AUGMENTATION_BASESTAT_CHANCE = config.getInt("AugmentationBaseStatChance", 1);
RETAIL_LIKE_AUGMENTATION = config.getBoolean("RetailLikeAugmentation", true);
String[] array = config.getString("RetailLikeAugmentationNoGradeChance", "55,35,7,3").split(",");
RETAIL_LIKE_AUGMENTATION_NG_CHANCE = new int[array.length];
for (int i = 0; i < 4; i++)
{
RETAIL_LIKE_AUGMENTATION_NG_CHANCE[i] = Integer.parseInt(array[i]);
}
array = config.getString("RetailLikeAugmentationMidGradeChance", "55,35,7,3").split(",");
RETAIL_LIKE_AUGMENTATION_MID_CHANCE = new int[array.length];
for (int i = 0; i < 4; i++)
{
RETAIL_LIKE_AUGMENTATION_MID_CHANCE[i] = Integer.parseInt(array[i]);
}
array = config.getString("RetailLikeAugmentationHighGradeChance", "55,35,7,3").split(",");
RETAIL_LIKE_AUGMENTATION_HIGH_CHANCE = new int[array.length];
for (int i = 0; i < 4; i++)
{
RETAIL_LIKE_AUGMENTATION_HIGH_CHANCE[i] = Integer.parseInt(array[i]);
}
array = config.getString("RetailLikeAugmentationTopGradeChance", "55,35,7,3").split(",");
RETAIL_LIKE_AUGMENTATION_TOP_CHANCE = new int[array.length];
for (int i = 0; i < 4; i++)
{
RETAIL_LIKE_AUGMENTATION_TOP_CHANCE[i] = Integer.parseInt(array[i]);
}
array = config.getString("AugmentationBlackList", "6656,6657,6658,6659,6660,6661,6662,8191").split(",");
AUGMENTATION_BLACKLIST = new int[array.length];
for (int i = 0; i < array.length; i++)
{
AUGMENTATION_BLACKLIST[i] = Integer.parseInt(array[i]);
}
Arrays.sort(AUGMENTATION_BLACKLIST);
ALT_ALLOW_AUGMENT_PVP_ITEMS = config.getBoolean("AltAllowAugmentPvPItems", false);
ALT_ALLOW_AUGMENT_TRADE = config.getBoolean("AltAllowAugmentTrade", false);
ALT_ALLOW_AUGMENT_DESTROY = config.getBoolean("AltAllowAugmentDestroy", true);
SOUL_CRYSTAL_CHANCE_MULTIPLIER = config.getDouble("SoulCrystalChanceMultiplier", 1);
ALT_GAME_KARMA_PLAYER_CAN_BE_KILLED_IN_PEACEZONE = config.getBoolean("AltKarmaPlayerCanBeKilledInPeaceZone", false);
ALT_GAME_KARMA_PLAYER_CAN_SHOP = config.getBoolean("AltKarmaPlayerCanShop", true);
ALT_GAME_KARMA_PLAYER_CAN_TELEPORT = config.getBoolean("AltKarmaPlayerCanTeleport", true);
ALT_GAME_KARMA_PLAYER_CAN_USE_GK = config.getBoolean("AltKarmaPlayerCanUseGK", false);
ALT_GAME_KARMA_PLAYER_CAN_TRADE = config.getBoolean("AltKarmaPlayerCanTrade", true);
ALT_GAME_KARMA_PLAYER_CAN_USE_WAREHOUSE = config.getBoolean("AltKarmaPlayerCanUseWareHouse", true);
FAME_SYSTEM_ENABLED = config.getBoolean("EnableFameSystem", true);
MAX_PERSONAL_FAME_POINTS = config.getInt("MaxPersonalFamePoints", 100000);
FORTRESS_ZONE_FAME_TASK_FREQUENCY = config.getInt("FortressZoneFameTaskFrequency", 300);
FORTRESS_ZONE_FAME_AQUIRE_POINTS = config.getInt("FortressZoneFameAquirePoints", 31);
CASTLE_ZONE_FAME_TASK_FREQUENCY = config.getInt("CastleZoneFameTaskFrequency", 300);
CASTLE_ZONE_FAME_AQUIRE_POINTS = config.getInt("CastleZoneFameAquirePoints", 125);
FAME_FOR_DEAD_PLAYERS = config.getBoolean("FameForDeadPlayers", true);
IS_CRAFTING_ENABLED = config.getBoolean("CraftingEnabled", true);
DWARF_RECIPE_LIMIT = config.getInt("DwarfRecipeLimit", 50);
COMMON_RECIPE_LIMIT = config.getInt("CommonRecipeLimit", 50);
ALT_GAME_CREATION = config.getBoolean("AltGameCreation", false);
ALT_GAME_CREATION_SPEED = config.getDouble("AltGameCreationSpeed", 1);
ALT_GAME_CREATION_XP_RATE = config.getDouble("AltGameCreationXpRate", 1);
ALT_GAME_CREATION_SP_RATE = config.getDouble("AltGameCreationSpRate", 1);
ALT_BLACKSMITH_USE_RECIPES = config.getBoolean("AltBlacksmithUseRecipes", true);
ALT_CLAN_LEADER_INSTANT_ACTIVATION = config.getBoolean("AltClanLeaderInstantActivation", false);
ALT_CLAN_JOIN_DAYS = config.getInt("DaysBeforeJoinAClan", 1);
ALT_CLAN_CREATE_DAYS = config.getInt("DaysBeforeCreateAClan", 10);
ALT_CLAN_DISSOLVE_DAYS = config.getInt("DaysToPassToDissolveAClan", 7);
ALT_ALLY_JOIN_DAYS_WHEN_LEAVED = config.getInt("DaysBeforeJoinAllyWhenLeaved", 1);
ALT_ALLY_JOIN_DAYS_WHEN_DISMISSED = config.getInt("DaysBeforeJoinAllyWhenDismissed", 1);
ALT_ACCEPT_CLAN_DAYS_WHEN_DISMISSED = config.getInt("DaysBeforeAcceptNewClanWhenDismissed", 1);
ALT_CREATE_ALLY_DAYS_WHEN_DISSOLVED = config.getInt("DaysBeforeCreateNewAllyWhenDissolved", 1);
ALT_MAX_NUM_OF_CLANS_IN_ALLY = config.getInt("AltMaxNumOfClansInAlly", 3);
ALT_CLAN_MEMBERS_FOR_WAR = config.getInt("AltClanMembersForWar", 15);
ALT_GAME_NEW_CHAR_ALWAYS_IS_NEWBIE = config.getBoolean("AltNewCharAlwaysIsNewbie", false);
ALT_MEMBERS_CAN_WITHDRAW_FROM_CLANWH = config.getBoolean("AltMembersCanWithdrawFromClanWH", false);
REMOVE_CASTLE_CIRCLETS = config.getBoolean("RemoveCastleCirclets", true);
ALT_PARTY_RANGE = config.getInt("AltPartyRange", 1500);
ALT_LEAVE_PARTY_LEADER = config.getBoolean("AltLeavePartyLeader", false);
STARTING_ADENA = config.getInt("StartingAdena", 0);
STARTING_LEVEL = config.getByte("StartingLevel", (byte) 1);
STARTING_SP = config.getInt("StartingSP", 0);
MAX_ADENA = config.getInt("MaxAdena", 2000000000);
if (MAX_ADENA < 0)
{
MAX_ADENA = Integer.MAX_VALUE;
}
AUTO_LOOT = config.getBoolean("AutoLoot", false);
AUTO_LOOT_RAIDS = config.getBoolean("AutoLootRaids", false);
AUTO_LOOT_SLOT_LIMIT = config.getBoolean("AutoLootSlotLimit", false);
LOOT_RAIDS_PRIVILEGE_INTERVAL = config.getInt("RaidLootRightsInterval", 900) * 1000;
LOOT_RAIDS_PRIVILEGE_CC_SIZE = config.getInt("RaidLootRightsCCSize", 45);
final String[] autoLootItemIds = config.getString("AutoLootItemIds", "0").split(",");
AUTO_LOOT_ITEM_IDS = new HashSet<>(autoLootItemIds.length);
for (String item : autoLootItemIds)
{
Integer itm = 0;
try
{
itm = Integer.parseInt(item);
}
catch (NumberFormatException nfe)
{
LOGGER.warning("Auto loot item ids: Wrong ItemId passed: " + item);
LOGGER.warning(nfe.getMessage());
}
if (itm != 0)
{
AUTO_LOOT_ITEM_IDS.add(itm);
}
}
ENABLE_KEYBOARD_MOVEMENT = config.getBoolean("KeyboardMovement", true);
UNSTUCK_INTERVAL = config.getInt("UnstuckInterval", 300);
TELEPORT_WATCHDOG_TIMEOUT = config.getInt("TeleportWatchdogTimeout", 0);
PLAYER_SPAWN_PROTECTION = config.getInt("PlayerSpawnProtection", 0);
PLAYER_TELEPORT_PROTECTION = config.getInt("PlayerTeleportProtection", 0);
RANDOM_RESPAWN_IN_TOWN_ENABLED = config.getBoolean("RandomRespawnInTownEnabled", true);
OFFSET_ON_TELEPORT_ENABLED = config.getBoolean("OffsetOnTeleportEnabled", true);
MAX_OFFSET_ON_TELEPORT = config.getInt("MaxOffsetOnTeleport", 50);
TELEPORT_WHILE_SIEGE_IN_PROGRESS = config.getBoolean("TeleportWhileSiegeInProgress", true);
PETITIONING_ALLOWED = config.getBoolean("PetitioningAllowed", true);
MAX_PETITIONS_PER_PLAYER = config.getInt("MaxPetitionsPerPlayer", 5);
MAX_PETITIONS_PENDING = config.getInt("MaxPetitionsPending", 25);
MAX_FREE_TELEPORT_LEVEL = config.getInt("MaxFreeTeleportLevel", 40);
ALT_RECOMMEND = config.getBoolean("AltRecommend", false);
DELETE_DAYS = config.getInt("DeleteCharAfterDays", 7);
DISCONNECT_AFTER_DEATH = config.getBoolean("DisconnectAfterDeath", true);
PARTY_XP_CUTOFF_METHOD = Enum.valueOf(PartyExpType.class, config.getString("PartyXpCutoffMethod", "LEVEL").toUpperCase());
PARTY_XP_CUTOFF_PERCENT = config.getDouble("PartyXpCutoffPercent", 3);
PARTY_XP_CUTOFF_LEVEL = config.getInt("PartyXpCutoffLevel", 20);
final String[] gaps = config.getString("PartyXpCutoffGaps", "0,9;10,14;15,99").split(";");
PARTY_XP_CUTOFF_GAPS = new int[gaps.length][2];
for (int i = 0; i < gaps.length; i++)
{
PARTY_XP_CUTOFF_GAPS[i] = new int[]
{
Integer.parseInt(gaps[i].split(",")[0]),
Integer.parseInt(gaps[i].split(",")[1])
};
}
final String[] percents = config.getString("PartyXpCutoffGapPercent", "100;30;0").split(";");
PARTY_XP_CUTOFF_GAP_PERCENTS = new int[percents.length];
for (int i = 0; i < percents.length; i++)
{
PARTY_XP_CUTOFF_GAP_PERCENTS[i] = Integer.parseInt(percents[i]);
}
DISABLE_TUTORIAL = config.getBoolean("DisableTutorial", false);
EXPERTISE_PENALTY = config.getBoolean("ExpertisePenalty", true);
STORE_RECIPE_SHOPLIST = config.getBoolean("StoreRecipeShopList", false);
FORBIDDEN_NAMES = config.getString("ForbiddenNames", "").split(",");
SILENCE_MODE_EXCLUDE = config.getBoolean("SilenceModeExclude", false);
ALT_VALIDATE_TRIGGER_SKILLS = config.getBoolean("AltValidateTriggerSkills", false);
PLAYER_MOVEMENT_BLOCK_TIME = config.getInt("NpcTalkBlockingTime", 0) * 1000;
RANDOMIZE_AUTO_ATTACK_DAMAGE = config.getBoolean("RandomizeAutoAttackDamage", true);
RANDOMIZE_PHYSICAL_SKILL_DAMAGE = config.getBoolean("RandomizePhysicalSkillDamage", true);
RANDOMIZE_MAGICAL_SKILL_DAMAGE = config.getBoolean("RandomizeMagicalSkillDamage", true);
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import java.util.Arrays;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the PVP related configurations.
* @author Mobius
*/
public class PvpConfig
{
// File
private static final String PVP_CONFIG_FILE = "./config/PVP.ini";
// Constants
public static boolean KARMA_DROP_GM;
public static boolean KARMA_AWARD_PK_KILL;
public static int KARMA_PK_LIMIT;
public static String KARMA_NONDROPPABLE_PET_ITEMS;
public static String KARMA_NONDROPPABLE_ITEMS;
public static int[] KARMA_LIST_NONDROPPABLE_PET_ITEMS;
public static int[] KARMA_LIST_NONDROPPABLE_ITEMS;
public static boolean ANTIFEED_ENABLE;
public static boolean ANTIFEED_DUALBOX;
public static boolean ANTIFEED_DISCONNECTED_AS_DUALBOX;
public static int ANTIFEED_INTERVAL;
public static int PVP_NORMAL_TIME;
public static int PVP_PVP_TIME;
public static boolean FLAG_PLAYER_ON_RAID_ATTACK;
public static boolean FLAG_PLAYER_ON_CHAMPION_ATTACK;
public static boolean FLAG_PLAYER_ON_SUMMON_PET_RAID_CHAMPION_ATTACK;
public static void load()
{
final ConfigReader config = new ConfigReader(PVP_CONFIG_FILE);
KARMA_DROP_GM = config.getBoolean("CanGMDropEquipment", false);
KARMA_AWARD_PK_KILL = config.getBoolean("AwardPKKillPVPPoint", false);
KARMA_PK_LIMIT = config.getInt("MinimumPKRequiredToDrop", 5);
KARMA_NONDROPPABLE_PET_ITEMS = config.getString("ListOfPetItems", "2375,3500,3501,3502,4422,4423,4424,4425,6648,6649,6650,9882");
KARMA_NONDROPPABLE_ITEMS = config.getString("ListOfNonDroppableItems", "57,1147,425,1146,461,10,2368,7,6,2370,2369,6842,6611,6612,6613,6614,6615,6616,6617,6618,6619,6620,6621,7694,8181,5575,7694");
String[] karma = KARMA_NONDROPPABLE_PET_ITEMS.split(",");
KARMA_LIST_NONDROPPABLE_PET_ITEMS = new int[karma.length];
for (int i = 0; i < karma.length; i++)
{
KARMA_LIST_NONDROPPABLE_PET_ITEMS[i] = Integer.parseInt(karma[i]);
}
Arrays.sort(KARMA_LIST_NONDROPPABLE_PET_ITEMS);
karma = KARMA_NONDROPPABLE_ITEMS.split(",");
KARMA_LIST_NONDROPPABLE_ITEMS = new int[karma.length];
for (int i = 0; i < karma.length; i++)
{
KARMA_LIST_NONDROPPABLE_ITEMS[i] = Integer.parseInt(karma[i]);
}
Arrays.sort(KARMA_LIST_NONDROPPABLE_ITEMS);
ANTIFEED_ENABLE = config.getBoolean("AntiFeedEnable", false);
ANTIFEED_DUALBOX = config.getBoolean("AntiFeedDualbox", true);
ANTIFEED_DISCONNECTED_AS_DUALBOX = config.getBoolean("AntiFeedDisconnectedAsDualbox", true);
ANTIFEED_INTERVAL = config.getInt("AntiFeedInterval", 120) * 1000;
PVP_NORMAL_TIME = config.getInt("PvPVsNormalTime", 120000);
PVP_PVP_TIME = config.getInt("PvPVsPvPTime", 60000);
FLAG_PLAYER_ON_RAID_ATTACK = config.getBoolean("FlagPlayerOnRaidAttack", true);
FLAG_PLAYER_ON_CHAMPION_ATTACK = config.getBoolean("FlagPlayerOnChampionAttack", true);
FLAG_PLAYER_ON_SUMMON_PET_RAID_CHAMPION_ATTACK = config.getBoolean("FlagPlayerOnSummonPetRaidChampionAttack", true);
}
}
@@ -0,0 +1,243 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import org.l2jmobius.commons.util.ConfigReader;
import org.l2jmobius.commons.util.StringUtil;
import org.l2jmobius.gameserver.entity.actor.enums.npc.DropType;
import org.l2jmobius.gameserver.entity.actor.holders.npc.DropHolder;
/**
* This class loads all the rates related configurations.
* @author Mobius
*/
public class RatesConfig
{
private static final Logger LOGGER = Logger.getLogger(RatesConfig.class.getName());
// File
private static final String RATES_CONFIG_FILE = "./config/Rates.ini";
// Constants
public static float RATE_XP;
public static float RATE_SP;
public static float RATE_PARTY_XP;
public static float RATE_PARTY_SP;
public static float RATE_EXTRACTABLE;
public static int RATE_DROP_MANOR;
public static float QUEST_ITEM_DROP_AMOUNT_MULTIPLIER;
public static float RATE_QUEST_REWARD;
public static float RATE_QUEST_REWARD_XP;
public static float RATE_QUEST_REWARD_SP;
public static float RATE_QUEST_REWARD_ADENA;
public static boolean RATE_QUEST_REWARD_USE_MULTIPLIERS;
public static float RATE_QUEST_REWARD_POTION;
public static float RATE_QUEST_REWARD_SCROLL;
public static float RATE_QUEST_REWARD_RECIPE;
public static float RATE_QUEST_REWARD_MATERIAL;
public static int MONSTER_EXP_MAX_LEVEL_DIFFERENCE;
public static float RATE_VITALITY_LEVEL_1;
public static float RATE_VITALITY_LEVEL_2;
public static float RATE_VITALITY_LEVEL_3;
public static float RATE_VITALITY_LEVEL_4;
public static float RATE_RECOVERY_VITALITY_PEACE_ZONE;
public static float RATE_VITALITY_LOST;
public static float RATE_VITALITY_GAIN;
public static float RATE_RECOVERY_ON_RECONNECT;
public static float RATE_KARMA_LOST;
public static float RATE_KARMA_EXP_LOST;
public static float RATE_SIEGE_GUARDS_PRICE;
public static int PLAYER_DROP_LIMIT;
public static int PLAYER_RATE_DROP;
public static int PLAYER_RATE_DROP_ITEM;
public static int PLAYER_RATE_DROP_EQUIP;
public static int PLAYER_RATE_DROP_EQUIP_WEAPON;
public static float PET_XP_RATE;
public static int PET_FOOD_RATE;
public static float SINEATER_XP_RATE;
public static int KARMA_DROP_LIMIT;
public static int KARMA_RATE_DROP;
public static int KARMA_RATE_DROP_ITEM;
public static int KARMA_RATE_DROP_EQUIP;
public static int KARMA_RATE_DROP_EQUIP_WEAPON;
public static float RATE_DEATH_DROP_AMOUNT_MULTIPLIER;
public static float RATE_SPOIL_DROP_AMOUNT_MULTIPLIER;
public static float RATE_HERB_DROP_AMOUNT_MULTIPLIER;
public static float RATE_RAID_DROP_AMOUNT_MULTIPLIER;
public static float RATE_DEATH_DROP_CHANCE_MULTIPLIER;
public static float RATE_SPOIL_DROP_CHANCE_MULTIPLIER;
public static float RATE_HERB_DROP_CHANCE_MULTIPLIER;
public static float RATE_RAID_DROP_CHANCE_MULTIPLIER;
public static Map<Integer, Float> RATE_DROP_AMOUNT_BY_ID;
public static Map<Integer, Float> RATE_DROP_CHANCE_BY_ID;
public static int DROP_MAX_OCCURRENCES_NORMAL;
public static int DROP_MAX_OCCURRENCES_RAIDBOSS;
public static int DROP_ADENA_MIN_LEVEL_DIFFERENCE;
public static int DROP_ADENA_MAX_LEVEL_DIFFERENCE;
public static double DROP_ADENA_MIN_LEVEL_GAP_CHANCE;
public static int DROP_ITEM_MIN_LEVEL_DIFFERENCE;
public static int DROP_ITEM_MAX_LEVEL_DIFFERENCE;
public static double DROP_ITEM_MIN_LEVEL_GAP_CHANCE;
public static int EVENT_ITEM_MAX_LEVEL_DIFFERENCE;
public static boolean BOSS_DROP_ENABLED;
public static int BOSS_DROP_MIN_LEVEL;
public static int BOSS_DROP_MAX_LEVEL;
public static List<DropHolder> BOSS_DROP_LIST = new ArrayList<>();
public static void load()
{
final ConfigReader config = new ConfigReader(RATES_CONFIG_FILE);
RATE_XP = config.getFloat("RateXp", 1);
RATE_SP = config.getFloat("RateSp", 1);
RATE_PARTY_XP = config.getFloat("RatePartyXp", 1);
RATE_PARTY_SP = config.getFloat("RatePartySp", 1);
RATE_EXTRACTABLE = config.getFloat("RateExtractable", 1);
RATE_DROP_MANOR = config.getInt("RateDropManor", 1);
QUEST_ITEM_DROP_AMOUNT_MULTIPLIER = config.getFloat("QuestItemDropAmountMultiplier", 1);
RATE_QUEST_REWARD = config.getFloat("RateQuestReward", 1);
RATE_QUEST_REWARD_XP = config.getFloat("RateQuestRewardXP", 1);
RATE_QUEST_REWARD_SP = config.getFloat("RateQuestRewardSP", 1);
RATE_QUEST_REWARD_ADENA = config.getFloat("RateQuestRewardAdena", 1);
RATE_QUEST_REWARD_USE_MULTIPLIERS = config.getBoolean("UseQuestRewardMultipliers", false);
RATE_QUEST_REWARD_POTION = config.getFloat("RateQuestRewardPotion", 1);
RATE_QUEST_REWARD_SCROLL = config.getFloat("RateQuestRewardScroll", 1);
RATE_QUEST_REWARD_RECIPE = config.getFloat("RateQuestRewardRecipe", 1);
RATE_QUEST_REWARD_MATERIAL = config.getFloat("RateQuestRewardMaterial", 1);
MONSTER_EXP_MAX_LEVEL_DIFFERENCE = config.getInt("MonsterExpMaxLevelDifference", 11);
RATE_VITALITY_LEVEL_1 = config.getFloat("RateVitalityLevel1", 1.5f);
RATE_VITALITY_LEVEL_2 = config.getFloat("RateVitalityLevel2", 2);
RATE_VITALITY_LEVEL_3 = config.getFloat("RateVitalityLevel3", 2.5f);
RATE_VITALITY_LEVEL_4 = config.getFloat("RateVitalityLevel4", 3);
RATE_RECOVERY_VITALITY_PEACE_ZONE = config.getFloat("RateRecoveryPeaceZone", 1);
RATE_VITALITY_LOST = config.getFloat("RateVitalityLost", 1);
RATE_VITALITY_GAIN = config.getFloat("RateVitalityGain", 1);
RATE_RECOVERY_ON_RECONNECT = config.getFloat("RateRecoveryOnReconnect", 4);
RATE_KARMA_LOST = config.getFloat("RateKarmaLost", -1);
if (RATE_KARMA_LOST == -1)
{
RATE_KARMA_LOST = RATE_XP;
}
RATE_KARMA_EXP_LOST = config.getFloat("RateKarmaExpLost", 1);
RATE_SIEGE_GUARDS_PRICE = config.getFloat("RateSiegeGuardsPrice", 1);
PLAYER_DROP_LIMIT = config.getInt("PlayerDropLimit", 3);
PLAYER_RATE_DROP = config.getInt("PlayerRateDrop", 5);
PLAYER_RATE_DROP_ITEM = config.getInt("PlayerRateDropItem", 70);
PLAYER_RATE_DROP_EQUIP = config.getInt("PlayerRateDropEquip", 25);
PLAYER_RATE_DROP_EQUIP_WEAPON = config.getInt("PlayerRateDropEquipWeapon", 5);
PET_XP_RATE = config.getFloat("PetXpRate", 1);
PET_FOOD_RATE = config.getInt("PetFoodRate", 1);
SINEATER_XP_RATE = config.getFloat("SinEaterXpRate", 1);
KARMA_DROP_LIMIT = config.getInt("KarmaDropLimit", 10);
KARMA_RATE_DROP = config.getInt("KarmaRateDrop", 70);
KARMA_RATE_DROP_ITEM = config.getInt("KarmaRateDropItem", 50);
KARMA_RATE_DROP_EQUIP = config.getInt("KarmaRateDropEquip", 40);
KARMA_RATE_DROP_EQUIP_WEAPON = config.getInt("KarmaRateDropEquipWeapon", 10);
RATE_DEATH_DROP_AMOUNT_MULTIPLIER = config.getFloat("DeathDropAmountMultiplier", 1);
RATE_SPOIL_DROP_AMOUNT_MULTIPLIER = config.getFloat("SpoilDropAmountMultiplier", 1);
RATE_HERB_DROP_AMOUNT_MULTIPLIER = config.getFloat("HerbDropAmountMultiplier", 1);
RATE_RAID_DROP_AMOUNT_MULTIPLIER = config.getFloat("RaidDropAmountMultiplier", 1);
RATE_DEATH_DROP_CHANCE_MULTIPLIER = config.getFloat("DeathDropChanceMultiplier", 1);
RATE_SPOIL_DROP_CHANCE_MULTIPLIER = config.getFloat("SpoilDropChanceMultiplier", 1);
RATE_HERB_DROP_CHANCE_MULTIPLIER = config.getFloat("HerbDropChanceMultiplier", 1);
RATE_RAID_DROP_CHANCE_MULTIPLIER = config.getFloat("RaidDropChanceMultiplier", 1);
final String[] dropAmountMultiplier = config.getString("DropAmountMultiplierByItemId", "").split(";");
RATE_DROP_AMOUNT_BY_ID = new HashMap<>(dropAmountMultiplier.length);
if (!dropAmountMultiplier[0].isEmpty())
{
for (String item : dropAmountMultiplier)
{
final String[] itemSplit = item.split(",");
if (itemSplit.length != 2)
{
LOGGER.warning(StringUtil.concat("Config.load(): invalid config property -> DropAmountMultiplierByItemId \"", item, "\""));
}
else
{
try
{
RATE_DROP_AMOUNT_BY_ID.put(Integer.parseInt(itemSplit[0]), Float.parseFloat(itemSplit[1]));
}
catch (NumberFormatException nfe)
{
if (!item.isEmpty())
{
LOGGER.warning(StringUtil.concat("Config.load(): invalid config property -> DropAmountMultiplierByItemId \"", item, "\""));
}
}
}
}
}
final String[] dropChanceMultiplier = config.getString("DropChanceMultiplierByItemId", "").split(";");
RATE_DROP_CHANCE_BY_ID = new HashMap<>(dropChanceMultiplier.length);
if (!dropChanceMultiplier[0].isEmpty())
{
for (String item : dropChanceMultiplier)
{
final String[] itemSplit = item.split(",");
if (itemSplit.length != 2)
{
LOGGER.warning(StringUtil.concat("Config.load(): invalid config property -> DropChanceMultiplierByItemId \"", item, "\""));
}
else
{
try
{
RATE_DROP_CHANCE_BY_ID.put(Integer.parseInt(itemSplit[0]), Float.parseFloat(itemSplit[1]));
}
catch (NumberFormatException nfe)
{
if (!item.isEmpty())
{
LOGGER.warning(StringUtil.concat("Config.load(): invalid config property -> DropChanceMultiplierByItemId \"", item, "\""));
}
}
}
}
}
DROP_MAX_OCCURRENCES_NORMAL = config.getInt("DropMaxOccurrencesNormal", 2);
DROP_MAX_OCCURRENCES_RAIDBOSS = config.getInt("DropMaxOccurrencesRaidboss", 7);
DROP_ADENA_MIN_LEVEL_DIFFERENCE = config.getInt("DropAdenaMinLevelDifference", 8);
DROP_ADENA_MAX_LEVEL_DIFFERENCE = config.getInt("DropAdenaMaxLevelDifference", 15);
DROP_ADENA_MIN_LEVEL_GAP_CHANCE = config.getDouble("DropAdenaMinLevelGapChance", 10);
DROP_ITEM_MIN_LEVEL_DIFFERENCE = config.getInt("DropItemMinLevelDifference", 5);
DROP_ITEM_MAX_LEVEL_DIFFERENCE = config.getInt("DropItemMaxLevelDifference", 10);
DROP_ITEM_MIN_LEVEL_GAP_CHANCE = config.getDouble("DropItemMinLevelGapChance", 10);
EVENT_ITEM_MAX_LEVEL_DIFFERENCE = config.getInt("EventItemMaxLevelDifference", 9);
BOSS_DROP_ENABLED = config.getBoolean("BossDropEnable", false);
BOSS_DROP_MIN_LEVEL = config.getInt("BossDropMinLevel", 40);
BOSS_DROP_MAX_LEVEL = config.getInt("BossDropMaxLevel", 999);
BOSS_DROP_LIST.clear();
for (String s : config.getString("BossDropList", "").trim().split(";"))
{
if (s.isEmpty())
{
continue;
}
BOSS_DROP_LIST.add(new DropHolder(DropType.DROP, Integer.parseInt(s.split(",")[0]), Integer.parseInt(s.split(",")[1]), Integer.parseInt(s.split(",")[2]), (Double.parseDouble(s.split(",")[3]))));
}
}
}
@@ -0,0 +1,559 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.Inet6Address;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.IntStream;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.l2jmobius.commons.util.ConfigReader;
import org.l2jmobius.commons.util.IXmlReader;
import org.l2jmobius.commons.util.StringUtil;
/**
* This class loads all the server related configurations.
* @author Mobius
*/
public class ServerConfig
{
private static final Logger LOGGER = Logger.getLogger(ServerConfig.class.getName());
// Files
private static final String SERVER_CONFIG_FILE = "./config/Server.ini";
private static final String IPCONFIG_FILE = "./config/ipconfig.xml";
private static final String CHAT_FILTER_FILE = "./config/chatfilter.txt";
private static final String HEXID_FILE = "./config/hexid.txt";
// Constants
public static String GAMESERVER_HOSTNAME;
public static int PORT_GAME;
public static int GAME_SERVER_LOGIN_PORT;
public static String GAME_SERVER_LOGIN_HOST;
public static boolean PACKET_ENCRYPTION;
public static int REQUEST_ID;
public static boolean ACCEPT_ALTERNATE_ID;
public static File DATAPACK_ROOT;
public static File SCRIPT_ROOT;
public static Pattern CHARNAME_TEMPLATE_PATTERN;
public static Pattern PET_NAME_TEMPLATE_PATTERN;
public static Pattern CLAN_NAME_TEMPLATE_PATTERN;
public static int MAX_CHARACTERS_NUMBER_PER_ACCOUNT;
public static int MAXIMUM_ONLINE_USERS;
public static boolean HARDWARE_INFO_ENABLED;
public static boolean KICK_MISSING_HWID;
public static int MAX_PLAYERS_PER_HWID;
public static List<Integer> PROTOCOL_LIST;
public static int SERVER_LIST_TYPE;
public static int SERVER_LIST_AGE;
public static boolean SERVER_LIST_BRACKET;
public static boolean DEADLOCK_WATCHER;
public static int DEADLOCK_CHECK_INTERVAL;
public static boolean RESTART_ON_DEADLOCK;
public static boolean SERVER_RESTART_SCHEDULE_ENABLED;
public static boolean SERVER_RESTART_SCHEDULE_MESSAGE;
public static int SERVER_RESTART_SCHEDULE_COUNTDOWN;
public static String[] SERVER_RESTART_SCHEDULE;
public static List<Integer> SERVER_RESTART_DAYS;
public static boolean PRECAUTIONARY_RESTART_ENABLED;
public static boolean PRECAUTIONARY_RESTART_CPU;
public static boolean PRECAUTIONARY_RESTART_MEMORY;
public static boolean PRECAUTIONARY_RESTART_CHECKS;
public static int PRECAUTIONARY_RESTART_PERCENTAGE;
public static int PRECAUTIONARY_RESTART_DELAY;
public static List<String> GAME_SERVER_SUBNETS;
public static List<String> GAME_SERVER_HOSTS;
// Other
public static boolean RESERVE_HOST_ON_LOGIN = false;
public static List<String> FILTER_LIST;
public static int SERVER_ID;
public static byte[] HEX_ID;
public static void load()
{
final ConfigReader config = new ConfigReader(SERVER_CONFIG_FILE);
GAMESERVER_HOSTNAME = config.getString("GameserverHostname", "0.0.0.0");
PORT_GAME = config.getInt("GameserverPort", 7777);
GAME_SERVER_LOGIN_PORT = config.getInt("LoginPort", 9014);
GAME_SERVER_LOGIN_HOST = config.getString("LoginHost", "127.0.0.1");
PACKET_ENCRYPTION = config.getBoolean("PacketEncryption", false);
REQUEST_ID = config.getInt("RequestServerID", 0);
ACCEPT_ALTERNATE_ID = config.getBoolean("AcceptAlternateID", true);
try
{
DATAPACK_ROOT = new File(config.getString("DatapackRoot", ".").replace('\\', '/')).getCanonicalFile();
}
catch (IOException e)
{
LOGGER.log(Level.WARNING, "Error setting datapack root!", e);
DATAPACK_ROOT = new File(".");
}
try
{
SCRIPT_ROOT = new File(config.getString("ScriptRoot", "./data/scripts").replace('\\', '/')).getCanonicalFile();
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Error setting script root!", e);
SCRIPT_ROOT = new File(".");
}
Pattern charNamePattern;
try
{
charNamePattern = Pattern.compile(config.getString("CnameTemplate", ".*"));
}
catch (PatternSyntaxException e)
{
LOGGER.log(Level.WARNING, "Character name pattern is invalid!", e);
charNamePattern = Pattern.compile(".*");
}
CHARNAME_TEMPLATE_PATTERN = charNamePattern;
Pattern petNamePattern;
try
{
petNamePattern = Pattern.compile(config.getString("PetNameTemplate", ".*"));
}
catch (PatternSyntaxException e)
{
LOGGER.log(Level.WARNING, "Pet name pattern is invalid!", e);
petNamePattern = Pattern.compile(".*");
}
PET_NAME_TEMPLATE_PATTERN = petNamePattern;
Pattern clanNamePattern;
try
{
clanNamePattern = Pattern.compile(config.getString("ClanNameTemplate", ".*"));
}
catch (PatternSyntaxException e)
{
LOGGER.log(Level.WARNING, "Clan name pattern is invalid!", e);
clanNamePattern = Pattern.compile(".*");
}
CLAN_NAME_TEMPLATE_PATTERN = clanNamePattern;
MAX_CHARACTERS_NUMBER_PER_ACCOUNT = config.getInt("CharMaxNumber", 7);
MAXIMUM_ONLINE_USERS = config.getInt("MaximumOnlineUsers", 100);
HARDWARE_INFO_ENABLED = config.getBoolean("EnableHardwareInfo", false);
KICK_MISSING_HWID = config.getBoolean("KickMissingHWID", false);
MAX_PLAYERS_PER_HWID = config.getInt("MaxPlayersPerHWID", 0);
if (MAX_PLAYERS_PER_HWID > 0)
{
KICK_MISSING_HWID = true;
}
final String[] protocols = config.getString("AllowedProtocolRevisions", "746").split(";");
PROTOCOL_LIST = new ArrayList<>(protocols.length);
for (String protocol : protocols)
{
try
{
PROTOCOL_LIST.add(Integer.parseInt(protocol.trim()));
}
catch (NumberFormatException e)
{
LOGGER.warning("Wrong config protocol version: " + protocol + ". Skipped.");
}
}
SERVER_LIST_TYPE = getServerTypeId(config.getString("ServerListType", "Free").split(","));
SERVER_LIST_AGE = config.getInt("ServerListAge", 0);
SERVER_LIST_BRACKET = config.getBoolean("ServerListBrackets", false);
DEADLOCK_WATCHER = config.getBoolean("DeadlockWatcher", true);
DEADLOCK_CHECK_INTERVAL = config.getInt("DeadlockCheckInterval", 20);
RESTART_ON_DEADLOCK = config.getBoolean("RestartOnDeadlock", false);
SERVER_RESTART_SCHEDULE_ENABLED = config.getBoolean("ServerRestartScheduleEnabled", false);
SERVER_RESTART_SCHEDULE_MESSAGE = config.getBoolean("ServerRestartScheduleMessage", false);
SERVER_RESTART_SCHEDULE_COUNTDOWN = config.getInt("ServerRestartScheduleCountdown", 600);
SERVER_RESTART_SCHEDULE = config.getString("ServerRestartSchedule", "08:00").split(",");
SERVER_RESTART_DAYS = new ArrayList<>();
for (String day : config.getString("ServerRestartDays", "").trim().split(","))
{
if (StringUtil.isNumeric(day))
{
SERVER_RESTART_DAYS.add(Integer.parseInt(day));
}
}
PRECAUTIONARY_RESTART_ENABLED = config.getBoolean("PrecautionaryRestartEnabled", false);
PRECAUTIONARY_RESTART_CPU = config.getBoolean("PrecautionaryRestartCpu", true);
PRECAUTIONARY_RESTART_MEMORY = config.getBoolean("PrecautionaryRestartMemory", false);
PRECAUTIONARY_RESTART_CHECKS = config.getBoolean("PrecautionaryRestartChecks", true);
PRECAUTIONARY_RESTART_PERCENTAGE = config.getInt("PrecautionaryRestartPercentage", 95);
PRECAUTIONARY_RESTART_DELAY = config.getInt("PrecautionaryRestartDelay", 60) * 1000;
final IPConfigData ipConfigData = new IPConfigData();
GAME_SERVER_SUBNETS = ipConfigData.getSubnets();
GAME_SERVER_HOSTS = ipConfigData.getHosts();
// Load chatfilter.txt file.
loadChatFilter();
// Load hexid.txt file.
loadHexid();
}
/**
* Loads the chat filter words from the specified file.<br>
* This method reads lines from the {@code CHAT_FILTER_FILE}, trims whitespace and ignores empty lines or lines starting with a '#' character.<br>
* The filtered words are collected into the {@code FILTER_LIST}. If an error occurs during file reading, a warning message is logged.
*/
private static void loadChatFilter()
{
try
{
FILTER_LIST = Files.lines(Paths.get(CHAT_FILTER_FILE), StandardCharsets.UTF_8).map(String::trim).filter(line -> (!line.isEmpty() && (line.charAt(0) != '#'))).toList();
LOGGER.info("Loaded " + FILTER_LIST.size() + " Filter Words.");
}
catch (IOException e)
{
LOGGER.log(Level.WARNING, "Error while loading chat filter words!", e);
}
}
/**
* Loads the HexID configuration from a properties file.<br>
* This method reads the {@code HEXID_FILE} and attempts to load the server ID and hexadecimal ID if available.<br>
* If the file exists, it parses the properties to retrieve the {@code ServerID} and {@code HexID} values.<br>
* The {@code ServerID} is stored as an integer, while the {@code HexID} is converted from a hexadecimal string to a byte array.<br>
* If the file does not contain valid data or cannot be loaded, a warning is logged and the system attempts to retrieve the HexID from another source.
*/
private static void loadHexid()
{
final File hexIdFile = new File(HEXID_FILE);
if (hexIdFile.exists())
{
final ConfigReader hexId = new ConfigReader(HEXID_FILE);
if (hexId.containsKey("ServerID") && hexId.containsKey("HexID"))
{
SERVER_ID = hexId.getInt("ServerID", 1);
try
{
HEX_ID = new BigInteger(hexId.getString("HexID", null), 16).toByteArray();
}
catch (Exception e)
{
LOGGER.warning("Could not load HexID file (" + HEXID_FILE + "). Hopefully login will give us one.");
}
}
}
if (HEX_ID == null)
{
LOGGER.warning("Could not load HexID file (" + HEXID_FILE + "). Hopefully login will give us one.");
}
}
/**
* Save hexadecimal ID of the server in the config file.<br>
* Check {@link #HEXID_FILE}.
* @param serverId the ID of the server whose hexId to save
* @param hexId the hexadecimal ID to store
*/
public static void saveHexid(int serverId, String hexId)
{
saveHexid(serverId, hexId, HEXID_FILE);
}
/**
* Save hexadecimal ID of the server in the config file.
* @param serverId the ID of the server whose hexId to save
* @param hexId the hexadecimal ID to store
* @param fileName name of the config file
*/
private static void saveHexid(int serverId, String hexId, String fileName)
{
try
{
final Properties hexSetting = new Properties();
final File file = new File(fileName);
// Create a new empty file only if it doesn't exist.
if (!file.exists())
{
try (OutputStream out = new FileOutputStream(file))
{
hexSetting.setProperty("ServerID", String.valueOf(serverId));
hexSetting.setProperty("HexID", hexId);
hexSetting.store(out, "The HexId to Auth into LoginServer");
LOGGER.log(Level.INFO, "Gameserver: Generated new HexID file for server id " + serverId + ".");
}
}
}
catch (Exception e)
{
LOGGER.warning(StringUtil.concat("Failed to save hex id to ", fileName, " File."));
LOGGER.warning("Config: " + e.getMessage());
}
}
/**
* Calculates a bitwise ID representing the types of servers specified. Each server type is associated with a unique bit position, allowing multiple types to be combined in a single integer using bitwise OR.
* @param serverTypes An array of server type names as strings. Any unrecognized types are ignored.
* @return An integer representing the combined server types, where each bit corresponds to a specific server type. The result is 0 if no recognized types are provided.
*/
public static int getServerTypeId(String[] serverTypes)
{
int serverType = 0;
for (String cType : serverTypes)
{
switch (cType.trim().toLowerCase())
{
case "normal":
{
serverType |= 0x01;
break;
}
case "relax":
{
serverType |= 0x02;
break;
}
case "test":
{
serverType |= 0x04;
break;
}
case "nolabel":
{
serverType |= 0x08;
break;
}
case "restricted":
{
serverType |= 0x10;
break;
}
case "event":
{
serverType |= 0x20;
break;
}
case "free":
{
serverType |= 0x40;
break;
}
default:
{
break;
}
}
}
return serverType;
}
/**
* A configuration class for managing server IP and subnet settings. This class loads network configuration settings from an XML file or performs automatic configuration if the file is unavailable.<br>
* <p>
* If the configuration file exists, it parses the file to define subnets and hosts manually. If the file is missing, it attempts automatic configuration by retrieving the external IP address and identifying local network interfaces to configure internal IP addresses and subnets.
* </p>
*/
private static class IPConfigData implements IXmlReader
{
private static final List<String> _subnets = new ArrayList<>(5);
private static final List<String> _hosts = new ArrayList<>(5);
public IPConfigData()
{
load();
}
@Override
public void load()
{
final File file = new File(IPCONFIG_FILE);
if (file.exists())
{
LOGGER.info("Network Config: ipconfig.xml exists, using manual configuration...");
parseFile(new File(IPCONFIG_FILE));
}
else // Auto configuration...
{
LOGGER.info("Network Config: ipconfig.xml does not exist, using automatic configuration...");
autoIpConfig();
}
}
@Override
public void parseDocument(Document document, File file)
{
NamedNodeMap attrs;
for (Node n = document.getFirstChild(); n != null; n = n.getNextSibling())
{
if ("gameserver".equalsIgnoreCase(n.getNodeName()))
{
for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling())
{
if ("define".equalsIgnoreCase(d.getNodeName()))
{
attrs = d.getAttributes();
_subnets.add(attrs.getNamedItem("subnet").getNodeValue());
_hosts.add(attrs.getNamedItem("address").getNodeValue());
if (_hosts.size() != _subnets.size())
{
LOGGER.warning("Failed to Load " + IPCONFIG_FILE + " File - subnets does not match server addresses.");
}
}
}
final Node att = n.getAttributes().getNamedItem("address");
if (att == null)
{
LOGGER.warning("Failed to load " + IPCONFIG_FILE + " file - default server address is missing.");
_hosts.add("127.0.0.1");
}
else
{
_hosts.add(att.getNodeValue());
}
_subnets.add("0.0.0.0/0");
}
}
}
protected void autoIpConfig()
{
String externalIp = "127.0.0.1";
try
{
// Java 19
// final URL autoIp = new URL("http://checkip.amazonaws.com");
// Java 20
final URL autoIp = URI.create("http://checkip.amazonaws.com").toURL();
try (BufferedReader in = new BufferedReader(new InputStreamReader(autoIp.openStream())))
{
externalIp = in.readLine();
}
}
catch (IOException e)
{
LOGGER.log(Level.INFO, "Failed to connect to checkip.amazonaws.com please check your internet connection using 127.0.0.1!");
externalIp = "127.0.0.1";
}
try
{
final Enumeration<NetworkInterface> niList = NetworkInterface.getNetworkInterfaces();
while (niList.hasMoreElements())
{
final NetworkInterface ni = niList.nextElement();
if (!ni.isUp() || ni.isVirtual())
{
continue;
}
if (!ni.isLoopback() && ((ni.getHardwareAddress() == null) || (ni.getHardwareAddress().length != 6)))
{
continue;
}
for (InterfaceAddress ia : ni.getInterfaceAddresses())
{
if (ia.getAddress() instanceof Inet6Address)
{
continue;
}
final String hostAddress = ia.getAddress().getHostAddress();
final int subnetPrefixLength = ia.getNetworkPrefixLength();
final int subnetMaskInt = IntStream.rangeClosed(1, subnetPrefixLength).reduce((r, _) -> (r << 1) + 1).orElse(0) << (32 - subnetPrefixLength);
final int hostAddressInt = Arrays.stream(hostAddress.split("\\.")).mapToInt(Integer::parseInt).reduce((r, e) -> (r << 8) + e).orElse(0);
final int subnetAddressInt = hostAddressInt & subnetMaskInt;
final String subnetAddress = ((subnetAddressInt >> 24) & 0xFF) + "." + ((subnetAddressInt >> 16) & 0xFF) + "." + ((subnetAddressInt >> 8) & 0xFF) + "." + (subnetAddressInt & 0xFF);
final String subnet = subnetAddress + '/' + subnetPrefixLength;
if (!_subnets.contains(subnet) && !subnet.equals("0.0.0.0/0"))
{
_subnets.add(subnet);
_hosts.add(hostAddress);
LOGGER.info("Network Config: Adding new subnet: " + subnet + " address: " + hostAddress);
}
}
}
// External host and subnet.
_hosts.add(externalIp);
_subnets.add("0.0.0.0/0");
LOGGER.info("Network Config: Adding new subnet: 0.0.0.0/0 address: " + externalIp);
}
catch (SocketException e)
{
LOGGER.log(Level.INFO, "Network Config: Configuration failed please configure manually using ipconfig.xml", e);
System.exit(0);
}
}
protected List<String> getSubnets()
{
if (_subnets.isEmpty())
{
return Arrays.asList("0.0.0.0/0");
}
return _subnets;
}
protected List<String> getHosts()
{
if (_hosts.isEmpty())
{
return Arrays.asList("127.0.0.1");
}
return _hosts;
}
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config.custom;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the allowed player races related configurations.
* @author Mobius
*/
public class AllowedPlayerRacesConfig
{
// File
private static final String ALLOWED_PLAYER_RACES_CONFIG_FILE = "./config/Custom/AllowedPlayerRaces.ini";
// Constants
public static boolean ALLOW_HUMAN;
public static boolean ALLOW_ELF;
public static boolean ALLOW_DARKELF;
public static boolean ALLOW_ORC;
public static boolean ALLOW_DWARF;
public static void load()
{
final ConfigReader config = new ConfigReader(ALLOWED_PLAYER_RACES_CONFIG_FILE);
ALLOW_HUMAN = config.getBoolean("AllowHuman", true);
ALLOW_ELF = config.getBoolean("AllowElf", true);
ALLOW_DARKELF = config.getBoolean("AllowDarkElf", true);
ALLOW_ORC = config.getBoolean("AllowOrc", true);
ALLOW_DWARF = config.getBoolean("AllowDwarf", true);
}
}
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config.custom;
import java.util.HashSet;
import java.util.Set;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the custom auto play related configurations.
* @author Mobius
*/
public class AutoPlayConfig
{
// File
private static final String AUTO_PLAY_CONFIG_FILE = "./config/Custom/AutoPlay.ini";
// Constants
public static boolean ENABLE_AUTO_PLAY;
public static boolean ENABLE_AUTO_POTION;
public static boolean ENABLE_AUTO_SKILL;
public static boolean ENABLE_AUTO_ITEM;
public static boolean RESUME_AUTO_PLAY;
public static boolean ENABLE_AUTO_ASSIST;
public static int AUTO_PLAY_SHORT_RANGE;
public static int AUTO_PLAY_LONG_RANGE;
public static boolean AUTO_PLAY_PREMIUM;
public static Set<Integer> DISABLED_AUTO_SKILLS = new HashSet<>();
public static Set<Integer> DISABLED_AUTO_ITEMS = new HashSet<>();
public static Set<Integer> IGNORED_AUTO_PICK_ITEMS = new HashSet<>();
public static String AUTO_PLAY_LOGIN_MESSAGE;
public static void load()
{
final ConfigReader config = new ConfigReader(AUTO_PLAY_CONFIG_FILE);
ENABLE_AUTO_PLAY = config.getBoolean("EnableAutoPlay", false);
ENABLE_AUTO_POTION = config.getBoolean("EnableAutoPotion", true);
ENABLE_AUTO_SKILL = config.getBoolean("EnableAutoSkill", true);
ENABLE_AUTO_ITEM = config.getBoolean("EnableAutoItem", true);
RESUME_AUTO_PLAY = config.getBoolean("ResumeAutoPlay", false);
ENABLE_AUTO_ASSIST = config.getBoolean("AssistLeader", false);
AUTO_PLAY_SHORT_RANGE = config.getInt("ShortRange", 600);
AUTO_PLAY_LONG_RANGE = config.getInt("LongRange", 1400);
AUTO_PLAY_PREMIUM = config.getBoolean("AutoPlayPremium", false);
DISABLED_AUTO_SKILLS.clear();
final String disabledSkills = config.getString("DisabledSkillIds", "");
if (!disabledSkills.isEmpty())
{
for (String s : disabledSkills.split(","))
{
DISABLED_AUTO_SKILLS.add(Integer.parseInt(s.trim()));
}
}
DISABLED_AUTO_ITEMS.clear();
final String disabledItems = config.getString("DisabledItemIds", "");
if (!disabledItems.isEmpty())
{
for (String s : disabledItems.split(","))
{
DISABLED_AUTO_ITEMS.add(Integer.parseInt(s.trim()));
}
}
IGNORED_AUTO_PICK_ITEMS.clear();
final String ignoredAutoPickItems = config.getString("IgnoredAutoPickItems", "").trim();
if (!ignoredAutoPickItems.isEmpty())
{
for (String itemIdString : ignoredAutoPickItems.split(","))
{
IGNORED_AUTO_PICK_ITEMS.add(Integer.parseInt(itemIdString.trim()));
}
}
AUTO_PLAY_LOGIN_MESSAGE = config.getString("AutoPlayLoginMessage", "");
}
}
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config.custom;
import java.util.HashSet;
import java.util.Set;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the custom auto potions related configurations.
* @author Mobius
*/
public class AutoPotionsConfig
{
// File
private static final String AUTO_POTIONS_CONFIG_FILE = "./config/Custom/AutoPotions.ini";
// Constants
public static boolean AUTO_POTIONS_ENABLED;
public static boolean AUTO_POTIONS_IN_OLYMPIAD;
public static int AUTO_POTION_MIN_LEVEL;
public static boolean AUTO_CP_ENABLED;
public static boolean AUTO_HP_ENABLED;
public static boolean AUTO_MP_ENABLED;
public static int AUTO_CP_PERCENTAGE;
public static int AUTO_HP_PERCENTAGE;
public static int AUTO_MP_PERCENTAGE;
public static Set<Integer> AUTO_CP_ITEM_IDS = new HashSet<>();
public static Set<Integer> AUTO_HP_ITEM_IDS = new HashSet<>();
public static Set<Integer> AUTO_MP_ITEM_IDS = new HashSet<>();
public static void load()
{
final ConfigReader config = new ConfigReader(AUTO_POTIONS_CONFIG_FILE);
AUTO_POTIONS_ENABLED = config.getBoolean("AutoPotionsEnabled", false);
AUTO_POTIONS_IN_OLYMPIAD = config.getBoolean("AutoPotionsInOlympiad", false);
AUTO_POTION_MIN_LEVEL = config.getInt("AutoPotionMinimumLevel", 1);
AUTO_CP_ENABLED = config.getBoolean("AutoCpEnabled", true);
AUTO_HP_ENABLED = config.getBoolean("AutoHpEnabled", true);
AUTO_MP_ENABLED = config.getBoolean("AutoMpEnabled", true);
AUTO_CP_PERCENTAGE = config.getInt("AutoCpPercentage", 70);
AUTO_HP_PERCENTAGE = config.getInt("AutoHpPercentage", 70);
AUTO_MP_PERCENTAGE = config.getInt("AutoMpPercentage", 70);
AUTO_CP_ITEM_IDS.clear();
for (String s : config.getString("AutoCpItemIds", "0").split(","))
{
AUTO_CP_ITEM_IDS.add(Integer.parseInt(s));
}
AUTO_HP_ITEM_IDS.clear();
for (String s : config.getString("AutoHpItemIds", "0").split(","))
{
AUTO_HP_ITEM_IDS.add(Integer.parseInt(s));
}
AUTO_MP_ITEM_IDS.clear();
for (String s : config.getString("AutoMpItemIds", "0").split(","))
{
AUTO_MP_ITEM_IDS.add(Integer.parseInt(s));
}
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config.custom;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the custom banking related configurations.
* @author Mobius
*/
public class BankingConfig
{
// File
private static final String BANKING_CONFIG_FILE = "./config/Custom/Banking.ini";
// Constants
public static boolean BANKING_SYSTEM_ENABLED;
public static int BANKING_SYSTEM_GOLDBAR_COUNT;
public static int BANKING_SYSTEM_ADENA_COUNT;
public static boolean BANKING_SYSTEM_AUTO_CONVERT_ENABLED;
public static long BANKING_SYSTEM_AUTO_CONVERT_ADENA_LIMIT;
public static void load()
{
final ConfigReader config = new ConfigReader(BANKING_CONFIG_FILE);
BANKING_SYSTEM_ENABLED = config.getBoolean("BankingEnabled", false);
BANKING_SYSTEM_GOLDBAR_COUNT = config.getInt("BankingGoldbarCount", 1);
BANKING_SYSTEM_ADENA_COUNT = config.getInt("BankingAdenaCount", 1000000000);
BANKING_SYSTEM_AUTO_CONVERT_ENABLED = config.getBoolean("BankingAutoConvert", false);
BANKING_SYSTEM_AUTO_CONVERT_ADENA_LIMIT = config.getLong("BankingAutoConvertAdenaLimit", 51000000000L);
}
}
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config.custom;
import java.util.HashSet;
import java.util.Set;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the custom boss announcement related configurations.
* @author Mobius
*/
public class BossAnnouncementsConfig
{
// File
private static final String BOSS_ANNOUNCEMENTS_CONFIG_FILE = "./config/Custom/BossAnnouncements.ini";
// Constants
public static boolean RAIDBOSS_SPAWN_ANNOUNCEMENTS;
public static boolean RAIDBOSS_DEFEAT_ANNOUNCEMENTS;
public static boolean RAIDBOSS_INSTANCE_ANNOUNCEMENTS;
public static boolean GRANDBOSS_SPAWN_ANNOUNCEMENTS;
public static boolean GRANDBOSS_DEFEAT_ANNOUNCEMENTS;
public static boolean GRANDBOSS_INSTANCE_ANNOUNCEMENTS;
public static Set<Integer> RAIDBOSSES_EXCLUDED_FROM_SPAWN_ANNOUNCEMENTS = new HashSet<>();
public static Set<Integer> RAIDBOSSES_EXCLUDED_FROM_DEFEAT_ANNOUNCEMENTS = new HashSet<>();
public static void load()
{
final ConfigReader config = new ConfigReader(BOSS_ANNOUNCEMENTS_CONFIG_FILE);
RAIDBOSS_SPAWN_ANNOUNCEMENTS = config.getBoolean("RaidBossSpawnAnnouncements", false);
RAIDBOSS_DEFEAT_ANNOUNCEMENTS = config.getBoolean("RaidBossDefeatAnnouncements", false);
RAIDBOSS_INSTANCE_ANNOUNCEMENTS = config.getBoolean("RaidBossInstanceAnnouncements", false);
GRANDBOSS_SPAWN_ANNOUNCEMENTS = config.getBoolean("GrandBossSpawnAnnouncements", false);
GRANDBOSS_DEFEAT_ANNOUNCEMENTS = config.getBoolean("GrandBossDefeatAnnouncements", false);
GRANDBOSS_INSTANCE_ANNOUNCEMENTS = config.getBoolean("GrandBossInstanceAnnouncements", false);
RAIDBOSSES_EXCLUDED_FROM_SPAWN_ANNOUNCEMENTS.clear();
for (String raidbossId : config.getString("RaidbossExcludedFromSpawnAnnouncements", "").split(","))
{
if (!raidbossId.isEmpty())
{
RAIDBOSSES_EXCLUDED_FROM_SPAWN_ANNOUNCEMENTS.add(Integer.parseInt(raidbossId.trim()));
}
}
RAIDBOSSES_EXCLUDED_FROM_DEFEAT_ANNOUNCEMENTS.clear();
for (String raidbossId : config.getString("RaidbossExcludedFromDefeatAnnouncements", "").split(","))
{
if (!raidbossId.isEmpty())
{
RAIDBOSSES_EXCLUDED_FROM_DEFEAT_ANNOUNCEMENTS.add(Integer.parseInt(raidbossId.trim()));
}
}
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2013 L2jMobius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.l2jmobius.gameserver.config.custom;
import org.l2jmobius.commons.util.ConfigReader;
/**
* This class loads all the cancel return related configurations.
* @author Naker
*/
public class CancelReturnConfig
{
// File
private static final String CANCEL_RETURN_CONFIG_FILE = "./config/Custom/CancelReturn.ini";
// Constants
public static boolean CANCEL_RETURN_ON;
public static boolean CANCEL_RETURN_MOB;
public static boolean CANCEL_RETURN_PLAYER;
public static boolean CANCEL_RETURN_PLAYER_OLYS;
public static int TIME_TO_RETURN;
public static void load()
{
final ConfigReader config = new ConfigReader(CANCEL_RETURN_CONFIG_FILE);
CANCEL_RETURN_ON = config.getBoolean("CancelReturn", false);
CANCEL_RETURN_MOB = config.getBoolean("ReturnMonster", true);
CANCEL_RETURN_PLAYER = config.getBoolean("ReturnPlayer", true);
CANCEL_RETURN_PLAYER_OLYS = config.getBoolean("ReturnPlayerOlys", false);
TIME_TO_RETURN = config.getInt("TimeToReturn", 10) * 1000;
}
}

Some files were not shown because too many files have changed in this diff Show More