Added unit stances
This commit is contained in:
@@ -353,6 +353,9 @@ command.rebuild = Rebuild
|
|||||||
command.assist = Assist Player
|
command.assist = Assist Player
|
||||||
command.move = Move
|
command.move = Move
|
||||||
command.boost = Boost
|
command.boost = Boost
|
||||||
|
stance.stop = Cancel Orders
|
||||||
|
stance.shoot = Stance: Shoot
|
||||||
|
stance.holdfire = Stance: Hold Fire
|
||||||
openlink = Open Link
|
openlink = Open Link
|
||||||
copylink = Copy Link
|
copylink = Copy Link
|
||||||
back = Back
|
back = Back
|
||||||
|
|||||||
49
core/src/mindustry/ai/UnitStance.java
Normal file
49
core/src/mindustry/ai/UnitStance.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package mindustry.ai;
|
||||||
|
|
||||||
|
import arc.*;
|
||||||
|
import arc.scene.style.*;
|
||||||
|
import arc.struct.*;
|
||||||
|
import mindustry.gen.*;
|
||||||
|
|
||||||
|
public class UnitStance{
|
||||||
|
/** List of all stances by ID. */
|
||||||
|
public static final Seq<UnitStance> all = new Seq<>();
|
||||||
|
|
||||||
|
public static final UnitStance
|
||||||
|
|
||||||
|
stopStance = new UnitStance("stop", "cancel"), //not a real stance, cannot be selected, just cancels ordewrs
|
||||||
|
shootStance = new UnitStance("shoot", "commandAttack"),
|
||||||
|
holdFireStance = new UnitStance("holdfire", "none");
|
||||||
|
|
||||||
|
/** Unique ID number. */
|
||||||
|
public final int id;
|
||||||
|
/** Named used for tooltip/description. */
|
||||||
|
public final String name;
|
||||||
|
/** Name of UI icon (from Icon class). */
|
||||||
|
public final String icon;
|
||||||
|
|
||||||
|
public UnitStance(String name, String icon){
|
||||||
|
this.name = name;
|
||||||
|
this.icon = icon;
|
||||||
|
|
||||||
|
id = all.size;
|
||||||
|
all.add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String localized(){
|
||||||
|
return Core.bundle.get("stance." + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextureRegionDrawable getIcon(){
|
||||||
|
return Icon.icons.get(icon, Icon.cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public char getEmoji() {
|
||||||
|
return (char) Iconc.codes.get(icon, Iconc.cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString(){
|
||||||
|
return "UnitStance:" + name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,8 @@ public class CommandAI extends AIController{
|
|||||||
protected Seq<Unit> local = new Seq<>(false);
|
protected Seq<Unit> local = new Seq<>(false);
|
||||||
protected boolean flocked;
|
protected boolean flocked;
|
||||||
|
|
||||||
|
/** Stance, usually related to firing mode. */
|
||||||
|
public UnitStance stance = UnitStance.shootStance;
|
||||||
/** Current command this unit is following. */
|
/** Current command this unit is following. */
|
||||||
public @Nullable UnitCommand command;
|
public @Nullable UnitCommand command;
|
||||||
/** Current controller instance based on command. */
|
/** Current controller instance based on command. */
|
||||||
@@ -62,6 +64,8 @@ public class CommandAI extends AIController{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateUnit(){
|
public void updateUnit(){
|
||||||
|
//this should not be possible
|
||||||
|
if(stance == UnitStance.stopStance) stance = UnitStance.shootStance;
|
||||||
|
|
||||||
//remove invalid targets
|
//remove invalid targets
|
||||||
if(commandQueue.any()){
|
if(commandQueue.any()){
|
||||||
@@ -91,6 +95,12 @@ public class CommandAI extends AIController{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clearCommands(){
|
||||||
|
commandQueue.clear();
|
||||||
|
targetPos = null;
|
||||||
|
attackTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void defaultBehavior(){
|
public void defaultBehavior(){
|
||||||
|
|
||||||
//acquiring naval targets isn't supported yet, so use the fallback dumb AI
|
//acquiring naval targets isn't supported yet, so use the fallback dumb AI
|
||||||
@@ -256,6 +266,11 @@ public class CommandAI extends AIController{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldFire(){
|
||||||
|
return stance != UnitStance.holdFireStance;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void hit(Bullet bullet){
|
public void hit(Bullet bullet){
|
||||||
if(unit.team.isAI() && bullet.owner instanceof Teamc teamc && teamc.team() != unit.team && attackTarget == null &&
|
if(unit.team.isAI() && bullet.owner instanceof Teamc teamc && teamc.team() != unit.team && attackTarget == null &&
|
||||||
|
|||||||
@@ -5924,7 +5924,7 @@ public class Blocks{
|
|||||||
forceDark = true;
|
forceDark = true;
|
||||||
privileged = true;
|
privileged = true;
|
||||||
size = 1;
|
size = 1;
|
||||||
maxInstructionsPerTick = 500;
|
maxInstructionsPerTick = 1000;
|
||||||
range = Float.MAX_VALUE;
|
range = Float.MAX_VALUE;
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,12 @@ public class AIController implements UnitController{
|
|||||||
mount.aimY = to.y;
|
mount.aimY = to.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
unit.isShooting |= (mount.shoot = mount.rotate = shoot);
|
mount.shoot = mount.rotate = shoot;
|
||||||
|
if(!shouldFire()){
|
||||||
|
mount.shoot = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unit.isShooting |= mount.shoot;
|
||||||
|
|
||||||
if(mount.target == null && !shoot && !Angles.within(mount.rotation, mount.weapon.baseRotation, 0.01f) && noTargetTime >= rotateBackTimer){
|
if(mount.target == null && !shoot && !Angles.within(mount.rotation, mount.weapon.baseRotation, 0.01f) && noTargetTime >= rotateBackTimer){
|
||||||
mount.rotate = true;
|
mount.rotate = true;
|
||||||
@@ -202,6 +207,11 @@ public class AIController implements UnitController{
|
|||||||
return Units.invalidateTarget(target, unit.team, x, y, range);
|
return Units.invalidateTarget(target, unit.team, x, y, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return whether the unit should actually fire bullets (as opposed to just targeting something) */
|
||||||
|
public boolean shouldFire(){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean shouldShoot(){
|
public boolean shouldShoot(){
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -473,7 +473,7 @@ public class DesktopInput extends InputHandler{
|
|||||||
cursorType = ui.targetCursor;
|
cursorType = ui.targetCursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(input.keyTap(Binding.command_queue) && keybinds.get(Binding.command_mode).key.type != KeyType.mouse){
|
if(input.keyTap(Binding.command_queue) && keybinds.get(Binding.command_queue).key.type != KeyType.mouse){
|
||||||
commandTap(input.mouseX(), input.mouseY(), true);
|
commandTap(input.mouseX(), input.mouseY(), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,6 +312,29 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Remote(called = Loc.server, targets = Loc.both, forward = true)
|
||||||
|
public static void setUnitStance(Player player, int[] unitIds, UnitStance stance){
|
||||||
|
if(player == null || unitIds == null || stance == null) return;
|
||||||
|
|
||||||
|
if(net.server() && !netServer.admins.allowAction(player, ActionType.commandUnits, event -> {
|
||||||
|
event.unitIDs = unitIds;
|
||||||
|
})){
|
||||||
|
throw new ValidateException(player, "Player cannot command units.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int id : unitIds){
|
||||||
|
Unit unit = Groups.unit.getByID(id);
|
||||||
|
if(unit != null && unit.team == player.team() && unit.controller() instanceof CommandAI ai){
|
||||||
|
if(stance == UnitStance.stopStance){ //not a real stance, just cancels orders
|
||||||
|
ai.clearCommands();
|
||||||
|
}else{
|
||||||
|
ai.stance = stance;
|
||||||
|
}
|
||||||
|
unit.lastCommanded = player.coloredName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Remote(called = Loc.server, targets = Loc.both, forward = true)
|
@Remote(called = Loc.server, targets = Loc.both, forward = true)
|
||||||
public static void commandBuilding(Player player, int[] buildings, Vec2 target){
|
public static void commandBuilding(Player player, int[] buildings, Vec2 target){
|
||||||
if(player == null || target == null) return;
|
if(player == null || target == null) return;
|
||||||
|
|||||||
@@ -314,6 +314,16 @@ public class TypeIO{
|
|||||||
return val == 255 ? null : UnitCommand.all.get(val);
|
return val == 255 ? null : UnitCommand.all.get(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void writeStance(Writes write, @Nullable UnitStance stance){
|
||||||
|
write.b(stance == null ? 255 : stance.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UnitStance readStance(Reads read){
|
||||||
|
int val = read.ub();
|
||||||
|
//never returns null
|
||||||
|
return val == 255 ? UnitStance.shootStance : UnitStance.all.get(val);
|
||||||
|
}
|
||||||
|
|
||||||
public static void writeEntity(Writes write, Entityc entity){
|
public static void writeEntity(Writes write, Entityc entity){
|
||||||
write.i(entity == null ? -1 : entity.id());
|
write.i(entity == null ? -1 : entity.id());
|
||||||
}
|
}
|
||||||
@@ -472,7 +482,7 @@ public class TypeIO{
|
|||||||
write.b(3);
|
write.b(3);
|
||||||
write.i(logic.controller.pos());
|
write.i(logic.controller.pos());
|
||||||
}else if(control instanceof CommandAI ai){
|
}else if(control instanceof CommandAI ai){
|
||||||
write.b(7);
|
write.b(8);
|
||||||
write.bool(ai.attackTarget != null);
|
write.bool(ai.attackTarget != null);
|
||||||
write.bool(ai.targetPos != null);
|
write.bool(ai.targetPos != null);
|
||||||
|
|
||||||
@@ -507,6 +517,8 @@ public class TypeIO{
|
|||||||
write.b(3);
|
write.b(3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeStance(write, ai.stance);
|
||||||
}else if(control instanceof AssemblerAI){ //hate
|
}else if(control instanceof AssemblerAI){ //hate
|
||||||
write.b(5);
|
write.b(5);
|
||||||
}else{
|
}else{
|
||||||
@@ -538,8 +550,8 @@ public class TypeIO{
|
|||||||
out.controller = world.build(pos);
|
out.controller = world.build(pos);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
//type 4 is the old CommandAI with no commandIndex, type 6 is the new one with the index as a single byte, type 7 is the one with the command queue
|
//type 4 is the old CommandAI with no commandIndex, type 6 is the new one with the index as a single byte, type 7 is the one with the command queue, 8 adds a stance
|
||||||
}else if(type == 4 || type == 6 || type == 7){
|
}else if(type == 4 || type == 6 || type == 7 || type == 8){
|
||||||
CommandAI ai = prev instanceof CommandAI pai ? pai : new CommandAI();
|
CommandAI ai = prev instanceof CommandAI pai ? pai : new CommandAI();
|
||||||
|
|
||||||
boolean hasAttack = read.bool(), hasPos = read.bool();
|
boolean hasAttack = read.bool(), hasPos = read.bool();
|
||||||
@@ -562,13 +574,13 @@ public class TypeIO{
|
|||||||
ai.attackTarget = null;
|
ai.attackTarget = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(type == 6 || type == 7){
|
if(type == 6 || type == 7 || type == 8){
|
||||||
byte id = read.b();
|
byte id = read.b();
|
||||||
ai.command = id < 0 ? null : UnitCommand.all.get(id);
|
ai.command = id < 0 ? null : UnitCommand.all.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
//command queue only in type 7
|
//command queue only in type 7
|
||||||
if(type == 7){
|
if(type == 7 || type == 8){
|
||||||
ai.commandQueue.clear();
|
ai.commandQueue.clear();
|
||||||
int length = read.ub();
|
int length = read.ub();
|
||||||
for(int i = 0; i < length; i++){
|
for(int i = 0; i < length; i++){
|
||||||
@@ -590,6 +602,10 @@ public class TypeIO{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(type == 8){
|
||||||
|
ai.stance = readStance(read);
|
||||||
|
}
|
||||||
|
|
||||||
return ai;
|
return ai;
|
||||||
}else if(type == 5){
|
}else if(type == 5){
|
||||||
//augh
|
//augh
|
||||||
|
|||||||
@@ -296,6 +296,8 @@ public class UnitType extends UnlockableContent implements Senseable{
|
|||||||
public UnitCommand[] commands = {};
|
public UnitCommand[] commands = {};
|
||||||
/** Command to assign to this unit upon creation. Null indicates the first command in the array. */
|
/** Command to assign to this unit upon creation. Null indicates the first command in the array. */
|
||||||
public @Nullable UnitCommand defaultCommand;
|
public @Nullable UnitCommand defaultCommand;
|
||||||
|
/** Stances this unit can have. An empty array means stances will be assigned based on unit capabilities in init(). */
|
||||||
|
public UnitStance[] stances = {};
|
||||||
|
|
||||||
/** color for outline generated around sprites */
|
/** color for outline generated around sprites */
|
||||||
public Color outlineColor = Pal.darkerMetal;
|
public Color outlineColor = Pal.darkerMetal;
|
||||||
@@ -696,7 +698,9 @@ public class UnitType extends UnlockableContent implements Senseable{
|
|||||||
}
|
}
|
||||||
|
|
||||||
//if a status effects slows a unit when firing, don't shoot while moving.
|
//if a status effects slows a unit when firing, don't shoot while moving.
|
||||||
autoFindTarget = !weapons.contains(w -> w.shootStatus.speedMultiplier < 0.99f) || alwaysShootWhenMoving;
|
if(autoFindTarget){
|
||||||
|
autoFindTarget = !weapons.contains(w -> w.shootStatus.speedMultiplier < 0.99f) || alwaysShootWhenMoving;
|
||||||
|
}
|
||||||
|
|
||||||
clipSize = Math.max(clipSize, lightRadius * 1.1f);
|
clipSize = Math.max(clipSize, lightRadius * 1.1f);
|
||||||
singleTarget = weapons.size <= 1 && !forceMultiTarget;
|
singleTarget = weapons.size <= 1 && !forceMultiTarget;
|
||||||
@@ -830,6 +834,14 @@ public class UnitType extends UnlockableContent implements Senseable{
|
|||||||
commands = cmds.toArray();
|
commands = cmds.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(stances.length == 0){
|
||||||
|
if(canAttack){
|
||||||
|
stances = new UnitStance[]{UnitStance.stopStance, UnitStance.shootStance, UnitStance.holdFireStance};
|
||||||
|
}else{
|
||||||
|
stances = new UnitStance[]{UnitStance.stopStance, UnitStance.shootStance};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//dynamically create ammo capacity based on firing rate
|
//dynamically create ammo capacity based on firing rate
|
||||||
if(ammoCapacity < 0){
|
if(ammoCapacity < 0){
|
||||||
float shotsPerSecond = weapons.sumf(w -> w.useAmmo ? 60f / w.reload : 0f);
|
float shotsPerSecond = weapons.sumf(w -> w.useAmmo ? 60f / w.reload : 0f);
|
||||||
|
|||||||
@@ -441,6 +441,9 @@ public class PlacementFragment{
|
|||||||
UnitCommand[] currentCommand = {null};
|
UnitCommand[] currentCommand = {null};
|
||||||
var commands = new Seq<UnitCommand>();
|
var commands = new Seq<UnitCommand>();
|
||||||
|
|
||||||
|
UnitStance[] currentStance = {null};
|
||||||
|
var stances = new Seq<UnitStance>();
|
||||||
|
|
||||||
rebuildCommand = () -> {
|
rebuildCommand = () -> {
|
||||||
u.clearChildren();
|
u.clearChildren();
|
||||||
var units = control.input.selectedUnits;
|
var units = control.input.selectedUnits;
|
||||||
@@ -450,7 +453,8 @@ public class PlacementFragment{
|
|||||||
counts[unit.type.id] ++;
|
counts[unit.type.id] ++;
|
||||||
}
|
}
|
||||||
commands.clear();
|
commands.clear();
|
||||||
boolean firstCommand = false;
|
stances.clear();
|
||||||
|
boolean firstCommand = false, firstStance = false;
|
||||||
Table unitlist = u.table().growX().left().get();
|
Table unitlist = u.table().growX().left().get();
|
||||||
unitlist.left();
|
unitlist.left();
|
||||||
|
|
||||||
@@ -489,13 +493,23 @@ public class PlacementFragment{
|
|||||||
//remove commands that this next unit type doesn't have
|
//remove commands that this next unit type doesn't have
|
||||||
commands.removeAll(com -> !Structs.contains(type.commands, com));
|
commands.removeAll(com -> !Structs.contains(type.commands, com));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!firstStance){
|
||||||
|
stances.add(type.stances);
|
||||||
|
firstStance = true;
|
||||||
|
}else{
|
||||||
|
//remove commands that this next unit type doesn't have
|
||||||
|
stances.removeAll(st -> !Structs.contains(type.stances, st));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//list commands
|
||||||
if(commands.size > 1){
|
if(commands.size > 1){
|
||||||
u.row();
|
u.row();
|
||||||
|
|
||||||
u.table(coms -> {
|
u.table(coms -> {
|
||||||
|
coms.left();
|
||||||
for(var command : commands){
|
for(var command : commands){
|
||||||
coms.button(Icon.icons.get(command.icon, Icon.cancel), Styles.clearNoneTogglei, () -> {
|
coms.button(Icon.icons.get(command.icon, Icon.cancel), Styles.clearNoneTogglei, () -> {
|
||||||
IntSeq ids = new IntSeq();
|
IntSeq ids = new IntSeq();
|
||||||
@@ -508,37 +522,71 @@ public class PlacementFragment{
|
|||||||
}
|
}
|
||||||
}).fillX().padTop(4f).left();
|
}).fillX().padTop(4f).left();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//list stances
|
||||||
|
if(stances.size > 0){
|
||||||
|
u.row();
|
||||||
|
|
||||||
|
u.table(coms -> {
|
||||||
|
coms.left();
|
||||||
|
for(var stance : stances){
|
||||||
|
coms.button(Icon.icons.get(stance.icon, Icon.cancel), Styles.clearNoneTogglei, () -> {
|
||||||
|
IntSeq ids = new IntSeq();
|
||||||
|
for(var unit : units){
|
||||||
|
ids.add(unit.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Call.setUnitStance(Vars.player, ids.toArray(), stance);
|
||||||
|
}).checked(i -> currentStance[0] == stance).size(50f).tooltip(stance.localized());
|
||||||
|
}
|
||||||
|
}).fillX().padTop(4f).left();
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
u.add(Core.bundle.get("commandmode.nounits")).color(Color.lightGray).growX().center().labelAlign(Align.center).pad(6);
|
u.add(Core.bundle.get("commandmode.nounits")).color(Color.lightGray).growX().center().labelAlign(Align.center).pad(6);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
u.update(() -> {
|
u.update(() -> {
|
||||||
boolean hadCommand = false;
|
{
|
||||||
UnitCommand shareCommand = null;
|
boolean hadCommand = false, hadStance = false;
|
||||||
|
UnitCommand shareCommand = null;
|
||||||
|
UnitStance shareStance = null;
|
||||||
|
|
||||||
//find the command that all units have, or null if they do not share one
|
//find the command that all units have, or null if they do not share one
|
||||||
for(var unit : control.input.selectedUnits){
|
for(var unit : control.input.selectedUnits){
|
||||||
if(unit.isCommandable()){
|
if(unit.isCommandable()){
|
||||||
var nextCommand = unit.command().command;
|
var nextCommand = unit.command().command;
|
||||||
|
|
||||||
if(hadCommand){
|
if(hadCommand){
|
||||||
if(shareCommand != nextCommand){
|
if(shareCommand != nextCommand){
|
||||||
shareCommand = null;
|
shareCommand = null;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
shareCommand = nextCommand;
|
||||||
|
hadCommand = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextStance = unit.command().stance;
|
||||||
|
|
||||||
|
if(hadStance){
|
||||||
|
if(shareStance != nextStance){
|
||||||
|
shareStance = null;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
shareStance = nextStance;
|
||||||
|
hadStance = true;
|
||||||
}
|
}
|
||||||
}else{
|
|
||||||
shareCommand = nextCommand;
|
|
||||||
hadCommand = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
currentCommand[0] = shareCommand;
|
currentCommand[0] = shareCommand;
|
||||||
|
currentStance[0] = shareStance;
|
||||||
|
|
||||||
int size = control.input.selectedUnits.size;
|
int size = control.input.selectedUnits.size;
|
||||||
if(curCount[0] != size){
|
if(curCount[0] != size){
|
||||||
curCount[0] = size;
|
curCount[0] = size;
|
||||||
rebuildCommand.run();
|
rebuildCommand.run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
rebuildCommand.run();
|
rebuildCommand.run();
|
||||||
|
|||||||
@@ -361,11 +361,11 @@ public class CoreBlock extends StorageBlock{
|
|||||||
public void changeTeam(Team next){
|
public void changeTeam(Team next){
|
||||||
if(this.team == next) return;
|
if(this.team == next) return;
|
||||||
|
|
||||||
state.teams.unregisterCore(this);
|
onRemoved();
|
||||||
|
|
||||||
super.changeTeam(next);
|
super.changeTeam(next);
|
||||||
|
|
||||||
state.teams.registerCore(this);
|
onProximityUpdate();
|
||||||
|
|
||||||
Events.fire(new CoreChangeEvent(this));
|
Events.fire(new CoreChangeEvent(this));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user