Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
ziqian zhang
Grasscutter
Commits
219a8508
Commit
219a8508
authored
May 08, 2022
by
Akka
Browse files
Merge remote-tracking branch 'origin/development' into tower
parents
4b6842f0
65861c3c
Changes
66
Hide whitespace changes
Inline
Side-by-side
src/main/java/emu/grasscutter/command/commands/TeleportAllCommand.java
View file @
219a8508
...
...
@@ -10,9 +10,9 @@ import java.util.List;
import
static
emu
.
grasscutter
.
utils
.
Language
.
translate
;
@Command
(
label
=
"tpall"
,
usage
=
"tpall"
,
description
=
"Teleports all players in your world to your position"
,
permission
=
"player.tpall"
)
@Command
(
label
=
"tpall"
,
usage
=
"tpall"
,
permission
=
"player.tpall"
,
description
=
"commands.teleportAll.description"
)
public
final
class
TeleportAllCommand
implements
CommandHandler
{
@Override
public
void
execute
(
Player
sender
,
Player
targetPlayer
,
List
<
String
>
args
)
{
if
(
targetPlayer
==
null
)
{
...
...
src/main/java/emu/grasscutter/command/commands/TeleportCommand.java
View file @
219a8508
...
...
@@ -10,8 +10,7 @@ import java.util.List;
import
static
emu
.
grasscutter
.
utils
.
Language
.
translate
;
@Command
(
label
=
"teleport"
,
usage
=
"teleport <x> <y> <z> [scene id]"
,
aliases
=
{
"tp"
},
description
=
"Change the player's position."
,
permission
=
"player.teleport"
)
@Command
(
label
=
"teleport"
,
usage
=
"teleport <x> <y> <z> [scene id]"
,
aliases
=
{
"tp"
},
permission
=
"player.teleport"
,
description
=
"commands.teleport.description"
)
public
final
class
TeleportCommand
implements
CommandHandler
{
private
float
parseRelative
(
String
input
,
Float
current
)
{
// TODO: Maybe this will be useful elsewhere later
...
...
src/main/java/emu/grasscutter/command/commands/WeatherCommand.java
View file @
219a8508
...
...
@@ -11,8 +11,7 @@ import java.util.List;
import
static
emu
.
grasscutter
.
utils
.
Language
.
translate
;
@Command
(
label
=
"weather"
,
usage
=
"weather <weatherId> [climateId]"
,
description
=
"Changes the weather."
,
aliases
=
{
"w"
},
permission
=
"player.weather"
)
@Command
(
label
=
"weather"
,
usage
=
"weather <weatherId> [climateId]"
,
aliases
=
{
"w"
},
permission
=
"player.weather"
,
description
=
"commands.weather.description"
)
public
final
class
WeatherCommand
implements
CommandHandler
{
@Override
...
...
src/main/java/emu/grasscutter/game/gacha/GachaBanner.java
View file @
219a8508
...
...
@@ -96,7 +96,7 @@ public class GachaBanner {
return
toProto
(
""
);
}
public
GachaInfo
toProto
(
String
sessionKey
)
{
String
record
=
"http
s
://"
String
record
=
"http
"
+
(
Grasscutter
.
getConfig
().
getDispatchOptions
().
FrontHTTPS
?
"s"
:
""
)
+
"
://"
+
(
Grasscutter
.
getConfig
().
getDispatchOptions
().
PublicIp
.
isEmpty
()
?
Grasscutter
.
getConfig
().
getDispatchOptions
().
Ip
:
Grasscutter
.
getConfig
().
getDispatchOptions
().
PublicIp
)
...
...
src/main/java/emu/grasscutter/game/managers/SotSManager
/SotSManager
.java
→
src/main/java/emu/grasscutter/game/managers/SotSManager.java
View file @
219a8508
package
emu.grasscutter.game.managers
.SotSManager
;
package
emu.grasscutter.game.managers
;
import
emu.grasscutter.Grasscutter
;
import
emu.grasscutter.game.avatar.Avatar
;
import
emu.grasscutter.game.entity.EntityAvatar
;
import
emu.grasscutter.game.entity.GameEntity
;
import
emu.grasscutter.game.managers.MovementManager.MovementManager
;
import
emu.grasscutter.game.player.Player
;
import
emu.grasscutter.game.props.FightProperty
;
import
emu.grasscutter.game.props.PlayerProperty
;
import
emu.grasscutter.game.world.World
;
import
emu.grasscutter.net.proto.ChangeHpReasonOuterClass
;
import
emu.grasscutter.net.proto.PropChangeReasonOuterClass
;
import
emu.grasscutter.server.game.GameSession
;
...
...
@@ -29,6 +26,8 @@ public class SotSManager {
private
final
Player
player
;
private
Timer
autoRecoverTimer
;
public
final
static
int
GlobalMaximumSpringVolume
=
8500000
;
public
SotSManager
(
Player
player
)
{
this
.
player
=
player
;
}
...
...
src/main/java/emu/grasscutter/game/managers/StaminaManager/AfterUpdateStaminaListener.java
0 → 100644
View file @
219a8508
package
emu.grasscutter.game.managers.StaminaManager
;
public
interface
AfterUpdateStaminaListener
{
/**
* onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
* This gives listeners a chance to intercept this update.
*
* @param reason Why updating stamina.
* @param newStamina New Stamina value.
*/
void
onAfterUpdateStamina
(
String
reason
,
int
newStamina
);
}
src/main/java/emu/grasscutter/game/managers/StaminaManager/BeforeUpdateStaminaListener.java
0 → 100644
View file @
219a8508
package
emu.grasscutter.game.managers.StaminaManager
;
public
interface
BeforeUpdateStaminaListener
{
/**
* onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
* This gives listeners a chance to intercept this update.
* @param reason Why updating stamina.
* @param newStamina New ABSOLUTE stamina value.
* @return true if you want to cancel this update, otherwise false.
*/
int
onBeforeUpdateStamina
(
String
reason
,
int
newStamina
);
/**
* onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
* This gives listeners a chance to intercept this update.
* @param reason Why updating stamina.
* @param consumption ConsumptionType and RELATIVE stamina change amount.
* @return true if you want to cancel this update, otherwise false.
*/
Consumption
onBeforeUpdateStamina
(
String
reason
,
Consumption
consumption
);
}
\ No newline at end of file
src/main/java/emu/grasscutter/game/managers/StaminaManager/Consumption.java
0 → 100644
View file @
219a8508
package
emu.grasscutter.game.managers.StaminaManager
;
public
class
Consumption
{
public
ConsumptionType
consumptionType
;
public
int
amount
;
public
Consumption
(
ConsumptionType
ct
,
int
a
)
{
consumptionType
=
ct
;
amount
=
a
;
}
public
Consumption
(
ConsumptionType
ct
)
{
this
(
ct
,
ct
.
amount
);
}
}
src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java
0 → 100644
View file @
219a8508
package
emu.grasscutter.game.managers.StaminaManager
;
public
enum
ConsumptionType
{
None
(
0
),
// consume
CLIMB_START
(-
500
),
CLIMBING
(-
150
),
CLIMB_JUMP
(-
2500
),
SPRINT
(-
1800
),
DASH
(-
360
),
FLY
(-
60
),
SWIM_DASH_START
(-
20
),
SWIM_DASH
(-
204
),
SWIMMING
(-
80
),
// TODO: Slow swimming is handled per movement, not per second. Movement frequency depends on gender/age/height.
FIGHT
(
0
),
// See StaminaManager.getFightConsumption()
// restore
STANDBY
(
500
),
RUN
(
500
),
WALK
(
500
),
STANDBY_MOVE
(
500
),
POWERED_FLY
(
500
);
public
final
int
amount
;
ConsumptionType
(
int
amount
)
{
this
.
amount
=
amount
;
}
}
\ No newline at end of file
src/main/java/emu/grasscutter/game/managers/StaminaManager/README.md
0 → 100644
View file @
219a8508
# Stamina Manager
---
## UpdateStamina
```
java
// will use consumption.consumptionType as reason
public
int
updateStaminaRelative
(
GameSession
session
,
Consumption
consumption
);
```
```
java
public
int
updateStaminaAbsolute
(
GameSession
session
,
String
reason
,
int
newStamina
)
```
---
## Pause and Resume
```
java
public
void
startSustainedStaminaHandler
()
```
```
java
public
void
stopSustainedStaminaHandler
()
```
---
## Stamina change listeners and intercepting
### BeforeUpdateStaminaListener
```
java
import
emu.grasscutter.game.managers.StaminaManager.BeforeUpdateStaminaListener
;
// Listener sample: plugin disable CLIMB_JUMP stamina cost.
private
class
MyClass
implements
BeforeUpdateStaminaListener
{
// Make your class implement the listener, and pass in your class as a listener.
public
MyClass
()
{
getStaminaManager
().
registerBeforeUpdateStaminaListener
(
"myClass"
,
this
);
}
@Override
public
boolean
onBeforeUpdateStamina
(
String
reason
,
int
newStamina
)
{
// do not intercept this update
return
false
;
}
@Override
public
boolean
onBeforeUpdateStamina
(
String
reason
,
Consumption
consumption
)
{
// Try to intercept if this update is CLIMB_JUMP
if
(
consumption
.
consumptionType
==
ConsumptionType
.
CLIMB_JUMP
)
{
return
true
;
}
// If it is not CLIMB_JUMP, do not intercept.
return
false
;
}
}
```
### AfterUpdateStaminaListener
```
java
import
emu.grasscutter.game.managers.StaminaManager.AfterUpdateStaminaListener
;
// Listener sample: plugin listens for changes already made.
private
class
MyClass
implements
AfterUpdateStaminaListener
{
// Make your class implement the listener, and pass in your class as a listener.
public
MyClass
()
{
registerAfterUpdateStaminaListener
(
"myClass"
,
this
);
}
@Override
public
void
onAfterUpdateStamina
(
String
reason
,
int
newStamina
)
{
// ...
}
}
```
\ No newline at end of file
src/main/java/emu/grasscutter/game/managers/
MovementManager/Movement
Manager.java
→
src/main/java/emu/grasscutter/game/managers/
StaminaManager/Stamina
Manager.java
View file @
219a8508
package
emu.grasscutter.game.managers.
Movement
Manager
;
package
emu.grasscutter.game.managers.
Stamina
Manager
;
import
emu.grasscutter.Grasscutter
;
import
emu.grasscutter.game.entity.EntityAvatar
;
...
...
@@ -7,374 +7,421 @@ import emu.grasscutter.game.player.Player;
import
emu.grasscutter.game.props.FightProperty
;
import
emu.grasscutter.game.props.LifeState
;
import
emu.grasscutter.game.props.PlayerProperty
;
import
emu.grasscutter.net.proto.EntityMoveInfoOuterClass
;
import
emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo
;
import
emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify
;
import
emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo
;
import
emu.grasscutter.net.proto.MotionStateOuterClass.MotionState
;
import
emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType
;
import
emu.grasscutter.net.proto.VectorOuterClass
;
import
emu.grasscutter.net.proto.VectorOuterClass
.Vector
;
import
emu.grasscutter.server.game.GameSession
;
import
emu.grasscutter.server.packet.send.*
;
import
emu.grasscutter.utils.Position
;
import
org.jetbrains.annotations.NotNull
;
import
java.lang.Math
;
import
java.util.*
;
public
class
MovementManager
{
public
HashMap
<
String
,
HashSet
<
MotionState
>>
MotionStatesCategorized
=
new
HashMap
<>();
private
enum
ConsumptionType
{
None
(
0
),
// consume
CLIMB_START
(-
500
),
CLIMBING
(-
150
),
CLIMB_JUMP
(-
2500
),
DASH
(-
1800
),
SPRINT
(-
360
),
FLY
(-
60
),
SWIM_DASH_START
(-
200
),
SWIM_DASH
(-
200
),
SWIMMING
(-
80
),
FIGHT
(
0
),
// restore
STANDBY
(
500
),
RUN
(
500
),
WALK
(
500
),
STANDBY_MOVE
(
500
),
POWERED_FLY
(
500
);
public
final
int
amount
;
ConsumptionType
(
int
amount
)
{
this
.
amount
=
amount
;
}
}
private
class
Consumption
{
public
ConsumptionType
consumptionType
;
public
int
amount
;
public
Consumption
(
ConsumptionType
ct
,
int
a
)
{
consumptionType
=
ct
;
amount
=
a
;
}
public
Consumption
(
ConsumptionType
ct
)
{
this
(
ct
,
ct
.
amount
);
}
}
private
MotionState
previousState
=
MotionState
.
MOTION_STANDBY
;
private
MotionState
currentState
=
MotionState
.
MOTION_STANDBY
;
private
Position
previousCoordinates
=
new
Position
(
0
,
0
,
0
);
private
Position
currentCoordinates
=
new
Position
(
0
,
0
,
0
);
public
class
StaminaManager
{
private
final
Player
player
;
private
HashMap
<
String
,
HashSet
<
MotionState
>>
MotionStatesCategorized
=
new
HashMap
<>();
private
float
landSpeed
=
0
;
private
long
landTimeMillisecond
=
0
;
private
Timer
movementManagerTickTimer
;
public
final
static
int
GlobalMaximumStamina
=
24000
;
private
Position
currentCoordinates
=
new
Position
(
0
,
0
,
0
);
private
Position
previousCoordinates
=
new
Position
(
0
,
0
,
0
);
private
MotionState
currentState
=
MotionState
.
MOTION_STANDBY
;
private
MotionState
previousState
=
MotionState
.
MOTION_STANDBY
;
private
Timer
sustainedStaminaHandlerTimer
;
private
GameSession
cachedSession
=
null
;
private
GameEntity
cachedEntity
=
null
;
private
int
staminaRecoverDelay
=
0
;
private
int
skillCaster
=
0
;
private
int
skillCasting
=
0
;
public
MovementManager
(
Player
player
)
{
previousCoordinates
.
add
(
new
Position
(
0
,
0
,
0
));
private
HashMap
<
String
,
BeforeUpdateStaminaListener
>
beforeUpdateStaminaListeners
=
new
HashMap
<>();
private
HashMap
<
String
,
AfterUpdateStaminaListener
>
afterUpdateStaminaListeners
=
new
HashMap
<>();
public
StaminaManager
(
Player
player
)
{
this
.
player
=
player
;
MotionStatesCategorized
.
put
(
"SWIM"
,
new
HashSet
<>(
Arrays
.
asList
(
MotionState
.
MOTION_SWIM_MOVE
,
MotionState
.
MOTION_SWIM_IDLE
,
MotionState
.
MOTION_SWIM_DASH
,
MotionState
.
MOTION_SWIM_JUMP
MotionState
.
MOTION_SWIM_MOVE
,
MotionState
.
MOTION_SWIM_IDLE
,
MotionState
.
MOTION_SWIM_DASH
,
MotionState
.
MOTION_SWIM_JUMP
)));
MotionStatesCategorized
.
put
(
"STANDBY"
,
new
HashSet
<>(
Arrays
.
asList
(
MotionState
.
MOTION_STANDBY
,
MotionState
.
MOTION_STANDBY_MOVE
,
MotionState
.
MOTION_DANGER_STANDBY
,
MotionState
.
MOTION_DANGER_STANDBY_MOVE
,
MotionState
.
MOTION_LADDER_TO_STANDBY
,
MotionState
.
MOTION_JUMP_UP_WALL_FOR_STANDBY
MotionState
.
MOTION_STANDBY
,
MotionState
.
MOTION_STANDBY_MOVE
,
MotionState
.
MOTION_DANGER_STANDBY
,
MotionState
.
MOTION_DANGER_STANDBY_MOVE
,
MotionState
.
MOTION_LADDER_TO_STANDBY
,
MotionState
.
MOTION_JUMP_UP_WALL_FOR_STANDBY
)));
MotionStatesCategorized
.
put
(
"CLIMB"
,
new
HashSet
<>(
Arrays
.
asList
(
MotionState
.
MOTION_CLIMB
,
MotionState
.
MOTION_CLIMB_JUMP
,
MotionState
.
MOTION_STANDBY_TO_CLIMB
,
MotionState
.
MOTION_LADDER_IDLE
,
MotionState
.
MOTION_LADDER_MOVE
,
MotionState
.
MOTION_LADDER_SLIP
,
MotionState
.
MOTION_STANDBY_TO_LADDER
MotionState
.
MOTION_CLIMB
,
MotionState
.
MOTION_CLIMB_JUMP
,
MotionState
.
MOTION_STANDBY_TO_CLIMB
,
MotionState
.
MOTION_LADDER_IDLE
,
MotionState
.
MOTION_LADDER_MOVE
,
MotionState
.
MOTION_LADDER_SLIP
,
MotionState
.
MOTION_STANDBY_TO_LADDER
)));
MotionStatesCategorized
.
put
(
"FLY"
,
new
HashSet
<>(
Arrays
.
asList
(
MotionState
.
MOTION_FLY
,
MotionState
.
MOTION_FLY_IDLE
,
MotionState
.
MOTION_FLY_SLOW
,
MotionState
.
MOTION_FLY_FAST
,
MotionState
.
MOTION_POWERED_FLY
MotionState
.
MOTION_FLY
,
MotionState
.
MOTION_FLY_IDLE
,
MotionState
.
MOTION_FLY_SLOW
,
MotionState
.
MOTION_FLY_FAST
,
MotionState
.
MOTION_POWERED_FLY
)));
MotionStatesCategorized
.
put
(
"RUN"
,
new
HashSet
<>(
Arrays
.
asList
(
MotionState
.
MOTION_DASH
,
MotionState
.
MOTION_DANGER_DASH
,
MotionState
.
MOTION_DASH_BEFORE_SHAKE
,
MotionState
.
MOTION_RUN
,
MotionState
.
MOTION_DANGER_RUN
,
MotionState
.
MOTION_WALK
,
MotionState
.
MOTION_DANGER_WALK
MotionState
.
MOTION_DASH
,
MotionState
.
MOTION_DANGER_DASH
,
MotionState
.
MOTION_DASH_BEFORE_SHAKE
,
MotionState
.
MOTION_RUN
,
MotionState
.
MOTION_DANGER_RUN
,
MotionState
.
MOTION_WALK
,
MotionState
.
MOTION_DANGER_WALK
)));
MotionStatesCategorized
.
put
(
"FIGHT"
,
new
HashSet
<>(
Arrays
.
asList
(
MotionState
.
MOTION_FIGHT
)));
MotionStatesCategorized
.
put
(
"SKIFF"
,
new
HashSet
<>(
Arrays
.
asList
(
MotionState
.
MOTION_SKIFF_BOARDING
,
MotionState
.
MOTION_SKIFF_NORMAL
,
MotionState
.
MOTION_SKIFF_DASH
,
MotionState
.
MOTION_SKIFF_POWERED_DASH
)));
}
public
void
handle
(
GameSession
session
,
EntityMoveInfoOuterClass
.
EntityMoveInfo
moveInfo
,
GameEntity
entity
)
{
if
(
movementManagerTickTimer
==
null
)
{
movementManagerTickTimer
=
new
Timer
();
movementManagerTickTimer
.
scheduleAtFixedRate
(
new
MotionManagerTick
(),
0
,
200
);
// Listeners
public
boolean
registerBeforeUpdateStaminaListener
(
String
listenerName
,
BeforeUpdateStaminaListener
listener
)
{
if
(
beforeUpdateStaminaListeners
.
containsKey
(
listenerName
))
{
return
false
;
}
// cache info for later use in tick
cachedSession
=
session
;
cachedEntity
=
entity
;
beforeUpdateStaminaListeners
.
put
(
listenerName
,
listener
);
return
true
;
}
MotionInfo
motionInfo
=
moveInfo
.
getMotionInfo
();
moveEntity
(
entity
,
moveInfo
);
VectorOuterClass
.
Vector
posVector
=
motionInfo
.
getPos
();
Position
newPos
=
new
Position
(
posVector
.
getX
(),
posVector
.
getY
(),
posVector
.
getZ
());;
if
(
newPos
.
getX
()
!=
0
&&
newPos
.
getY
()
!=
0
&&
newPos
.
getZ
()
!=
0
)
{
currentCoordinates
=
newPos
;
public
boolean
unregisterBeforeUpdateStaminaListener
(
String
listenerName
)
{
if
(!
beforeUpdateStaminaListeners
.
containsKey
(
listenerName
))
{
return
false
;
}
currentState
=
motionInfo
.
getState
();
Grasscutter
.
getLogger
().
debug
(
""
+
currentState
+
"\t"
+
(
moveInfo
.
getIsReliable
()
?
"reliable"
:
""
));
handleFallOnGround
(
motionInfo
);
beforeUpdateStaminaListeners
.
remove
(
listenerName
);
return
true
;
}
public
void
resetTimer
()
{
Grasscutter
.
getLogger
().
debug
(
"MovementManager ticker stopped"
);
movementManagerTickTimer
.
cancel
();
movementManagerTickTimer
=
null
;
public
boolean
registerAfterUpdateStaminaListener
(
String
listenerName
,
AfterUpdateStaminaListener
listener
)
{
if
(
afterUpdateStaminaListeners
.
containsKey
(
listenerName
))
{
return
false
;
}
afterUpdateStaminaListeners
.
put
(
listenerName
,
listener
);
return
true
;
}
p
rivate
void
moveEntity
(
GameEntity
entity
,
EntityMoveInfoOuterClass
.
EntityMoveInfo
moveInfo
)
{
entity
.
getPosition
().
set
(
moveInfo
.
getMotionInfo
().
getPos
());
entity
.
getRotation
().
set
(
moveInfo
.
getMotionInfo
().
getRot
())
;
entity
.
setLastMoveSceneTimeMs
(
moveInfo
.
getSceneTime
());
entity
.
setLastMoveReliableSeq
(
moveInfo
.
getReliableSeq
()
);
entity
.
setMotionState
(
moveInfo
.
getMotionInfo
().
getState
())
;
p
ublic
boolean
unregisterAfterUpdateStaminaListener
(
String
listenerName
)
{
if
(!
afterUpdateStaminaListeners
.
containsKey
(
listenerName
))
{
return
false
;
}
afterUpdateStaminaListeners
.
remove
(
listenerName
);
return
true
;
}
private
boolean
isPlayerMoving
()
{
float
diffX
=
currentCoordinates
.
getX
()
-
previousCoordinates
.
getX
();
float
diffY
=
currentCoordinates
.
getY
()
-
previousCoordinates
.
getY
();
float
diffZ
=
currentCoordinates
.
getZ
()
-
previousCoordinates
.
getZ
();
// Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + ", " + diffX + ", " + diffY + ", " + diffZ);
return
Math
.
abs
(
diffX
)
>
0.2
||
Math
.
abs
(
diffY
)
>
0.1
||
Math
.
abs
(
diffZ
)
>
0.2
;
}
private
int
getCurrentStamina
()
{
return
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
);
Grasscutter
.
getLogger
().
trace
(
"isPlayerMoving: "
+
previousCoordinates
+
", "
+
currentCoordinates
+
", "
+
diffX
+
", "
+
diffY
+
", "
+
diffZ
);
return
Math
.
abs
(
diffX
)
>
0.3
||
Math
.
abs
(
diffY
)
>
0.2
||
Math
.
abs
(
diffZ
)
>
0.3
;
}
private
int
getMaximumStamina
()
{
return
player
.
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
);
}
// Returns new stamina
public
int
updateStamina
(
GameSession
session
,
int
amount
)
{
int
currentStamina
=
session
.
getPlayer
().
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
);
if
(
amount
==
0
)
{
public
int
updateStaminaRelative
(
GameSession
session
,
Consumption
consumption
)
{
int
currentStamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
);
if
(
consumption
.
amount
==
0
)
{
return
currentStamina
;
}
int
playerMaxStamina
=
session
.
getPlayer
().
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
);
int
newStamina
=
currentStamina
+
amount
;
// notify will update
for
(
Map
.
Entry
<
String
,
BeforeUpdateStaminaListener
>
listener
:
beforeUpdateStaminaListeners
.
entrySet
())
{
Consumption
overriddenConsumption
=
listener
.
getValue
().
onBeforeUpdateStamina
(
consumption
.
consumptionType
.
toString
(),
consumption
);
if
((
overriddenConsumption
.
consumptionType
!=
consumption
.
consumptionType
)
&&
(
overriddenConsumption
.
amount
!=
consumption
.
amount
))
{
Grasscutter
.
getLogger
().
debug
(
"[StaminaManager] Stamina update relative("
+
consumption
.
consumptionType
.
toString
()
+
", "
+
consumption
.
amount
+
") overridden to relative("
+
consumption
.
consumptionType
.
toString
()
+
", "
+
consumption
.
amount
+
") by: "
+
listener
.
getKey
());
return
currentStamina
;
}
}
int
playerMaxStamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
);
Grasscutter
.
getLogger
().
trace
(
currentStamina
+
"/"
+
playerMaxStamina
+
"\t"
+
currentState
+
"\t"
+
(
isPlayerMoving
()
?
"moving"
:
" "
)
+
"\t("
+
consumption
.
consumptionType
+
","
+
consumption
.
amount
+
")"
);
int
newStamina
=
currentStamina
+
consumption
.
amount
;
if
(
newStamina
<
0
)
{
newStamina
=
0
;
}
else
if
(
newStamina
>
playerMaxStamina
)
{
newStamina
=
playerMaxStamina
;
}
return
setStamina
(
session
,
consumption
.
consumptionType
.
toString
(),
newStamina
);
}
public
int
updateStaminaAbsolute
(
GameSession
session
,
String
reason
,
int
newStamina
)
{
int
currentStamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
);
// notify will update
for
(
Map
.
Entry
<
String
,
BeforeUpdateStaminaListener
>
listener
:
beforeUpdateStaminaListeners
.
entrySet
())
{
int
overriddenNewStamina
=
listener
.
getValue
().
onBeforeUpdateStamina
(
reason
,
newStamina
);
if
(
overriddenNewStamina
!=
newStamina
)
{
Grasscutter
.
getLogger
().
debug
(
"[StaminaManager] Stamina update absolute("
+
reason
+
", "
+
newStamina
+
") overridden to absolute("
+
reason
+
", "
+
newStamina
+
") by: "
+
listener
.
getKey
());
return
currentStamina
;
}
}
if
(
newStamina
>
playerMaxStamina
)
{
int
playerMaxStamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
);
if
(
newStamina
<
0
)
{
newStamina
=
0
;
}
else
if
(
newStamina
>
playerMaxStamina
)
{
newStamina
=
playerMaxStamina
;
}
session
.
getPlayer
().
setProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
,
newStamina
);
return
setStamina
(
session
,
reason
,
newStamina
);
}
// Returns new stamina and sends PlayerPropNotify
public
int
setStamina
(
GameSession
session
,
String
reason
,
int
newStamina
)
{
// set stamina
player
.
setProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
,
newStamina
);
session
.
send
(
new
PacketPlayerPropNotify
(
player
,
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
));
// notify updated
for
(
Map
.
Entry
<
String
,
AfterUpdateStaminaListener
>
listener
:
afterUpdateStaminaListeners
.
entrySet
())
{
listener
.
getValue
().
onAfterUpdateStamina
(
reason
,
newStamina
);
}
return
newStamina
;
}
private
void
handleFallOnGround
(
@NotNull
MotionInfo
motionInfo
)
{
MotionState
state
=
motionInfo
.
getState
();
// land speed and fall on ground event arrive in different packets
// cache land speed
if
(
state
==
MotionState
.
MOTION_LAND_SPEED
)
{
landSpeed
=
motionInfo
.
getSpeed
().
getY
();
landTimeMillisecond
=
System
.
currentTimeMillis
();
// Kills avatar, removes entity and sends notification.
// TODO: Probably move this to Avatar class? since other components may also need to kill avatar.
public
void
killAvatar
(
GameSession
session
,
GameEntity
entity
,
PlayerDieType
dieType
)
{
session
.
send
(
new
PacketAvatarLifeStateChangeNotify
(
player
.
getTeamManager
().
getCurrentAvatarEntity
().
getAvatar
(),
LifeState
.
LIFE_DEAD
,
dieType
));
session
.
send
(
new
PacketLifeStateChangeNotify
(
entity
,
LifeState
.
LIFE_DEAD
,
dieType
));
entity
.
setFightProperty
(
FightProperty
.
FIGHT_PROP_CUR_HP
,
0
);
entity
.
getWorld
().
broadcastPacket
(
new
PacketEntityFightPropUpdateNotify
(
entity
,
FightProperty
.
FIGHT_PROP_CUR_HP
));
entity
.
getWorld
().
broadcastPacket
(
new
PacketLifeStateChangeNotify
(
0
,
entity
,
LifeState
.
LIFE_DEAD
));
player
.
getScene
().
removeEntity
(
entity
);
((
EntityAvatar
)
entity
).
onDeath
(
dieType
,
0
);
}
public
void
startSustainedStaminaHandler
()
{
if
(!
player
.
isPaused
()
&&
sustainedStaminaHandlerTimer
==
null
)
{
sustainedStaminaHandlerTimer
=
new
Timer
();
sustainedStaminaHandlerTimer
.
scheduleAtFixedRate
(
new
SustainedStaminaHandler
(),
0
,
200
);
Grasscutter
.
getLogger
().
debug
(
"[MovementManager] SustainedStaminaHandlerTimer started"
);
}
if
(
state
==
MotionState
.
MOTION_FALL_ON_GROUND
)
{
// if not received immediately after MOTION_LAND_SPEED, discard this packet.
// TODO: Test in high latency.
int
maxDelay
=
200
;
if
((
System
.
currentTimeMillis
()
-
landTimeMillisecond
)
>
maxDelay
)
{
Grasscutter
.
getLogger
().
debug
(
"MOTION_FALL_ON_GROUND received after "
+
maxDelay
+
"ms, discard."
);
return
;
}
float
currentHP
=
cachedEntity
.
getFightProperty
(
FightProperty
.
FIGHT_PROP_CUR_HP
);
float
maxHP
=
cachedEntity
.
getFightProperty
(
FightProperty
.
FIGHT_PROP_MAX_HP
);
float
damage
=
0
;
Grasscutter
.
getLogger
().
debug
(
"LandSpeed: "
+
landSpeed
);
if
(
landSpeed
<
-
23.5
)
{
damage
=
(
float
)(
maxHP
*
0.33
);
}
if
(
landSpeed
<
-
25
)
{
damage
=
(
float
)(
maxHP
*
0.5
);
}
if
(
landSpeed
<
-
26.5
)
{
damage
=
(
float
)(
maxHP
*
0.66
);
}
if
(
landSpeed
<
-
28
)
{
damage
=
(
maxHP
*
1
);
}
float
newHP
=
currentHP
-
damage
;
if
(
newHP
<
0
)
{
newHP
=
0
;
}
Grasscutter
.
getLogger
().
debug
(
"Max: "
+
maxHP
+
"\tCurr: "
+
currentHP
+
"\tDamage: "
+
damage
+
"\tnewHP: "
+
newHP
);
cachedEntity
.
setFightProperty
(
FightProperty
.
FIGHT_PROP_CUR_HP
,
newHP
);
cachedEntity
.
getWorld
().
broadcastPacket
(
new
PacketEntityFightPropUpdateNotify
(
cachedEntity
,
FightProperty
.
FIGHT_PROP_CUR_HP
));
if
(
newHP
==
0
)
{
killAvatar
(
cachedSession
,
cachedEntity
,
PlayerDieType
.
PLAYER_DIE_FALL
);
}
landSpeed
=
0
;
}
public
void
stopSustainedStaminaHandler
()
{
if
(
sustainedStaminaHandlerTimer
!=
null
)
{
sustainedStaminaHandlerTimer
.
cancel
();
sustainedStaminaHandlerTimer
=
null
;
Grasscutter
.
getLogger
().
debug
(
"[MovementManager] SustainedStaminaHandlerTimer stopped"
);
}
}
private
void
handleDrowning
()
{
int
stamina
=
getCurrentStamina
();
if
(
stamina
<
10
)
{
boolean
isSwimming
=
MotionStatesCategorized
.
get
(
"SWIM"
).
contains
(
currentState
);
Grasscutter
.
getLogger
().
debug
(
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
)
+
"/"
+
player
.
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
)
+
"\t"
+
currentState
+
"\t"
+
isSwimming
);
if
(
isSwimming
&&
currentState
!=
MotionState
.
MOTION_SWIM_IDLE
)
{
killAvatar
(
cachedSession
,
cachedEntity
,
PlayerDieType
.
PLAYER_DIE_DRAWN
);
// Handlers
// External trigger handler
public
void
handleEvtDoSkillSuccNotify
(
GameSession
session
,
EvtDoSkillSuccNotify
notify
)
{
handleImmediateStamina
(
session
,
notify
);
}
public
void
handleCombatInvocationsNotify
(
GameSession
session
,
EntityMoveInfo
moveInfo
,
GameEntity
entity
)
{
// cache info for later use in SustainedStaminaHandler tick
cachedSession
=
session
;
cachedEntity
=
entity
;
MotionInfo
motionInfo
=
moveInfo
.
getMotionInfo
();
MotionState
motionState
=
motionInfo
.
getState
();
boolean
isReliable
=
moveInfo
.
getIsReliable
();
Grasscutter
.
getLogger
().
trace
(
""
+
motionState
+
"\t"
+
(
isReliable
?
"reliable"
:
""
));
if
(
isReliable
)
{
currentState
=
motionState
;
Vector
posVector
=
motionInfo
.
getPos
();
Position
newPos
=
new
Position
(
posVector
.
getX
(),
posVector
.
getY
(),
posVector
.
getZ
());
if
(
newPos
.
getX
()
!=
0
&&
newPos
.
getY
()
!=
0
&&
newPos
.
getZ
()
!=
0
)
{
currentCoordinates
=
newPos
;
}
}
startSustainedStaminaHandler
();
handleImmediateStamina
(
session
,
motionInfo
,
motionState
,
entity
);
}
public
void
killAvatar
(
GameSession
session
,
GameEntity
entity
,
PlayerDieType
dieType
)
{
cachedSession
.
send
(
new
PacketAvatarLifeStateChangeNotify
(
cachedSession
.
getPlayer
().
getTeamManager
().
getCurrentAvatarEntity
().
getAvatar
(),
LifeState
.
LIFE_DEAD
,
dieType
));
cachedSession
.
send
(
new
PacketLifeStateChangeNotify
(
cachedEntity
,
LifeState
.
LIFE_DEAD
,
dieType
));
cachedEntity
.
setFightProperty
(
FightProperty
.
FIGHT_PROP_CUR_HP
,
0
);
cachedEntity
.
getWorld
().
broadcastPacket
(
new
PacketEntityFightPropUpdateNotify
(
cachedEntity
,
FightProperty
.
FIGHT_PROP_CUR_HP
));
entity
.
getWorld
().
broadcastPacket
(
new
PacketLifeStateChangeNotify
(
0
,
entity
,
LifeState
.
LIFE_DEAD
));
session
.
getPlayer
().
getScene
().
removeEntity
(
entity
);
((
EntityAvatar
)
entity
).
onDeath
(
dieType
,
0
);
// Internal handler
private
void
handleImmediateStamina
(
GameSession
session
,
MotionInfo
motionInfo
,
MotionState
motionState
,
GameEntity
entity
)
{
switch
(
motionState
)
{
case
MOTION_DASH_BEFORE_SHAKE:
if
(
previousState
!=
MotionState
.
MOTION_DASH_BEFORE_SHAKE
)
{
updateStaminaRelative
(
session
,
new
Consumption
(
ConsumptionType
.
SPRINT
));
}
break
;
case
MOTION_CLIMB_JUMP:
if
(
previousState
!=
MotionState
.
MOTION_CLIMB_JUMP
)
{
updateStaminaRelative
(
session
,
new
Consumption
(
ConsumptionType
.
CLIMB_JUMP
));
}
break
;
case
MOTION_SWIM_DASH:
if
(
previousState
!=
MotionState
.
MOTION_SWIM_DASH
)
{
updateStaminaRelative
(
session
,
new
Consumption
(
ConsumptionType
.
SWIM_DASH_START
));
}
break
;
}
}
private
void
handleImmediateStamina
(
GameSession
session
,
EvtDoSkillSuccNotify
notify
)
{
Consumption
consumption
=
getFightConsumption
(
notify
.
getSkillId
());
updateStaminaRelative
(
session
,
consumption
);
}
private
class
MotionManagerTick
extends
TimerTask
{
private
class
SustainedStaminaHandler
extends
TimerTask
{
public
void
run
()
{
if
(
Grasscutter
.
getConfig
().
OpenStamina
)
{
boolean
moving
=
isPlayerMoving
();
if
(
moving
||
(
getCurrentStamina
()
<
getMaximumStamina
()))
{
// Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + (getCurrentStamina() >= getMaximumStamina()) + ", recalculate stamina");
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
int
currentStamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
);
int
maxStamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
);
if
(
moving
||
(
currentStamina
<
maxStamina
))
{
Grasscutter
.
getLogger
().
trace
(
"Player moving: "
+
moving
+
", stamina full: "
+
(
currentStamina
>=
maxStamina
)
+
", recalculate stamina"
);
// TODO: refactor these conditions.
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
if
(
MotionStatesCategorized
.
get
(
"CLIMB"
).
contains
(
currentState
))
{
consumption
=
getClimbConsumption
();
consumption
=
getClimb
Sustained
Consumption
();
}
else
if
(
MotionStatesCategorized
.
get
(
"SWIM"
).
contains
((
currentState
)))
{
consumption
=
getSwimConsumptions
();
consumption
=
getSwim
Sustained
Consumptions
();
}
else
if
(
MotionStatesCategorized
.
get
(
"RUN"
).
contains
(
currentState
))
{
consumption
=
getRunWalkDashConsumption
();
consumption
=
getRunWalkDash
Sustained
Consumption
();
}
else
if
(
MotionStatesCategorized
.
get
(
"FLY"
).
contains
(
currentState
))
{
consumption
=
getFlyConsumption
();
consumption
=
getFly
Sustained
Consumption
();
}
else
if
(
MotionStatesCategorized
.
get
(
"STANDBY"
).
contains
(
currentState
))
{
consumption
=
getStandConsumption
();
}
else
if
(
MotionStatesCategorized
.
get
(
"FIGHT"
).
contains
(
currentState
))
{
consumption
=
getFightConsumption
();
consumption
=
getStandSustainedConsumption
();
}
// delay 2 seconds before start recovering - as official server does.
/*
TODO: Reductions that apply to all motion types:
Elemental Resonance
Wind: -15%
Skills
Diona E: -10% while shield lasts
Barbara E: -12% while lasts
*/
if
(
cachedSession
!=
null
)
{
if
(
consumption
.
amount
<
0
)
{
staminaRecoverDelay
=
0
;
}
if
(
consumption
.
amount
>
0
&&
consumption
.
consumptionType
!=
ConsumptionType
.
POWERED_FLY
)
{
// For POWERED_FLY recover immediately - things like Amber's gliding exam may require this.
if
(
staminaRecoverDelay
<
10
)
{
// For others recover after 2 seconds (10 ticks) - as official server does.
staminaRecoverDelay
++;
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
consumption
.
amount
=
0
;
Grasscutter
.
getLogger
().
trace
(
"[StaminaManager] Delaying recovery: "
+
staminaRecoverDelay
);
}
}
// Grasscutter.getLogger().debug(getCurrentStamina() + "/" + getMaximumStamina() + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t(" + consumption.consumptionType + "," + consumption.amount + ")");
updateStamina
(
cachedSession
,
consumption
.
amount
);
updateStaminaRelative
(
cachedSession
,
consumption
);
}
// tick triggered
handleDrowning
();
}
}
previousState
=
currentState
;
previousCoordinates
=
new
Position
(
currentCoordinates
.
getX
(),
currentCoordinates
.
getY
(),
currentCoordinates
.
getZ
());;
previousCoordinates
=
new
Position
(
currentCoordinates
.
getX
(),
currentCoordinates
.
getY
(),
currentCoordinates
.
getZ
()
);
}
}
private
void
handleDrowning
()
{
int
stamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
);
if
(
stamina
<
10
)
{
Grasscutter
.
getLogger
().
trace
(
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
)
+
"/"
+
player
.
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
)
+
"\t"
+
currentState
);
if
(
currentState
!=
MotionState
.
MOTION_SWIM_IDLE
)
{
killAvatar
(
cachedSession
,
cachedEntity
,
PlayerDieType
.
PLAYER_DIE_DRAWN
);
}
}
}
private
Consumption
getClimbConsumption
()
{
// Consumption Calculators
// Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina
private
Consumption
getFightConsumption
(
int
skillCasting
)
{
/* TODO:
Instead of handling here, consider call StaminaManager.updateStamina****() with a Consumption object with
type=FIGHT and a modified amount when handling attacks for more accurate attack start/end time and
other info. Handling it here could be very complicated.
Charged attack
Default:
Polearm: (-2500)
Claymore: (-4000 per second, -800 each tick)
Catalyst: (-5000)
Talent:
Ningguang: When Ningguang is in possession of Star Jades, her Charged Attack does not consume Stamina. (Catalyst * 0)
Klee: When Jumpy Dumpty and Normal Attacks deal DMG, Klee has a 50% chance to obtain an Explosive Spark.
This Explosive Spark is consumed by the next Charged Attack, which costs no Stamina. (Catalyst * 0)
Constellations:
Hu Tao: While in a Paramita Papilio state activated by Guide to Afterlife, Hu Tao's Charge Attacks do not consume Stamina. (Polearm * 0)
Character Specific:
Keqing: (-2500)
Diluc: (Claymore * 0.5)
Talent Moving: (Those are skills too)
Ayaka: (-1000 initial) (-1500 per second) When the Cryo application at the end of Kamisato Art: Senho hits an opponent (+1000)
Mona: (-1000 initial) (-1500 per second)
*/
// TODO: Currently only handling Ayaka and Mona's talent moving initial costs.
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
if
(
currentState
==
MotionState
.
MOTION_CLIMB
)
{
HashMap
<
Integer
,
Integer
>
fightingCost
=
new
HashMap
<>()
{{
put
(
10013
,
-
1000
);
// Kamisato Ayaka
put
(
10413
,
-
1000
);
// Mona
}};
if
(
fightingCost
.
containsKey
(
skillCasting
))
{
consumption
=
new
Consumption
(
ConsumptionType
.
FIGHT
,
fightingCost
.
get
(
skillCasting
));
}
return
consumption
;
}
private
Consumption
getClimbSustainedConsumption
()
{
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
if
(
currentState
==
MotionState
.
MOTION_CLIMB
&&
isPlayerMoving
())
{
consumption
=
new
Consumption
(
ConsumptionType
.
CLIMBING
);
if
(
previousState
!=
MotionState
.
MOTION_CLIMB
&&
previousState
!=
MotionState
.
MOTION_CLIMB_JUMP
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
CLIMB_START
);
}
if
(!
isPlayerMoving
())
{
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
}
}
if
(
currentState
==
MotionState
.
MOTION_CLIMB_JUMP
)
{
if
(
previousState
!=
MotionState
.
MOTION_CLIMB_JUMP
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
CLIMB_JUMP
);
}
}
// TODO: Foods
return
consumption
;
}
private
Consumption
getSwimConsumptions
()
{
private
Consumption
getSwimSustainedConsumptions
()
{
handleDrowning
();
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
if
(
currentState
==
MotionState
.
MOTION_SWIM_MOVE
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
SWIMMING
);
}
if
(
currentState
==
MotionState
.
MOTION_SWIM_DASH
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
SWIM_DASH_START
);
if
(
previousState
==
MotionState
.
MOTION_SWIM_DASH
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
SWIM_DASH
);
}
consumption
=
new
Consumption
(
ConsumptionType
.
SWIM_DASH
);
}
return
consumption
;
}
private
Consumption
getRunWalkDashConsumption
()
{
private
Consumption
getRunWalkDash
Sustained
Consumption
()
{
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
if
(
currentState
==
MotionState
.
MOTION_DASH_BEFORE_SHAKE
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
DASH
);
if
(
previousState
==
MotionState
.
MOTION_DASH_BEFORE_SHAKE
)
{
// only charge once
consumption
=
new
Consumption
(
ConsumptionType
.
SPRINT
);
}
}
if
(
currentState
==
MotionState
.
MOTION_DASH
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
SPRINT
);
consumption
=
new
Consumption
(
ConsumptionType
.
DASH
);
// TODO: Foods
}
if
(
currentState
==
MotionState
.
MOTION_RUN
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
RUN
);
...
...
@@ -385,30 +432,34 @@ public class MovementManager {
return
consumption
;
}
private
Consumption
getFlyConsumption
()
{
private
Consumption
getFlySustainedConsumption
()
{
// POWERED_FLY, e.g. wind tunnel
if
(
currentState
==
MotionState
.
MOTION_POWERED_FLY
)
{
return
new
Consumption
(
ConsumptionType
.
POWERED_FLY
);
}
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
FLY
);
// Talent
HashMap
<
Integer
,
Float
>
glidingCostReduction
=
new
HashMap
<>()
{{
put
(
212301
,
0.8f
);
// Amber
put
(
222301
,
0.8f
);
// Venti
}};
float
reduction
=
1
;
for
(
EntityAvatar
entity:
cachedSession
.
getPlayer
().
getTeamManager
().
getActiveTeam
())
{
for
(
int
skillId:
entity
.
getAvatar
().
getProudSkillList
())
{
for
(
EntityAvatar
entity
:
cachedSession
.
getPlayer
().
getTeamManager
().
getActiveTeam
())
{
for
(
int
skillId
:
entity
.
getAvatar
().
getProudSkillList
())
{
if
(
glidingCostReduction
.
containsKey
(
skillId
))
{
reduction
=
glidingCostReduction
.
get
(
skillId
);
float
potentialLowerReduction
=
glidingCostReduction
.
get
(
skillId
);
if
(
potentialLowerReduction
<
reduction
)
{
reduction
=
potentialLowerReduction
;
}
}
}
}
consumption
.
amount
*=
reduction
;
// POWERED_FLY, e.g. wind tunnel
if
(
currentState
==
MotionState
.
MOTION_POWERED_FLY
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
POWERED_FLY
);
}
// TODO: Foods
return
consumption
;
}
private
Consumption
getStandConsumption
()
{
private
Consumption
getStand
Sustained
Consumption
()
{
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
if
(
currentState
==
MotionState
.
MOTION_STANDBY
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
STANDBY
);
...
...
@@ -418,25 +469,4 @@ public class MovementManager {
}
return
consumption
;
}
private
Consumption
getFightConsumption
()
{
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
HashMap
<
Integer
,
Integer
>
fightingCost
=
new
HashMap
<>()
{{
put
(
10013
,
-
1000
);
// Kamisato Ayaka
put
(
10413
,
-
1000
);
// Mona
}};
if
(
fightingCost
.
containsKey
(
skillCasting
))
{
consumption
=
new
Consumption
(
ConsumptionType
.
FIGHT
,
fightingCost
.
get
(
skillCasting
));
// only handle once, so reset.
skillCasting
=
0
;
skillCaster
=
0
;
}
return
consumption
;
}
public
void
notifySkill
(
int
caster
,
int
skillId
)
{
skillCaster
=
caster
;
skillCasting
=
skillId
;
}
}
src/main/java/emu/grasscutter/game/player/Player.java
View file @
219a8508
...
...
@@ -22,8 +22,8 @@ import emu.grasscutter.game.inventory.GameItem;
import
emu.grasscutter.game.inventory.Inventory
;
import
emu.grasscutter.game.mail.Mail
;
import
emu.grasscutter.game.mail.MailHandler
;
import
emu.grasscutter.game.managers.
MovementManager.Movement
Manager
;
import
emu.grasscutter.game.managers.SotSManager
.SotSManager
;
import
emu.grasscutter.game.managers.
StaminaManager.Stamina
Manager
;
import
emu.grasscutter.game.managers.SotSManager
;
import
emu.grasscutter.game.props.ActionReason
;
import
emu.grasscutter.game.props.EntityType
;
import
emu.grasscutter.game.props.PlayerProperty
;
...
...
@@ -62,9 +62,6 @@ import java.util.concurrent.LinkedBlockingQueue;
@Entity
(
value
=
"players"
,
useDiscriminator
=
false
)
public
class
Player
{
@Transient
private
static
int
GlobalMaximumSpringVolume
=
8500000
;
@Transient
private
static
int
GlobalMaximumStamina
=
24000
;
@Id
private
int
id
;
@Indexed
(
options
=
@IndexOptions
(
unique
=
true
))
private
String
accountId
;
...
...
@@ -132,7 +129,7 @@ public class Player {
@Transient
private
final
InvokeHandler
<
AbilityInvokeEntry
>
clientAbilityInitFinishHandler
;
private
MapMarksManager
mapMarksManager
;
@Transient
private
MovementManager
movement
Manager
;
@Transient
private
StaminaManager
stamina
Manager
;
private
long
springLastUsed
;
...
...
@@ -178,7 +175,7 @@ public class Player {
this
.
expeditionInfo
=
new
HashMap
<>();
this
.
messageHandler
=
null
;
this
.
mapMarksManager
=
new
MapMarksManager
();
this
.
movement
Manager
=
new
Movement
Manager
(
this
);
this
.
stamina
Manager
=
new
Stamina
Manager
(
this
);
this
.
sotsManager
=
new
SotSManager
(
this
);
}
...
...
@@ -206,7 +203,7 @@ public class Player {
this
.
getRotation
().
set
(
0
,
307
,
0
);
this
.
messageHandler
=
null
;
this
.
mapMarksManager
=
new
MapMarksManager
();
this
.
movement
Manager
=
new
Movement
Manager
(
this
);
this
.
stamina
Manager
=
new
Stamina
Manager
(
this
);
this
.
sotsManager
=
new
SotSManager
(
this
);
}
...
...
@@ -875,11 +872,11 @@ public class Player {
}
public
void
onPause
()
{
getStaminaManager
().
stopSustainedStaminaHandler
();
}
public
void
onUnpause
()
{
getStaminaManager
().
startSustainedStaminaHandler
();
}
public
void
sendPacket
(
BasePacket
packet
)
{
...
...
@@ -1024,7 +1021,7 @@ public class Player {
return
mapMarksManager
;
}
public
Movement
Manager
get
Movement
Manager
()
{
return
movement
Manager
;
}
public
Stamina
Manager
get
Stamina
Manager
()
{
return
stamina
Manager
;
}
public
SotSManager
getSotSManager
()
{
return
sotsManager
;
}
...
...
@@ -1152,7 +1149,7 @@ public class Player {
public
void
onLogout
()
{
// stop stamina calculation
get
Movement
Manager
().
resetTim
er
();
get
Stamina
Manager
().
stopSustainedStaminaHandl
er
();
// force to leave the dungeon
if
(
getScene
().
getSceneType
()
==
SceneType
.
SCENE_DUNGEON
)
{
...
...
@@ -1214,7 +1211,7 @@ public class Player {
}
else
if
(
prop
==
PlayerProperty
.
PROP_LAST_CHANGE_AVATAR_TIME
)
{
// 10001
// TODO: implement sanity check
}
else
if
(
prop
==
PlayerProperty
.
PROP_MAX_SPRING_VOLUME
)
{
// 10002
if
(!(
value
>=
0
&&
value
<=
GlobalMaximumSpringVolume
))
{
return
false
;
}
if
(!(
value
>=
0
&&
value
<=
getSotSManager
().
GlobalMaximumSpringVolume
))
{
return
false
;
}
}
else
if
(
prop
==
PlayerProperty
.
PROP_CUR_SPRING_VOLUME
)
{
// 10003
int
playerMaximumSpringVolume
=
getProperty
(
PlayerProperty
.
PROP_MAX_SPRING_VOLUME
);
if
(!(
value
>=
0
&&
value
<=
playerMaximumSpringVolume
))
{
return
false
;
}
...
...
@@ -1231,7 +1228,7 @@ public class Player {
}
else
if
(
prop
==
PlayerProperty
.
PROP_IS_TRANSFERABLE
)
{
// 10009
if
(!(
0
<=
value
&&
value
<=
1
))
{
return
false
;
}
}
else
if
(
prop
==
PlayerProperty
.
PROP_MAX_STAMINA
)
{
// 10010
if
(!(
value
>=
0
&&
value
<=
GlobalMaximumStamina
))
{
return
false
;
}
if
(!(
value
>=
0
&&
value
<=
getStaminaManager
().
GlobalMaximumStamina
))
{
return
false
;
}
}
else
if
(
prop
==
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
)
{
// 10011
int
playerMaximumStamina
=
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
);
if
(!(
value
>=
0
&&
value
<=
playerMaximumStamina
))
{
return
false
;
}
...
...
@@ -1242,7 +1239,7 @@ public class Player {
}
else
if
(
prop
==
PlayerProperty
.
PROP_PLAYER_EXP
)
{
// 10014
if
(!(
0
<=
value
))
{
return
false
;
}
}
else
if
(
prop
==
PlayerProperty
.
PROP_PLAYER_HCOIN
)
{
// 10015
// see
10015
// see
PlayerProperty.PROP_PLAYER_HCOIN comments
}
else
if
(
prop
==
PlayerProperty
.
PROP_PLAYER_SCOIN
)
{
// 10016
// See 10015
}
else
if
(
prop
==
PlayerProperty
.
PROP_PLAYER_MP_SETTING_TYPE
)
{
// 10017
...
...
src/main/java/emu/grasscutter/game/player/TeamManager.java
View file @
219a8508
...
...
@@ -557,7 +557,7 @@ public class TeamManager {
// return;
// }
// }
player
.
get
Movement
Manager
().
resetTim
er
();
// prevent drowning immediately after respawn
player
.
get
Stamina
Manager
().
stopSustainedStaminaHandl
er
();
// prevent drowning immediately after respawn
// Revive all team members
for
(
EntityAvatar
entity
:
getActiveTeam
())
{
...
...
src/main/java/emu/grasscutter/plugin/PluginManager.java
View file @
219a8508
...
...
@@ -4,12 +4,12 @@ import emu.grasscutter.Grasscutter;
import
emu.grasscutter.server.event.Event
;
import
emu.grasscutter.server.event.EventHandler
;
import
emu.grasscutter.server.event.HandlerPriority
;
import
emu.grasscutter.utils.EventConsumer
;
import
emu.grasscutter.utils.Utils
;
import
java.io.File
;
import
java.io.InputStreamReader
;
import
java.lang.reflect.Method
;
import
java.net.MalformedURLException
;
import
java.net.URL
;
import
java.net.URLClassLoader
;
import
java.util.*
;
...
...
@@ -47,12 +47,23 @@ public final class PluginManager {
List
<
File
>
plugins
=
Arrays
.
stream
(
files
)
.
filter
(
file
->
file
.
getName
().
endsWith
(
".jar"
))
.
toList
();
URL
[]
pluginNames
=
new
URL
[
plugins
.
size
()];
plugins
.
forEach
(
plugin
->
{
try
{
pluginNames
[
plugins
.
indexOf
(
plugin
)]
=
plugin
.
toURI
().
toURL
();
}
catch
(
MalformedURLException
exception
)
{
Grasscutter
.
getLogger
().
warn
(
"Unable to load plugin."
,
exception
);
}
});
URLClassLoader
classLoader
=
new
URLClassLoader
(
pluginNames
);
plugins
.
forEach
(
plugin
->
{
try
{
URL
url
=
plugin
.
toURI
().
toURL
();
try
(
URLClassLoader
loader
=
new
URLClassLoader
(
new
URL
[]{
url
}))
{
URL
configFile
=
loader
.
findResource
(
"plugin.json"
);
URL
configFile
=
loader
.
findResource
(
"plugin.json"
);
// Find the plugin.json file for each plugin.
InputStreamReader
fileReader
=
new
InputStreamReader
(
configFile
.
openStream
());
PluginConfig
pluginConfig
=
Grasscutter
.
getGsonFactory
().
fromJson
(
fileReader
,
PluginConfig
.
class
);
...
...
@@ -68,10 +79,10 @@ public final class PluginManager {
JarEntry
entry
=
entries
.
nextElement
();
if
(
entry
.
isDirectory
()
||
!
entry
.
getName
().
endsWith
(
".class"
)
||
entry
.
getName
().
contains
(
"module-info"
))
continue
;
String
className
=
entry
.
getName
().
replace
(
".class"
,
""
).
replace
(
"/"
,
"."
);
l
oader
.
loadClass
(
className
);
classL
oader
.
loadClass
(
className
);
// Use the same class loader for ALL plugins.
}
Class
<?>
pluginClass
=
l
oader
.
loadClass
(
pluginConfig
.
mainClass
);
Class
<?>
pluginClass
=
classL
oader
.
loadClass
(
pluginConfig
.
mainClass
);
Plugin
pluginInstance
=
(
Plugin
)
pluginClass
.
getDeclaredConstructor
().
newInstance
();
this
.
loadPlugin
(
pluginInstance
,
PluginIdentifier
.
fromPluginConfig
(
pluginConfig
),
loader
);
...
...
@@ -156,6 +167,10 @@ public final class PluginManager {
.
toList
().
forEach
(
handler
->
this
.
invokeHandler
(
event
,
handler
));
}
public
Plugin
getPlugin
(
String
name
)
{
return
this
.
plugins
.
get
(
name
);
}
/**
* Performs logic checks then invokes the provided event handler.
* @param event The event passed through to the handler.
...
...
@@ -167,4 +182,4 @@ public final class PluginManager {
(
event
.
isCanceled
()
&&
handler
.
ignoresCanceled
())
)
handler
.
getCallback
().
consume
((
T
)
event
);
}
}
\ No newline at end of file
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java
View file @
219a8508
package
emu.grasscutter.server.packet.recv
;
import
emu.grasscutter.Grasscutter
;
import
emu.grasscutter.game.entity.GameEntity
;
import
emu.grasscutter.game.props.FightProperty
;
import
emu.grasscutter.net.packet.Opcodes
;
import
emu.grasscutter.net.packet.PacketOpcodes
;
import
emu.grasscutter.net.proto.CombatInvocationsNotifyOuterClass.CombatInvocationsNotify
;
...
...
@@ -8,11 +10,19 @@ import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry;
import
emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo
;
import
emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo
;
import
emu.grasscutter.net.packet.PacketHandler
;
import
emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo
;
import
emu.grasscutter.net.proto.MotionStateOuterClass.MotionState
;
import
emu.grasscutter.net.proto.PlayerDieTypeOuterClass
;
import
emu.grasscutter.server.game.GameSession
;
import
emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify
;
@Opcodes
(
PacketOpcodes
.
CombatInvocationsNotify
)
public
class
HandlerCombatInvocationsNotify
extends
PacketHandler
{
private
float
cachedLandingSpeed
=
0
;
private
long
cachedLandingTimeMillisecond
=
0
;
private
boolean
monitorLandingEvent
=
false
;
@Override
public
void
handle
(
GameSession
session
,
byte
[]
header
,
byte
[]
payload
)
throws
Exception
{
CombatInvocationsNotify
notif
=
CombatInvocationsNotify
.
parseFrom
(
payload
);
...
...
@@ -28,7 +38,34 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
EntityMoveInfo
moveInfo
=
EntityMoveInfo
.
parseFrom
(
entry
.
getCombatData
());
GameEntity
entity
=
session
.
getPlayer
().
getScene
().
getEntityById
(
moveInfo
.
getEntityId
());
if
(
entity
!=
null
)
{
session
.
getPlayer
().
getMovementManager
().
handle
(
session
,
moveInfo
,
entity
);
// Move player
MotionInfo
motionInfo
=
moveInfo
.
getMotionInfo
();
entity
.
getPosition
().
set
(
motionInfo
.
getPos
());
entity
.
getRotation
().
set
(
motionInfo
.
getRot
());
entity
.
setLastMoveSceneTimeMs
(
moveInfo
.
getSceneTime
());
entity
.
setLastMoveReliableSeq
(
moveInfo
.
getReliableSeq
());
MotionState
motionState
=
motionInfo
.
getState
();
entity
.
setMotionState
(
motionState
);
session
.
getPlayer
().
getStaminaManager
().
handleCombatInvocationsNotify
(
session
,
moveInfo
,
entity
);
// TODO: handle MOTION_FIGHT landing which has a different damage factor
// Also, for plunge attacks, LAND_SPEED is always -30 and is not useful.
// May need the height when starting plunge attack.
// MOTION_LAND_SPEED and MOTION_FALL_ON_GROUND arrive in different packets.
// Cache land speed for later use.
if
(
motionState
==
MotionState
.
MOTION_LAND_SPEED
)
{
cachedLandingSpeed
=
motionInfo
.
getSpeed
().
getY
();
cachedLandingTimeMillisecond
=
System
.
currentTimeMillis
();
monitorLandingEvent
=
true
;
}
if
(
monitorLandingEvent
)
{
if
(
motionState
==
MotionState
.
MOTION_FALL_ON_GROUND
)
{
monitorLandingEvent
=
false
;
handleFallOnGround
(
session
,
entity
,
motionState
);
}
}
}
break
;
default
:
...
...
@@ -47,5 +84,48 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
}
}
private
void
handleFallOnGround
(
GameSession
session
,
GameEntity
entity
,
MotionState
motionState
)
{
// People have reported that after plunge attack (client sends a FIGHT instead of FALL_ON_GROUND) they will die
// if they talk to an NPC (this is when the client sends a FALL_ON_GROUND) without jumping again.
// A dirty patch: if not received immediately after MOTION_LAND_SPEED, discard this packet.
// 200ms seems to be a reasonable delay.
int
maxDelay
=
200
;
long
actualDelay
=
System
.
currentTimeMillis
()
-
cachedLandingTimeMillisecond
;
Grasscutter
.
getLogger
().
trace
(
"MOTION_FALL_ON_GROUND received after "
+
actualDelay
+
"/"
+
maxDelay
+
"ms."
+
(
actualDelay
>
maxDelay
?
" Discard"
:
""
));
if
(
actualDelay
>
maxDelay
)
{
return
;
}
float
currentHP
=
entity
.
getFightProperty
(
FightProperty
.
FIGHT_PROP_CUR_HP
);
float
maxHP
=
entity
.
getFightProperty
(
FightProperty
.
FIGHT_PROP_MAX_HP
);
float
damageFactor
=
0
;
if
(
cachedLandingSpeed
<
-
23.5
)
{
damageFactor
=
0.33f
;
}
if
(
cachedLandingSpeed
<
-
25
)
{
damageFactor
=
0.5f
;
}
if
(
cachedLandingSpeed
<
-
26.5
)
{
damageFactor
=
0.66f
;
}
if
(
cachedLandingSpeed
<
-
28
)
{
damageFactor
=
1
f
;
}
float
damage
=
maxHP
*
damageFactor
;
float
newHP
=
currentHP
-
damage
;
if
(
newHP
<
0
)
{
newHP
=
0
;
}
if
(
damageFactor
>
0
)
{
Grasscutter
.
getLogger
().
debug
(
currentHP
+
"/"
+
maxHP
+
"\tLandingSpeed: "
+
cachedLandingSpeed
+
"\tDamageFactor: "
+
damageFactor
+
"\tDamage: "
+
damage
+
"\tNewHP: "
+
newHP
);
}
else
{
Grasscutter
.
getLogger
().
trace
(
currentHP
+
"/"
+
maxHP
+
"\tLandingSpeed: 0\tNo damage"
);
}
entity
.
setFightProperty
(
FightProperty
.
FIGHT_PROP_CUR_HP
,
newHP
);
entity
.
getWorld
().
broadcastPacket
(
new
PacketEntityFightPropUpdateNotify
(
entity
,
FightProperty
.
FIGHT_PROP_CUR_HP
));
if
(
newHP
==
0
)
{
session
.
getPlayer
().
getStaminaManager
().
killAvatar
(
session
,
entity
,
PlayerDieTypeOuterClass
.
PlayerDieType
.
PLAYER_DIE_FALL
);
}
cachedLandingSpeed
=
0
;
}
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java
View file @
219a8508
package
emu.grasscutter.server.packet.recv
;
import
emu.grasscutter.game.managers.SotSManager
.SotSManager
;
import
emu.grasscutter.game.managers.SotSManager
;
import
emu.grasscutter.game.player.Player
;
import
emu.grasscutter.game.props.FightProperty
;
import
emu.grasscutter.net.packet.Opcodes
;
import
emu.grasscutter.net.packet.PacketHandler
;
import
emu.grasscutter.net.packet.PacketOpcodes
;
import
emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason
;
import
emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason
;
import
emu.grasscutter.server.game.GameSession
;
import
emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify
;
import
emu.grasscutter.server.packet.send.PacketAvatarLifeStateChangeNotify
;
import
emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify
;
import
emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify
;
import
java.util.List
;
@Opcodes
(
PacketOpcodes
.
EnterTransPointRegionNotify
)
public
class
HandlerEnterTransPointRegionNotify
extends
PacketHandler
{
...
...
src/main/java/emu/grasscutter/server/packet/recv/HandlerEvtDoSkillSuccNotify.java
View file @
219a8508
package
emu.grasscutter.server.packet.recv
;
import
emu.grasscutter.Grasscutter
;
import
emu.grasscutter.net.packet.Opcodes
;
import
emu.grasscutter.net.packet.PacketHandler
;
import
emu.grasscutter.net.packet.PacketOpcodes
;
...
...
@@ -15,10 +14,7 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler {
EvtDoSkillSuccNotify
notify
=
EvtDoSkillSuccNotify
.
parseFrom
(
payload
);
// TODO: Will be used for deducting stamina for charged skills.
int
caster
=
notify
.
getCasterId
();
int
skillId
=
notify
.
getSkillId
();
session
.
getPlayer
().
getMovementManager
().
notifySkill
(
caster
,
skillId
);
session
.
getPlayer
().
getStaminaManager
().
handleEvtDoSkillSuccNotify
(
session
,
notify
);
}
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerExitTransPointRegionNotify.java
View file @
219a8508
package
emu.grasscutter.server.packet.recv
;
import
emu.grasscutter.game.managers.SotSManager
.SotSManager
;
import
emu.grasscutter.game.managers.SotSManager
;
import
emu.grasscutter.game.player.Player
;
import
emu.grasscutter.net.packet.Opcodes
;
import
emu.grasscutter.net.packet.PacketHandler
;
...
...
src/main/java/emu/grasscutter/tools/Tools.java
View file @
219a8508
...
...
@@ -13,16 +13,16 @@ import java.io.PrintWriter;
import
java.nio.charset.StandardCharsets
;
import
java.time.LocalDateTime
;
import
java.time.format.DateTimeFormatter
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.*
;
import
java.util.stream.Collectors
;
import
com.google.gson.reflect.TypeToken
;
import
emu.grasscutter.GameConstants
;
import
emu.grasscutter.Grasscutter
;
import
emu.grasscutter.command.Command
;
import
emu.grasscutter.command.CommandHandler
;
import
emu.grasscutter.command.CommandMap
;
import
emu.grasscutter.data.GameData
;
import
emu.grasscutter.data.ResourceLoader
;
import
emu.grasscutter.data.def.AvatarData
;
...
...
@@ -31,6 +31,8 @@ import emu.grasscutter.data.def.MonsterData;
import
emu.grasscutter.data.def.SceneData
;
import
emu.grasscutter.utils.Utils
;
import
static
emu
.
grasscutter
.
utils
.
Language
.
translate
;
public
final
class
Tools
{
public
static
void
createGmHandbook
()
throws
Exception
{
ToolsWithLanguageOption
.
createGmHandbook
(
getLanguageOption
());
...
...
@@ -111,7 +113,20 @@ final class ToolsWithLanguageOption {
writer
.
println
(
"// Grasscutter "
+
GameConstants
.
VERSION
+
" GM Handbook"
);
writer
.
println
(
"// Created "
+
dtf
.
format
(
now
)
+
System
.
lineSeparator
()
+
System
.
lineSeparator
());
CommandMap
cmdMap
=
new
CommandMap
(
true
);
List
<
Command
>
cmdList
=
new
ArrayList
<>(
cmdMap
.
getAnnotationsAsList
());
writer
.
println
(
"// Commands"
);
for
(
Command
cmd
:
cmdList
)
{
String
cmdName
=
cmd
.
label
();
while
(
cmdName
.
length
()
<=
15
)
{
cmdName
=
" "
+
cmdName
;
}
writer
.
println
(
cmdName
+
" : "
+
translate
(
cmd
.
description
()));
}
writer
.
println
();
list
=
new
ArrayList
<>(
GameData
.
getAvatarDataMap
().
keySet
());
Collections
.
sort
(
list
);
...
...
src/main/java/emu/grasscutter/utils/Language.java
View file @
219a8508
...
...
@@ -19,7 +19,7 @@ public final class Language {
* @return A language instance.
*/
public
static
Language
getLanguage
(
String
langCode
)
{
return
new
Language
(
langCode
+
".json"
);
return
new
Language
(
langCode
+
".json"
,
Grasscutter
.
getConfig
().
DefaultLanguage
.
toLanguageTag
()
);
}
/**
...
...
@@ -30,6 +30,7 @@ public final class Language {
*/
public
static
String
translate
(
String
key
,
Object
...
args
)
{
String
translated
=
Grasscutter
.
getLanguage
().
get
(
key
);
try
{
return
translated
.
formatted
(
args
);
}
catch
(
Exception
exception
)
{
...
...
@@ -38,48 +39,27 @@ public final class Language {
}
}
/**
* creates a language instance.
* @param fileName The name of the language file.
*/
private
Language
(
String
fileName
)
{
@Nullable
JsonObject
languageData
=
null
;
languageData
=
loadLanguage
(
fileName
);
if
(
languageData
==
null
)
{
Grasscutter
.
getLogger
().
info
(
"Now switch to default language"
);
languageData
=
loadDefaultLanguage
();
}
assert
languageData
!=
null
:
"languageData is null"
;
this
.
languageData
=
languageData
;
}
/**
* Load default language file and creates a language instance.
* @return language data
*/
private
JsonObject
loadDefaultLanguage
()
{
var
fileName
=
Grasscutter
.
getConfig
().
DefaultLanguage
.
toLanguageTag
()
+
".json"
;
return
loadLanguage
(
fileName
);
}
/**
* Reads a file and creates a language instance.
* @param fileName The name of the language file.
* @return language data
*/
private
JsonObject
load
Language
(
String
fileName
)
{
private
Language
(
String
fileName
,
String
fallback
)
{
@Nullable
JsonObject
languageData
=
null
;
try
{
InputStream
file
=
Grasscutter
.
class
.
getResourceAsStream
(
"/languages/"
+
fileName
);
languageData
=
Grasscutter
.
getGsonFactory
().
fromJson
(
Utils
.
readFromInputStream
(
file
),
JsonObject
.
class
);
String
translationContents
=
Utils
.
readFromInputStream
(
file
);
if
(
translationContents
.
equals
(
"empty"
))
{
file
=
Grasscutter
.
class
.
getResourceAsStream
(
"/languages/"
+
fallback
);
translationContents
=
Utils
.
readFromInputStream
(
file
);
}
languageData
=
Grasscutter
.
getGsonFactory
().
fromJson
(
translationContents
,
JsonObject
.
class
);
}
catch
(
Exception
exception
)
{
Grasscutter
.
getLogger
().
warn
(
"Failed to load language file: "
+
fileName
);
Grasscutter
.
getLogger
().
warn
(
"Failed to load language file: "
+
fileName
,
exception
);
}
return
languageData
;
this
.
languageData
=
languageData
;
}
/**
...
...
Prev
1
2
3
4
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment