Added offline avatar
This commit is contained in:
parent
d7e7e46eaa
commit
37d8fe1c31
12 changed files with 654 additions and 3 deletions
|
@ -4,6 +4,7 @@ import cr0s.warpdrive.config.WarpDriveConfig;
|
|||
import cr0s.warpdrive.event.EMPReceiver;
|
||||
import cr0s.warpdrive.event.ItemHandler;
|
||||
import cr0s.warpdrive.event.LivingHandler;
|
||||
import cr0s.warpdrive.event.PlayerHandler;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
@ -126,6 +127,7 @@ public class CommonProxy {
|
|||
// event handlers
|
||||
MinecraftForge.EVENT_BUS.register(new ItemHandler());
|
||||
MinecraftForge.EVENT_BUS.register(new LivingHandler());
|
||||
MinecraftForge.EVENT_BUS.register(new PlayerHandler());
|
||||
MinecraftForge.EVENT_BUS.register(EMPReceiver.class);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import net.minecraft.block.Block;
|
|||
import net.minecraft.block.BlockLiquid;
|
||||
import net.minecraft.block.material.Material;
|
||||
import net.minecraft.block.state.IBlockState;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.settings.KeyBinding;
|
||||
import net.minecraft.command.CommandException;
|
||||
import net.minecraft.command.EntitySelector;
|
||||
|
@ -31,6 +32,7 @@ import net.minecraft.item.ItemStack;
|
|||
import net.minecraft.nbt.CompressedStreamTools;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.tileentity.TileEntitySkull;
|
||||
import net.minecraft.util.EnumBlockRenderType;
|
||||
import net.minecraft.util.EnumFacing;
|
||||
import net.minecraft.util.math.AxisAlignedBB;
|
||||
|
@ -53,7 +55,6 @@ import net.minecraftforge.common.DimensionManager;
|
|||
import net.minecraftforge.common.property.IExtendedBlockState;
|
||||
import net.minecraftforge.common.property.IUnlistedProperty;
|
||||
import net.minecraftforge.fml.common.FMLCommonHandler;
|
||||
import net.minecraftforge.fml.common.Optional;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
|
@ -72,7 +73,13 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.lwjgl.input.Keyboard;
|
||||
import org.lwjgl.input.Mouse;
|
||||
|
@ -80,6 +87,8 @@ import org.lwjgl.input.Mouse;
|
|||
import net.minecraftforge.fluids.Fluid;
|
||||
import net.minecraftforge.fluids.FluidRegistry;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
/**
|
||||
* Common static methods
|
||||
*/
|
||||
|
@ -93,6 +102,9 @@ public class Commons {
|
|||
EnumBlockRenderType.MODEL
|
||||
);
|
||||
|
||||
private static final ExecutorService THREAD_POOL = new ThreadPoolExecutor(0, 2, 1L, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
|
||||
private static final CopyOnWriteArraySet<UUID> uuidGameProfileFilling = new CopyOnWriteArraySet<>();
|
||||
|
||||
private static Method methodThrowable_getStackTraceElement;
|
||||
|
||||
static {
|
||||
|
@ -1023,6 +1035,60 @@ public class Commons {
|
|||
return server.getPlayerList().getPlayerByUsername(namePlayer);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static EntityPlayerMP getOnlinePlayerByUUID(@Nonnull final UUID uuidPlayer) {
|
||||
final MinecraftServer server = FMLCommonHandler.instance().getMinecraftServerInstance();
|
||||
assert server != null;
|
||||
return server.getPlayerList().getPlayerByUUID(uuidPlayer);
|
||||
}
|
||||
|
||||
public interface ProfilePropertiesAvailableCallback {
|
||||
void profilePropertiesAvailable(@Nonnull final GameProfile gameProfile);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static GameProfile getGameProfile(@Nonnull final UUID uuidPlayer, @Nonnull final String namePlayer,
|
||||
final ProfilePropertiesAvailableCallback profilePropertiesAvailableCallback) {
|
||||
// validate context
|
||||
if ( TileEntitySkull.profileCache == null
|
||||
|| TileEntitySkull.sessionService == null ) {
|
||||
return new GameProfile(uuidPlayer, namePlayer);
|
||||
}
|
||||
|
||||
// check the cache first
|
||||
final GameProfile gameProfileCached = TileEntitySkull.profileCache.getProfileByUUID(uuidPlayer);
|
||||
if ( gameProfileCached != null
|
||||
&& !gameProfileCached.getProperties().isEmpty() ) {
|
||||
if (profilePropertiesAvailableCallback != null) {
|
||||
profilePropertiesAvailableCallback.profilePropertiesAvailable(gameProfileCached);
|
||||
}
|
||||
return gameProfileCached;
|
||||
}
|
||||
final GameProfile gameProfileEmpty = gameProfileCached != null ? gameProfileCached : new GameProfile(uuidPlayer, namePlayer);
|
||||
// return directly if properties aren't required or filling is already ongoing
|
||||
if ( profilePropertiesAvailableCallback == null
|
||||
|| uuidGameProfileFilling.contains(uuidPlayer) ) {
|
||||
return gameProfileEmpty;
|
||||
}
|
||||
|
||||
// return an empty profile while a thread will query the server
|
||||
uuidGameProfileFilling.add(uuidPlayer);
|
||||
THREAD_POOL.submit(() -> {
|
||||
final GameProfile gameProfileFilled = TileEntitySkull.sessionService.fillProfileProperties(gameProfileEmpty, true);
|
||||
|
||||
// return to main thread before updating our cache and informing caller
|
||||
Minecraft.getMinecraft().addScheduledTask(() -> {
|
||||
if (gameProfileEmpty != gameProfileFilled) {// when successful, a new object is created, so we might as well use it
|
||||
TileEntitySkull.profileCache.addEntry(gameProfileFilled);
|
||||
}
|
||||
profilePropertiesAvailableCallback.profilePropertiesAvailable(gameProfileFilled);
|
||||
uuidGameProfileFilling.remove(uuidPlayer);
|
||||
});
|
||||
});
|
||||
|
||||
return gameProfileEmpty;
|
||||
}
|
||||
|
||||
public static int colorARGBtoInt(final int alpha, final int red, final int green, final int blue) {
|
||||
return (clamp(0, 255, alpha) << 24)
|
||||
+ (clamp(0, 255, red ) << 16)
|
||||
|
|
|
@ -127,6 +127,7 @@ import cr0s.warpdrive.data.EnumHullPlainType;
|
|||
import cr0s.warpdrive.data.EnumTier;
|
||||
import cr0s.warpdrive.entity.EntityLaserExploder;
|
||||
import cr0s.warpdrive.entity.EntityNPC;
|
||||
import cr0s.warpdrive.entity.EntityOfflineAvatar;
|
||||
import cr0s.warpdrive.entity.EntityParticleBunch;
|
||||
import cr0s.warpdrive.entity.EntitySeat;
|
||||
import cr0s.warpdrive.event.ChatHandler;
|
||||
|
@ -1139,6 +1140,13 @@ public class WarpDrive {
|
|||
.build();
|
||||
event.getRegistry().register(entityEntry);
|
||||
|
||||
entityEntry = EntityEntryBuilder.create()
|
||||
.entity(EntityOfflineAvatar.class).factory(EntityOfflineAvatar::new)
|
||||
.tracker(200, 1, false)
|
||||
.id("entity_offline_avatar", WarpDriveConfig.G_ENTITY_OFFLINE_AVATAR_ID).name("EntityOfflineAvatar")
|
||||
.build();
|
||||
event.getRegistry().register(entityEntry);
|
||||
|
||||
entityEntry = EntityEntryBuilder.create()
|
||||
.entity(EntitySeat.class).factory(EntitySeat::new)
|
||||
.tracker(200, 1, false)
|
||||
|
|
|
@ -6,6 +6,7 @@ import cr0s.warpdrive.api.IBlockBase;
|
|||
import cr0s.warpdrive.api.IItemBase;
|
||||
import cr0s.warpdrive.block.breathing.BlockColorAirShield;
|
||||
import cr0s.warpdrive.entity.EntityNPC;
|
||||
import cr0s.warpdrive.entity.EntityOfflineAvatar;
|
||||
import cr0s.warpdrive.entity.EntityParticleBunch;
|
||||
import cr0s.warpdrive.event.ClientHandler;
|
||||
import cr0s.warpdrive.event.ModelBakeEventHandler;
|
||||
|
@ -13,6 +14,7 @@ import cr0s.warpdrive.event.TooltipHandler;
|
|||
import cr0s.warpdrive.render.ClientCameraHandler;
|
||||
import cr0s.warpdrive.render.CustomModelLoaderProjector;
|
||||
import cr0s.warpdrive.render.RenderEntityNPC;
|
||||
import cr0s.warpdrive.render.RenderEntityOfflineAvatar;
|
||||
import cr0s.warpdrive.render.RenderEntityParticleBunch;
|
||||
import cr0s.warpdrive.render.RenderOverlayAir;
|
||||
import cr0s.warpdrive.render.RenderOverlayCamera;
|
||||
|
@ -67,6 +69,13 @@ public class ClientProxy extends CommonProxy {
|
|||
return new RenderEntityNPC(manager);
|
||||
}
|
||||
});
|
||||
RenderingRegistry.registerEntityRenderingHandler(EntityOfflineAvatar.class, new IRenderFactory<EntityOfflineAvatar>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
public Render<EntityOfflineAvatar> createRenderFor(final RenderManager manager) {
|
||||
return new RenderEntityOfflineAvatar(manager);
|
||||
}
|
||||
});
|
||||
RenderingRegistry.registerEntityRenderingHandler(EntityParticleBunch.class, new IRenderFactory<EntityParticleBunch>() {
|
||||
@Nonnull
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package cr0s.warpdrive.client;
|
||||
|
||||
import cr0s.warpdrive.Commons;
|
||||
import cr0s.warpdrive.WarpDrive;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
// Collection of texture references supporting offline players, unlike net.minecraft.client.network.NetworkPlayerInfo
|
||||
public class PlayerTextureManager {
|
||||
|
||||
private static final PlayerTextureManager INSTANCE = new PlayerTextureManager();
|
||||
private static HashMap<UUID, PlayerTextures> mapIdPlayerTextures = new HashMap<>(0);
|
||||
public static final ResourceLocation RESOURCE_LOCATION_DEFAULT = new ResourceLocation("textures/entity/steve.png");
|
||||
public static final ResourceLocation RESOURCE_LOCATION_SLIM = new ResourceLocation("textures/entity/alex.png");
|
||||
|
||||
private static class PlayerTextures {
|
||||
public ResourceLocation resourceLocationSkin;
|
||||
public String skinType = "default";
|
||||
public ResourceLocation resourceLocationCape;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static ResourceLocation getPlayerSkin(@Nonnull final UUID uuidPlayer, @Nonnull final String namePlayer) {
|
||||
{
|
||||
final PlayerTextures playerTextures = mapIdPlayerTextures.get(uuidPlayer);
|
||||
if (playerTextures != null) {
|
||||
if (playerTextures.resourceLocationSkin == null) {
|
||||
return RESOURCE_LOCATION_DEFAULT;
|
||||
}
|
||||
return playerTextures.resourceLocationSkin;
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (INSTANCE) {
|
||||
final PlayerTextures playerTextures = new PlayerTextures();
|
||||
|
||||
// we clone to avoid synchronization issues without impacting CPU. Hence there's more memory load initially, but less lag during rendering.
|
||||
// note: clone is being picky, so we do our own cloning
|
||||
final HashMap<UUID, PlayerTextures> mapIdTypeTextureNew = new HashMap<>(mapIdPlayerTextures.size() + 1);
|
||||
mapIdTypeTextureNew.putAll(mapIdPlayerTextures);
|
||||
mapIdTypeTextureNew.put(uuidPlayer, playerTextures);
|
||||
mapIdPlayerTextures = mapIdTypeTextureNew;
|
||||
|
||||
Commons.getGameProfile(uuidPlayer, namePlayer, (gameProfileFilled) ->
|
||||
Minecraft.getMinecraft().getSkinManager().loadProfileTextures(gameProfileFilled, (type, location, profileTexture) -> {
|
||||
switch (type) {
|
||||
case SKIN:
|
||||
playerTextures.resourceLocationSkin = location;
|
||||
final String skinType = profileTexture.getMetadata("model");
|
||||
if (skinType == null) {
|
||||
playerTextures.skinType = "default";
|
||||
} else {
|
||||
playerTextures.skinType = skinType;
|
||||
}
|
||||
break;
|
||||
|
||||
case CAPE:
|
||||
playerTextures.resourceLocationCape = location;
|
||||
break;
|
||||
}
|
||||
}, true) );
|
||||
|
||||
return RESOURCE_LOCATION_DEFAULT;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -169,7 +169,8 @@ public class WarpDriveConfig {
|
|||
public static int G_ENTITY_PARTICLE_BUNCH_ID = 244;
|
||||
public static int G_ENTITY_LASER_EXPLODER_ID = 245;
|
||||
public static int G_ENTITY_NPC_ID = 246;
|
||||
public static int G_ENTITY_SEAT_ID = 247;
|
||||
public static int G_ENTITY_OFFLINE_AVATAR_ID = 247;
|
||||
public static int G_ENTITY_SEAT_ID = 248;
|
||||
|
||||
public static final int LUA_SCRIPTS_NONE = 0;
|
||||
public static final int LUA_SCRIPTS_TEMPLATES = 1;
|
||||
|
@ -302,6 +303,15 @@ public class WarpDriveConfig {
|
|||
public static int BIOMETRIC_SCANNER_DURATION_TICKS = 100;
|
||||
public static int BIOMETRIC_SCANNER_RANGE_BLOCKS = 3;
|
||||
|
||||
// Offline avatar
|
||||
public static boolean OFFLINE_AVATAR_ENABLE = true;
|
||||
public static boolean OFFLINE_AVATAR_CREATE_ONLY_ABOARD_SHIPS = true;
|
||||
public static boolean OFFLINE_AVATAR_FORGET_ON_DEATH = false;
|
||||
public static float OFFLINE_AVATAR_MODEL_SCALE = 0.5F;
|
||||
public static boolean OFFLINE_AVATAR_ALWAYS_RENDER_NAME_TAG = false;
|
||||
public static float OFFLINE_AVATAR_MIN_RANGE_FOR_REMOVAL = 1.0F;
|
||||
public static float OFFLINE_AVATAR_MAX_RANGE_FOR_REMOVAL = 5.0F;
|
||||
|
||||
// Radar
|
||||
public static int RADAR_MAX_ENERGY_STORED = 100000000; // 100kk eU
|
||||
public static int RADAR_SCAN_MIN_ENERGY_COST = 10000;
|
||||
|
@ -797,6 +807,8 @@ public class WarpDriveConfig {
|
|||
config.get("general", "entity_laser_exploder_id", G_ENTITY_LASER_EXPLODER_ID, "Entity laser exploder ID").getInt());
|
||||
G_ENTITY_NPC_ID = Commons.clamp(Integer.MIN_VALUE, Integer.MAX_VALUE,
|
||||
config.get("general", "entity_NPC_id", G_ENTITY_NPC_ID, "Entity NPC ID").getInt());
|
||||
G_ENTITY_OFFLINE_AVATAR_ID = Commons.clamp(Integer.MIN_VALUE, Integer.MAX_VALUE,
|
||||
config.get("general", "entity_offline_avatar_id", G_ENTITY_OFFLINE_AVATAR_ID, "Entity offline avatar ID").getInt());
|
||||
G_ENTITY_SEAT_ID = Commons.clamp(Integer.MIN_VALUE, Integer.MAX_VALUE,
|
||||
config.get("general", "entity_seat_id", G_ENTITY_SEAT_ID, "Entity seat ID").getInt());
|
||||
|
||||
|
@ -1034,6 +1046,22 @@ public class WarpDriveConfig {
|
|||
config.get("jump_gate", "size_max_per_side_by_tier", JUMP_GATE_SIZE_MAX_PER_SIDE_BY_TIER, "Maximum jump gate size on each axis in blocks, for a given tier").getIntList();
|
||||
clampByTier(1, Integer.MAX_VALUE, JUMP_GATE_SIZE_MAX_PER_SIDE_BY_TIER);
|
||||
|
||||
// Offline avatar
|
||||
OFFLINE_AVATAR_ENABLE =
|
||||
config.get("offline_avatar", "enable", OFFLINE_AVATAR_ENABLE, "Enable creation of offline avatars to follow ship movements. This only disable creating new ones.").getBoolean(OFFLINE_AVATAR_ENABLE);
|
||||
OFFLINE_AVATAR_CREATE_ONLY_ABOARD_SHIPS =
|
||||
config.get("offline_avatar", "create_only_aboard_ships", OFFLINE_AVATAR_CREATE_ONLY_ABOARD_SHIPS, "Only create an offline avatar when player disconnects while inside a ship. Disabling may cause lag in spawn areas...").getBoolean(OFFLINE_AVATAR_CREATE_ONLY_ABOARD_SHIPS);
|
||||
OFFLINE_AVATAR_FORGET_ON_DEATH =
|
||||
config.get("offline_avatar", "forget_on_death", OFFLINE_AVATAR_FORGET_ON_DEATH, "Enable to forget current avatar position when it's killed, or disable player teleportation to last known avatar's position").getBoolean(OFFLINE_AVATAR_FORGET_ON_DEATH);
|
||||
OFFLINE_AVATAR_MODEL_SCALE = (float) Commons.clamp(0.20D, 2.00D,
|
||||
config.get("offline_avatar", "model_scale", OFFLINE_AVATAR_MODEL_SCALE, "Scale of offline avatar compared to a normal player").getDouble(OFFLINE_AVATAR_MODEL_SCALE));
|
||||
OFFLINE_AVATAR_ALWAYS_RENDER_NAME_TAG =
|
||||
config.get("offline_avatar", "always_render_name_tag", OFFLINE_AVATAR_ALWAYS_RENDER_NAME_TAG, "Should avatar name tag always be visible?").getBoolean(OFFLINE_AVATAR_ALWAYS_RENDER_NAME_TAG);
|
||||
OFFLINE_AVATAR_MIN_RANGE_FOR_REMOVAL = (float) Commons.clamp(0.10D, 10.00D,
|
||||
config.get("offline_avatar", "min_range_for_removal", OFFLINE_AVATAR_MIN_RANGE_FOR_REMOVAL, "Minimum range between a player and their avatar to consider it for removal (i.e. ensuring connection was successful)").getDouble(OFFLINE_AVATAR_MIN_RANGE_FOR_REMOVAL));
|
||||
OFFLINE_AVATAR_MAX_RANGE_FOR_REMOVAL = (float) Commons.clamp(Math.max(3.00D, OFFLINE_AVATAR_MIN_RANGE_FOR_REMOVAL), Float.MAX_VALUE,
|
||||
config.get("offline_avatar", "max_range_for_removal", OFFLINE_AVATAR_MAX_RANGE_FOR_REMOVAL, "Maximum range between a player and his/her avatar to consider it for removal").getDouble(OFFLINE_AVATAR_MAX_RANGE_FOR_REMOVAL));
|
||||
|
||||
// Radar
|
||||
RADAR_MAX_ENERGY_STORED = Commons.clamp(0, Integer.MAX_VALUE,
|
||||
config.get("radar", "max_energy_stored", RADAR_MAX_ENERGY_STORED, "maximum energy stored").getInt());
|
||||
|
|
189
src/main/java/cr0s/warpdrive/data/OfflineAvatarManager.java
Normal file
189
src/main/java/cr0s/warpdrive/data/OfflineAvatarManager.java
Normal file
|
@ -0,0 +1,189 @@
|
|||
package cr0s.warpdrive.data;
|
||||
|
||||
import cr0s.warpdrive.Commons;
|
||||
import cr0s.warpdrive.WarpDrive;
|
||||
import cr0s.warpdrive.config.WarpDriveConfig;
|
||||
import cr0s.warpdrive.entity.EntityOfflineAvatar;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.UUID;
|
||||
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.inventory.EntityEquipmentSlot;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import net.minecraftforge.common.util.Constants;
|
||||
|
||||
public class OfflineAvatarManager {
|
||||
|
||||
private static final HashMap<UUID, GlobalPosition> registry = new HashMap<>(512);
|
||||
|
||||
public static void update(@Nonnull final EntityOfflineAvatar entityOfflineAvatar) {
|
||||
// validate context
|
||||
if (!Commons.isSafeThread()) {
|
||||
WarpDrive.logger.error(String.format("Non-threadsafe call to OfflineAvatarManager:update outside main thread, for %s",
|
||||
entityOfflineAvatar ));
|
||||
return;
|
||||
}
|
||||
if (entityOfflineAvatar.getPlayerUUID() == null) {
|
||||
WarpDrive.logger.error(String.format("Ignoring update for invalid EntityOfflineAvatar with no UUID %s",
|
||||
entityOfflineAvatar ));
|
||||
return;
|
||||
}
|
||||
|
||||
// add new entry
|
||||
// or update existing entry
|
||||
GlobalPosition globalPosition = registry.get(entityOfflineAvatar.getPlayerUUID());
|
||||
if ( globalPosition == null
|
||||
|| globalPosition.dimensionId != entityOfflineAvatar.world.provider.getDimension()
|
||||
|| globalPosition.x != (int) Math.floor(entityOfflineAvatar.posX)
|
||||
|| globalPosition.y != (int) Math.floor(entityOfflineAvatar.posY)
|
||||
|| globalPosition.z != (int) Math.floor(entityOfflineAvatar.posZ) ) {
|
||||
globalPosition = new GlobalPosition(entityOfflineAvatar);
|
||||
registry.put(entityOfflineAvatar.getPlayerUUID(), globalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public static void remove(@Nonnull final EntityOfflineAvatar entityOfflineAvatar) {
|
||||
// validate context
|
||||
if (!Commons.isSafeThread()) {
|
||||
WarpDrive.logger.error(String.format("Non-threadsafe call to OfflineAvatarManager:remove outside main thread, for %s",
|
||||
entityOfflineAvatar ));
|
||||
return;
|
||||
}
|
||||
if (entityOfflineAvatar.getPlayerUUID() == null) {
|
||||
WarpDrive.logger.error(String.format("Ignoring removal for invalid EntityOfflineAvatar with no UUID %s",
|
||||
entityOfflineAvatar ));
|
||||
return;
|
||||
}
|
||||
|
||||
// remove existing entry, if coordinates are matching
|
||||
final GlobalPosition globalPosition = registry.get(entityOfflineAvatar.getPlayerUUID());
|
||||
if ( globalPosition != null
|
||||
&& globalPosition.dimensionId == entityOfflineAvatar.world.provider.getDimension()
|
||||
&& globalPosition.x == (int) Math.floor(entityOfflineAvatar.posX)
|
||||
&& globalPosition.y == (int) Math.floor(entityOfflineAvatar.posY)
|
||||
&& globalPosition.z == (int) Math.floor(entityOfflineAvatar.posZ) ) {
|
||||
registry.remove(entityOfflineAvatar.getPlayerUUID());
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static GlobalPosition get(@Nonnull final UUID uuidPlayer) {
|
||||
// validate context
|
||||
if (!Commons.isSafeThread()) {
|
||||
WarpDrive.logger.error(String.format("Non-threadsafe call to OfflineAvatarManager:get outside main thread, for %s",
|
||||
uuidPlayer ));
|
||||
return null;
|
||||
}
|
||||
|
||||
// get existing entry
|
||||
return registry.get(uuidPlayer);
|
||||
}
|
||||
|
||||
public static void readFromNBT(@Nullable final NBTTagCompound tagCompound) {
|
||||
if ( tagCompound == null
|
||||
|| !tagCompound.hasKey("offlineAvatars") ) {
|
||||
registry.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// read all entries in a pre-build local collections using known stats to avoid re-allocations
|
||||
final NBTTagList tagList = tagCompound.getTagList("offlineAvatars", Constants.NBT.TAG_COMPOUND);
|
||||
final HashMap<UUID, GlobalPosition> registryLocal = new HashMap<>(tagList.tagCount());
|
||||
for (int index = 0; index < tagList.tagCount(); index++) {
|
||||
final NBTTagCompound tagCompoundItem = tagList.getCompoundTagAt(index);
|
||||
final UUID uuid = tagCompoundItem.getUniqueId("");
|
||||
final GlobalPosition globalPosition = new GlobalPosition(tagCompoundItem);
|
||||
registryLocal.put(uuid, globalPosition);
|
||||
}
|
||||
|
||||
// transfer to main one
|
||||
registry.clear();
|
||||
registry.putAll(registryLocal);
|
||||
for (final Entry<UUID, GlobalPosition> entry : registryLocal.entrySet()) {
|
||||
registry.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeToNBT(@Nonnull final NBTTagCompound tagCompound) {
|
||||
final NBTTagList tagList = new NBTTagList();
|
||||
for (final Entry<UUID, GlobalPosition> entry : registry.entrySet()) {
|
||||
final NBTTagCompound tagCompoundItem = new NBTTagCompound();
|
||||
tagCompoundItem.setUniqueId("", entry.getKey());
|
||||
entry.getValue().writeToNBT(tagCompoundItem);
|
||||
tagList.appendTag(tagCompoundItem);
|
||||
}
|
||||
tagCompound.setTag("offlineAvatars", tagList);
|
||||
}
|
||||
|
||||
public static void onPlayerLoggedOut(@Nonnull final EntityPlayer entityPlayer) {
|
||||
// skip dead players
|
||||
if (entityPlayer.isDead) {
|
||||
return;
|
||||
}
|
||||
|
||||
// skip players away from a ship
|
||||
final World world = entityPlayer.world;
|
||||
final BlockPos blockPos = entityPlayer.getPosition();
|
||||
if (WarpDriveConfig.OFFLINE_AVATAR_CREATE_ONLY_ABOARD_SHIPS) {
|
||||
final GlobalRegion globalRegionNearestShip = GlobalRegionManager.getNearest(EnumGlobalRegionType.SHIP, world, blockPos);
|
||||
if ( globalRegionNearestShip == null
|
||||
|| !globalRegionNearestShip.contains(blockPos) ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// spawn an offline avatar entity at location
|
||||
WarpDrive.logger.debug(String.format("Spawning offline avatar for %s",
|
||||
entityPlayer ));
|
||||
final EntityOfflineAvatar entityOfflineAvatar = new EntityOfflineAvatar(world);
|
||||
entityOfflineAvatar.setPosition(blockPos.getX() + 0.5D, blockPos.getY() + 0.1D, blockPos.getZ() + 0.5D);
|
||||
entityOfflineAvatar.setCustomNameTag(entityPlayer.getDisplayNameString());
|
||||
entityOfflineAvatar.setPlayer(entityPlayer.getUniqueID(), entityPlayer.getName());
|
||||
// copy equipment with a marker to remember those aren't 'legit' items
|
||||
for (final EntityEquipmentSlot entityEquipmentSlot : EntityEquipmentSlot.values()) {
|
||||
final ItemStack itemStack = entityPlayer.getItemStackFromSlot(entityEquipmentSlot).copy();
|
||||
if (!itemStack.isEmpty()) {
|
||||
if (!itemStack.hasTagCompound()) {
|
||||
itemStack.setTagCompound(new NBTTagCompound());
|
||||
}
|
||||
assert itemStack.getTagCompound() != null;
|
||||
itemStack.getTagCompound().setBoolean("isFakeItem", true);
|
||||
entityOfflineAvatar.setItemStackToSlot(entityEquipmentSlot, itemStack);
|
||||
entityOfflineAvatar.setDropChance(entityEquipmentSlot, 0.0F);
|
||||
}
|
||||
}
|
||||
world.spawnEntity(entityOfflineAvatar);
|
||||
}
|
||||
|
||||
public static void onPlayerLoggedIn(@Nonnull final EntityPlayer entityPlayer) {
|
||||
assert !entityPlayer.isAddedToWorld();
|
||||
|
||||
// skip if we have no record
|
||||
final GlobalPosition globalPosition = registry.get(entityPlayer.getUniqueID());
|
||||
if (globalPosition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// skip if player is already close by
|
||||
if (globalPosition.dimensionId == entityPlayer.dimension) {
|
||||
final double distance = entityPlayer.getDistance(globalPosition.x + 0.5D, globalPosition.y, globalPosition.z + 0.5D);
|
||||
if (distance < WarpDriveConfig.OFFLINE_AVATAR_MAX_RANGE_FOR_REMOVAL) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// teleport player
|
||||
// note: this is done before server loads the player's world
|
||||
entityPlayer.dimension = globalPosition.dimensionId;
|
||||
entityPlayer.setPosition(globalPosition.x, globalPosition.y, globalPosition.z);
|
||||
}
|
||||
}
|
177
src/main/java/cr0s/warpdrive/entity/EntityOfflineAvatar.java
Normal file
177
src/main/java/cr0s/warpdrive/entity/EntityOfflineAvatar.java
Normal file
|
@ -0,0 +1,177 @@
|
|||
package cr0s.warpdrive.entity;
|
||||
|
||||
import cr0s.warpdrive.Commons;
|
||||
import cr0s.warpdrive.WarpDrive;
|
||||
import cr0s.warpdrive.config.WarpDriveConfig;
|
||||
import cr0s.warpdrive.data.OfflineAvatarManager;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import net.minecraft.entity.EntityLiving;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.datasync.DataParameter;
|
||||
import net.minecraft.network.datasync.DataSerializers;
|
||||
import net.minecraft.network.datasync.EntityDataManager;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
|
||||
public class EntityOfflineAvatar extends EntityLiving {
|
||||
|
||||
// persistent properties
|
||||
private static final DataParameter<Optional<UUID>> DATA_PLAYER_UUID = EntityDataManager.createKey(EntityOfflineAvatar.class, DataSerializers.OPTIONAL_UNIQUE_ID);
|
||||
private static final DataParameter<String> DATA_PLAYER_NAME = EntityDataManager.createKey(EntityOfflineAvatar.class, DataSerializers.STRING);
|
||||
|
||||
// computed properties
|
||||
private boolean isDirtyGlobalPosition = true;
|
||||
private int tickUpdateGlobalPosition = 0;
|
||||
|
||||
public EntityOfflineAvatar(@Nonnull final World world) {
|
||||
super(world);
|
||||
|
||||
setCanPickUpLoot(false);
|
||||
setNoAI(true);
|
||||
setCustomNameTag("Offline avatar");
|
||||
setAlwaysRenderNameTag(WarpDriveConfig.OFFLINE_AVATAR_ALWAYS_RENDER_NAME_TAG);
|
||||
setSize(width * WarpDriveConfig.OFFLINE_AVATAR_MODEL_SCALE, height * WarpDriveConfig.OFFLINE_AVATAR_MODEL_SCALE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entityInit() {
|
||||
super.entityInit();
|
||||
|
||||
dataManager.register(DATA_PLAYER_UUID, Optional.absent());
|
||||
dataManager.register(DATA_PLAYER_NAME, "");
|
||||
}
|
||||
|
||||
public void setPlayer(@Nonnull final UUID uuidPlayer, @Nonnull final String namePlayer) {
|
||||
dataManager.set(DATA_PLAYER_UUID, Optional.of(uuidPlayer));
|
||||
dataManager.set(DATA_PLAYER_NAME, namePlayer);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public UUID getPlayerUUID() {
|
||||
return dataManager.get(DATA_PLAYER_UUID).orNull();
|
||||
}
|
||||
|
||||
public String getPlayerName() {
|
||||
return dataManager.get(DATA_PLAYER_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate() {
|
||||
super.onUpdate();
|
||||
|
||||
if (world.isRemote) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update registry
|
||||
if (isDirtyGlobalPosition) {
|
||||
tickUpdateGlobalPosition = 0;
|
||||
}
|
||||
tickUpdateGlobalPosition--;
|
||||
if (tickUpdateGlobalPosition <= 0) {
|
||||
tickUpdateGlobalPosition = WarpDriveConfig.G_REGISTRY_UPDATE_INTERVAL_TICKS;
|
||||
isDirtyGlobalPosition = false;
|
||||
|
||||
final UUID uuidPlayer = getPlayerUUID();
|
||||
if (uuidPlayer == null) {
|
||||
// cleanup invalid entities
|
||||
if (ticksExisted > 5) {
|
||||
WarpDrive.logger.error(String.format("Removing invalid EntityOfflineAvatar with no UUID %s",
|
||||
this ));
|
||||
setDead();
|
||||
}
|
||||
|
||||
} else {
|
||||
// cleanup online players
|
||||
final EntityPlayer entityPlayer = Commons.getOnlinePlayerByUUID(uuidPlayer);
|
||||
if ( entityPlayer != null
|
||||
&& world.provider.getDimension() == entityPlayer.world.provider.getDimension()
|
||||
&& isInRange(entityPlayer) ) {// (actually online in close proximity)
|
||||
OfflineAvatarManager.remove(this);
|
||||
setDead();
|
||||
|
||||
} else {// (actually offline or far away)
|
||||
OfflineAvatarManager.update(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInRange(@Nonnull final EntityPlayer entityPlayer) {
|
||||
final float distance = entityPlayer.getDistance(this);
|
||||
return distance >= WarpDriveConfig.OFFLINE_AVATAR_MIN_RANGE_FOR_REMOVAL
|
||||
&& distance <= WarpDriveConfig.OFFLINE_AVATAR_MAX_RANGE_FOR_REMOVAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDead() {
|
||||
super.setDead();
|
||||
|
||||
if (WarpDriveConfig.OFFLINE_AVATAR_FORGET_ON_DEATH) {
|
||||
OfflineAvatarManager.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRenderSizeModifier() {
|
||||
return WarpDriveConfig.OFFLINE_AVATAR_MODEL_SCALE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readEntityFromNBT(@Nonnull final NBTTagCompound tagCompound) {
|
||||
super.readEntityFromNBT(tagCompound);
|
||||
|
||||
final UUID uuidPlayer = tagCompound.getUniqueId("player");
|
||||
final String namePlayer = tagCompound.getString("playerName");
|
||||
if ( uuidPlayer == null
|
||||
|| namePlayer.isEmpty() ) {
|
||||
WarpDrive.logger.error(String.format("Removing on reading invalid offline avatar in %s",
|
||||
tagCompound ));
|
||||
setDead();
|
||||
return;
|
||||
}
|
||||
setPlayer(uuidPlayer, namePlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEntityToNBT(@Nonnull final NBTTagCompound tagCompound) {
|
||||
super.writeEntityToNBT(tagCompound);
|
||||
|
||||
final UUID uuidPlayer = getPlayerUUID();
|
||||
if (uuidPlayer == null) {
|
||||
WarpDrive.logger.error(String.format("Removing on writing invalid offline avatar in %s",
|
||||
tagCompound ));
|
||||
setDead();
|
||||
return;
|
||||
}
|
||||
tagCompound.setUniqueId("player", uuidPlayer);
|
||||
tagCompound.setString("playerName", getPlayerName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLivingUpdate() {
|
||||
super.onLivingUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isMovementBlocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getAlwaysRenderNameTagForRender() {
|
||||
return super.getAlwaysRenderNameTagForRender();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getAlwaysRenderNameTag() {
|
||||
return super.getAlwaysRenderNameTag();
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import cr0s.warpdrive.config.WarpDriveConfig;
|
|||
import cr0s.warpdrive.data.CelestialObjectManager;
|
||||
import cr0s.warpdrive.data.ChunkData;
|
||||
import cr0s.warpdrive.data.GlobalRegionManager;
|
||||
import cr0s.warpdrive.data.OfflineAvatarManager;
|
||||
import cr0s.warpdrive.data.StateAir;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
@ -59,6 +60,7 @@ public class ChunkHandler {
|
|||
final String filename = String.format("%s/%s.dat", event.getWorld().getSaveHandler().getWorldDirectory().getPath(), WarpDrive.MODID);
|
||||
final NBTTagCompound tagCompound = Commons.readNBTFromFile(filename);
|
||||
GlobalRegionManager.readFromNBT(tagCompound);
|
||||
OfflineAvatarManager.readFromNBT(tagCompound);
|
||||
|
||||
// enforce vanilla's WorldBorder diameter consistency
|
||||
final WorldBorder worldBorder = event.getWorld().getWorldBorder();
|
||||
|
@ -174,6 +176,7 @@ public class ChunkHandler {
|
|||
final String filename = String.format("%s/%s.dat", event.getWorld().getSaveHandler().getWorldDirectory().getPath(), WarpDrive.MODID);
|
||||
final NBTTagCompound tagCompound = new NBTTagCompound();
|
||||
GlobalRegionManager.writeToNBT(tagCompound);
|
||||
OfflineAvatarManager.writeToNBT(tagCompound);
|
||||
Commons.writeNBTToFile(filename, tagCompound);
|
||||
}
|
||||
|
||||
|
|
26
src/main/java/cr0s/warpdrive/event/PlayerHandler.java
Normal file
26
src/main/java/cr0s/warpdrive/event/PlayerHandler.java
Normal file
|
@ -0,0 +1,26 @@
|
|||
package cr0s.warpdrive.event;
|
||||
|
||||
import cr0s.warpdrive.config.WarpDriveConfig;
|
||||
import cr0s.warpdrive.data.OfflineAvatarManager;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.fml.common.eventhandler.EventPriority;
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.gameevent.PlayerEvent.PlayerLoggedOutEvent;
|
||||
|
||||
public class PlayerHandler {
|
||||
|
||||
@SubscribeEvent(priority = EventPriority.LOWEST)
|
||||
public void onPlayerLoadFromFile(@Nonnull final PlayerEvent.LoadFromFile event) {
|
||||
OfflineAvatarManager.onPlayerLoggedIn(event.getEntityPlayer());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onPlayerLoggedOut(@Nonnull final PlayerLoggedOutEvent event) {
|
||||
if (WarpDriveConfig.OFFLINE_AVATAR_ENABLE) {
|
||||
OfflineAvatarManager.onPlayerLoggedOut(event.player);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package cr0s.warpdrive.render;
|
||||
|
||||
import cr0s.warpdrive.client.PlayerTextureManager;
|
||||
import cr0s.warpdrive.config.WarpDriveConfig;
|
||||
import cr0s.warpdrive.entity.EntityOfflineAvatar;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import net.minecraft.client.model.ModelBiped;
|
||||
import net.minecraft.client.model.ModelPlayer;
|
||||
import net.minecraft.client.renderer.GlStateManager;
|
||||
import net.minecraft.client.renderer.entity.RenderLivingBase;
|
||||
import net.minecraft.client.renderer.entity.RenderManager;
|
||||
import net.minecraft.client.renderer.entity.layers.LayerArrow;
|
||||
import net.minecraft.client.renderer.entity.layers.LayerBipedArmor;
|
||||
import net.minecraft.client.renderer.entity.layers.LayerCustomHead;
|
||||
import net.minecraft.client.renderer.entity.layers.LayerElytra;
|
||||
import net.minecraft.client.renderer.entity.layers.LayerHeldItem;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
public class RenderEntityOfflineAvatar extends RenderLivingBase<EntityOfflineAvatar> {
|
||||
|
||||
public RenderEntityOfflineAvatar(@Nonnull final RenderManager renderManager) {
|
||||
super(renderManager, new ModelBiped(), WarpDriveConfig.OFFLINE_AVATAR_MODEL_SCALE);
|
||||
|
||||
final boolean useSmallArms = true;
|
||||
final ModelBiped modelPlayer = new ModelPlayer(0.0F, useSmallArms);
|
||||
mainModel = modelPlayer;
|
||||
addLayer(new LayerBipedArmor(this));
|
||||
addLayer(new LayerHeldItem(this));
|
||||
addLayer(new LayerArrow(this));
|
||||
addLayer(new LayerCustomHead(modelPlayer.bipedHead));
|
||||
addLayer(new LayerElytra(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preRenderCallback(@Nonnull final EntityOfflineAvatar entityOfflineAvatar, final float partialTickTime) {
|
||||
super.preRenderCallback(entityOfflineAvatar, partialTickTime);
|
||||
|
||||
GlStateManager.scale(WarpDriveConfig.OFFLINE_AVATAR_MODEL_SCALE,
|
||||
WarpDriveConfig.OFFLINE_AVATAR_MODEL_SCALE,
|
||||
WarpDriveConfig.OFFLINE_AVATAR_MODEL_SCALE );
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResourceLocation getEntityTexture(@Nonnull final EntityOfflineAvatar entityOfflineAvatar) {
|
||||
final UUID uuidPlayer = entityOfflineAvatar.getPlayerUUID();
|
||||
final String namePlayer = entityOfflineAvatar.getPlayerName();
|
||||
if (uuidPlayer != null) {
|
||||
return PlayerTextureManager.getPlayerSkin(uuidPlayer, namePlayer);
|
||||
}
|
||||
|
||||
return PlayerTextureManager.RESOURCE_LOCATION_DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canRenderName(@Nonnull final EntityOfflineAvatar entityOfflineAvatar) {
|
||||
return entityOfflineAvatar.getAlwaysRenderNameTagForRender();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doRender(@Nonnull final EntityOfflineAvatar entityOfflineAvatar,
|
||||
final double x, final double y, final double z, final float entityYaw, final float partialTicks) {
|
||||
super.doRender(entityOfflineAvatar, x, y, z, entityYaw, partialTicks);
|
||||
}
|
||||
}
|
|
@ -25,4 +25,8 @@ public net.minecraft.world.Explosion field_77283_e # exploder
|
|||
|
||||
# WorldProxy
|
||||
# public-f net.minecraft.world.World *()V # Doesn't matter what it is, we need it public
|
||||
# public-f net.minecraft.world.WorldServer *()V # Doesn't matter what it is, we need it public
|
||||
# public-f net.minecraft.world.WorldServer *()V # Doesn't matter what it is, we need it public
|
||||
|
||||
# GameProfile caching and filling
|
||||
public-f net.minecraft.tileentity.TileEntitySkull field_184298_j # profileCache
|
||||
public-f net.minecraft.tileentity.TileEntitySkull field_184299_k # sessionService
|
||||
|
|
Loading…
Reference in a new issue