Files
Mindustry/core/src/mindustry/entities/comp/PlayerComp.java

348 lines
10 KiB
Java

package mindustry.entities.comp;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import arc.util.pooling.*;
import mindustry.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.entities.units.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.net.Administration.*;
import mindustry.net.*;
import mindustry.net.Packets.*;
import mindustry.ui.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import static mindustry.Vars.*;
@EntityDef(value = {Playerc.class}, serialize = false)
@Component(base = true)
abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Drawc{
static final float deathDelay = 60f;
@Import float x, y;
@ReadOnly Unit unit = Nulls.unit;
transient @Nullable NetConnection con;
@ReadOnly Team team = Team.sharded;
@SyncLocal boolean typing, shooting, boosting;
@SyncLocal float mouseX, mouseY;
boolean admin;
String name = "frog";
Color color = new Color();
transient String locale = "en";
transient float deathTimer;
transient String lastText = "";
transient float textFadeTime;
transient private Unit lastReadUnit = Nulls.unit;
transient private int wrongReadUnits;
transient @Nullable Unit justSwitchFrom, justSwitchTo;
public boolean isBuilder(){
return unit.canBuild();
}
public @Nullable CoreBuild closestCore(){
return state.teams.closestCore(x, y, team);
}
public @Nullable CoreBuild core(){
return team.core();
}
/** @return largest/closest core, with largest cores getting priority */
@Nullable
public CoreBuild bestCore(){
return team.cores().min(Structs.comps(Structs.comparingInt(c -> -c.block.size), Structs.comparingFloat(c -> c.dst(x, y))));
}
public TextureRegion icon(){
//display default icon for dead players
if(dead()) return core() == null ? UnitTypes.alpha.fullIcon : ((CoreBlock)bestCore().block).unitType.fullIcon;
return unit.icon();
}
public boolean displayAmmo(){
return unit instanceof BlockUnitc || state.rules.unitAmmo;
}
public void reset(){
team = state.rules.defaultTeam;
admin = typing = false;
textFadeTime = 0f;
x = y = 0f;
if(!dead()){
unit.resetController();
unit = Nulls.unit;
}
}
@Override
public boolean isValidController(){
return isAdded();
}
@Replace
public float clipSize(){
return unit.isNull() ? 20 : unit.type.hitSize * 2f;
}
@Override
public void afterSync(){
//fix rubberbanding:
//when the player recs a unit that they JUST transitioned away from, use the new unit instead
//reason: we know the server is lying here, essentially skip the unit snapshot because we know the client's information is more recent
if(isLocal() && unit == justSwitchFrom && justSwitchFrom != null && justSwitchTo != null){
unit = justSwitchTo;
//if several snapshots have passed and this unit is still incorrect, something's wrong
if(++wrongReadUnits >= 2){
justSwitchFrom = null;
wrongReadUnits = 0;
}
}else{
justSwitchFrom = null;
justSwitchTo = null;
wrongReadUnits = 0;
}
//simulate a unit change after sync
Unit set = unit;
unit = lastReadUnit;
unit(set);
lastReadUnit = unit;
unit.aim(mouseX, mouseY);
//this is only necessary when the thing being controlled isn't synced
unit.controlWeapons(shooting, shooting);
//extra precaution, necessary for non-synced things
unit.controller(this);
}
@Override
public void update(){
if(!unit.isValid()){
clearUnit();
}
CoreBuild core;
if(!dead()){
set(unit);
unit.team(team);
deathTimer = 0;
//update some basic state to sync things
if(unit.type.canBoost){
unit.elevation = Mathf.approachDelta(unit.elevation, unit.onSolid() || boosting || (unit.isFlying() && !unit.canLand()) ? 1f : 0f, unit.type.riseSpeed);
}
}else if((core = bestCore()) != null){
//have a small delay before death to prevent the camera from jumping around too quickly
//(this is not for balance, it just looks better this way)
deathTimer += Time.delta;
if(deathTimer >= deathDelay){
//request spawn - this happens serverside only
core.requestSpawn(self());
deathTimer = 0;
}
}
textFadeTime -= Time.delta / (60 * 5);
}
public void checkSpawn(){
CoreBuild core = bestCore();
if(core != null){
core.requestSpawn(self());
}
}
@Override
public void remove(){
//clear unit upon removal
if(!unit.isNull()){
clearUnit();
}
}
public void team(Team team){
this.team = team;
unit.team(team);
}
public void clearUnit(){
unit(Nulls.unit);
}
public Unit unit(){
return unit;
}
public void unit(Unit unit){
//refuse to switch when the unit was just transitioned from
if(isLocal() && unit == justSwitchFrom && justSwitchFrom != null && justSwitchTo != null){
return;
}
if(unit == null) throw new IllegalArgumentException("Unit cannot be null. Use clearUnit() instead.");
if(this.unit == unit) return;
if(this.unit != Nulls.unit){
//un-control the old unit
this.unit.resetController();
}
this.unit = unit;
if(unit != Nulls.unit){
unit.team(team);
unit.controller(this);
//this player just became remote, snap the interpolation so it doesn't go wild
if(unit.isRemote()){
unit.snapInterpolation();
}
//reset selected block when switching units
if(!headless && isLocal()){
control.input.block = null;
}
}
Events.fire(new UnitChangeEvent(self(), unit));
}
boolean dead(){
return unit.isNull() || !unit.isValid();
}
String ip(){
return con == null ? "localhost" : con.address;
}
String uuid(){
return con == null ? "[LOCAL]" : con.uuid;
}
String usid(){
return con == null ? "[LOCAL]" : con.usid;
}
void kick(KickReason reason){
con.kick(reason);
}
void kick(KickReason reason, long duration){
con.kick(reason, duration);
}
void kick(String reason){
con.kick(reason);
}
void kick(String reason, long duration){
con.kick(reason, duration);
}
@Override
public void draw(){
if(unit != null && unit.inFogTo(Vars.player.team())) return;
Draw.z(Layer.playerName);
float z = Drawf.text();
Font font = Fonts.outline;
GlyphLayout layout = Pools.obtain(GlyphLayout.class, GlyphLayout::new);
final float nameHeight = 11;
final float textHeight = 15;
boolean ints = font.usesIntegerPositions();
font.setUseIntegerPositions(false);
font.getData().setScale(0.25f / Scl.scl(1f));
layout.setText(font, name);
if(!isLocal()){
Draw.color(0f, 0f, 0f, 0.3f);
Fill.rect(unit.x, unit.y + nameHeight - layout.height / 2, layout.width + 2, layout.height + 3);
Draw.color();
font.setColor(color);
font.draw(name, unit.x, unit.y + nameHeight, 0, Align.center, false);
if(admin){
float s = 3f;
Draw.color(color.r * 0.5f, color.g * 0.5f, color.b * 0.5f, 1f);
Draw.rect(Icon.adminSmall.getRegion(), unit.x + layout.width / 2f + 2 + 1, unit.y + nameHeight - 1.5f, s, s);
Draw.color(color);
Draw.rect(Icon.adminSmall.getRegion(), unit.x + layout.width / 2f + 2 + 1, unit.y + nameHeight - 1f, s, s);
}
}
if(Core.settings.getBool("playerchat") && ((textFadeTime > 0 && lastText != null) || typing)){
String text = textFadeTime <= 0 || lastText == null ? "[lightgray]" + Strings.animated(Time.time, 4, 15f, ".") : lastText;
float width = 100f;
float visualFadeTime = 1f - Mathf.curve(1f - textFadeTime, 0.9f);
font.setColor(1f, 1f, 1f, textFadeTime <= 0 || lastText == null ? 1f : visualFadeTime);
layout.setText(font, text, Color.white, width, Align.bottom, true);
Draw.color(0f, 0f, 0f, 0.3f * (textFadeTime <= 0 || lastText == null ? 1f : visualFadeTime));
Fill.rect(unit.x, unit.y + textHeight + layout.height - layout.height / 2f, layout.width + 2, layout.height + 3);
font.draw(text, unit.x - width / 2f, unit.y + textHeight + layout.height, width, Align.center, true);
}
Draw.reset();
Pools.free(layout);
font.getData().setScale(1f);
font.setColor(Color.white);
font.setUseIntegerPositions(ints);
Draw.z(z);
}
/** @return name with a markup color prefix */
String coloredName(){
return "[#" + color.toString().toUpperCase() + "]" + name;
}
void sendMessage(String text){
if(isLocal()){
if(ui != null){
ui.chatfrag.addMessage(text);
}
}else{
Call.sendMessage(con, text, null, null);
}
}
void sendMessage(String text, Player from){
sendMessage(text, from, null);
}
void sendMessage(String text, Player from, String unformatted){
if(isLocal()){
if(ui != null){
ui.chatfrag.addMessage(text);
}
}else{
Call.sendMessage(con, text, unformatted, from);
}
}
PlayerInfo getInfo(){
if(isLocal()){
throw new IllegalArgumentException("Local players cannot be traced and do not have info.");
}else{
return netServer.admins.getInfo(uuid());
}
}
}