    001/*
    002 * The FML Forge Mod Loader suite. Copyright (C) 2012 cpw
    003 *
    004 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
    005 * Software Foundation; either version 2.1 of the License, or any later version.
    006 *
    007 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
    008 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
    009 *
    010 * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51
    011 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
    012 */
    013package cpw.mods.fml.client;
    014
    015import java.util.ArrayList;
    016import java.util.Arrays;
    017import java.util.Collections;
    018import java.util.List;
    019import java.util.Map;
    020import java.util.Map.Entry;
    021import java.util.logging.Level;
    022import java.util.logging.Logger;
    023
    024import net.minecraft.client.Minecraft;
    025import net.minecraft.client.gui.GuiScreen;
    026import net.minecraft.client.multiplayer.GuiConnecting;
    027import net.minecraft.client.multiplayer.NetClientHandler;
    028import net.minecraft.client.multiplayer.WorldClient;
    029import net.minecraft.client.renderer.entity.Render;
    030import net.minecraft.client.renderer.entity.RenderManager;
    031import net.minecraft.crash.CrashReport;
    032import net.minecraft.entity.Entity;
    033import net.minecraft.entity.EntityLiving;
    034import net.minecraft.entity.player.EntityPlayer;
    035import net.minecraft.network.INetworkManager;
    036import net.minecraft.network.packet.NetHandler;
    037import net.minecraft.network.packet.Packet;
    038import net.minecraft.network.packet.Packet131MapData;
    039import net.minecraft.server.MinecraftServer;
    040import net.minecraft.world.World;
    041
    042import com.google.common.base.Throwables;
    043import com.google.common.collect.ImmutableList;
    044import com.google.common.collect.ImmutableMap;
    045import com.google.common.collect.MapDifference;
    046import com.google.common.collect.MapDifference.ValueDifference;
    047
    048import cpw.mods.fml.client.modloader.ModLoaderClientHelper;
    049import cpw.mods.fml.client.registry.KeyBindingRegistry;
    050import cpw.mods.fml.client.registry.RenderingRegistry;
    051import cpw.mods.fml.common.DummyModContainer;
    052import cpw.mods.fml.common.DuplicateModsFoundException;
    053import cpw.mods.fml.common.FMLCommonHandler;
    054import cpw.mods.fml.common.FMLLog;
    055import cpw.mods.fml.common.IFMLSidedHandler;
    056import cpw.mods.fml.common.Loader;
    057import cpw.mods.fml.common.LoaderException;
    058import cpw.mods.fml.common.MetadataCollection;
    059import cpw.mods.fml.common.MissingModsException;
    060import cpw.mods.fml.common.ModContainer;
    061import cpw.mods.fml.common.ModMetadata;
    062import cpw.mods.fml.common.ObfuscationReflectionHelper;
    063import cpw.mods.fml.common.WrongMinecraftVersionException;
    064import cpw.mods.fml.common.network.EntitySpawnAdjustmentPacket;
    065import cpw.mods.fml.common.network.EntitySpawnPacket;
    066import cpw.mods.fml.common.network.ModMissingPacket;
    067import cpw.mods.fml.common.registry.EntityRegistry.EntityRegistration;
    068import cpw.mods.fml.common.registry.GameData;
    069import cpw.mods.fml.common.registry.GameRegistry;
    070import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
    071import cpw.mods.fml.common.registry.IThrowableEntity;
    072import cpw.mods.fml.common.registry.ItemData;
    073import cpw.mods.fml.common.registry.LanguageRegistry;
    074import cpw.mods.fml.relauncher.Side;
    075
    076
    077/**
    078 * Handles primary communication from hooked code into the system
    079 *
    080 * The FML entry point is {@link #beginMinecraftLoading(Minecraft)} called from
    081 * {@link Minecraft}
    082 *
    083 * Obfuscated code should focus on this class and other members of the "server"
    084 * (or "client") code
    085 *
    086 * The actual mod loading is handled at arms length by {@link Loader}
    087 *
    088 * It is expected that a similar class will exist for each target environment:
    089 * Bukkit and Client side.
    090 *
    091 * It should not be directly modified.
    092 *
    093 * @author cpw
    094 *
    095 */
    096public class FMLClientHandler implements IFMLSidedHandler
    097{
    098    /**
    099     * The singleton
    100     */
    101    private static final FMLClientHandler INSTANCE = new FMLClientHandler();
    102
    103    /**
    104     * A reference to the server itself
    105     */
    106    private Minecraft client;
    107
    108    private DummyModContainer optifineContainer;
    109
    110    private boolean guiLoaded;
    111
    112    private boolean serverIsRunning;
    113
    114    private MissingModsException modsMissing;
    115
    116    private boolean loading;
    117
    118    private WrongMinecraftVersionException wrongMC;
    119
    120    private CustomModLoadingErrorDisplayException customError;
    121
    122    private DuplicateModsFoundException dupesFound;
    123
    124    private boolean serverShouldBeKilledQuietly;
    125
    126    /**
    127     * Called to start the whole game off
    128     *
    129     * @param minecraft The minecraft instance being launched
    130     */
    131    public void beginMinecraftLoading(Minecraft minecraft)
    132    {
    133        if (minecraft.isDemo())
    134        {
    135            FMLLog.severe("DEMO MODE DETECTED, FML will not work. Finishing now.");
    136            haltGame("FML will not run in demo mode", new RuntimeException());
    137            return;
    138        }
    139
    140        loading = true;
    141        client = minecraft;
    142        ObfuscationReflectionHelper.detectObfuscation(World.class);
    143        TextureFXManager.instance().setClient(client);
    144        FMLCommonHandler.instance().beginLoading(this);
    145        new ModLoaderClientHelper(client);
    146        try
    147        {
    148            Class<?> optifineConfig = Class.forName("Config", false, Loader.instance().getModClassLoader());
    149            String optifineVersion = (String) optifineConfig.getField("VERSION").get(null);
    150            Map<String,Object> dummyOptifineMeta = ImmutableMap.<String,Object>builder().put("name", "Optifine").put("version", optifineVersion).build();
    151            ModMetadata optifineMetadata = MetadataCollection.from(getClass().getResourceAsStream("optifinemod.info"),"optifine").getMetadataForId("optifine", dummyOptifineMeta);
    152            optifineContainer = new DummyModContainer(optifineMetadata);
    153            FMLLog.info("Forge Mod Loader has detected optifine %s, enabling compatibility features",optifineContainer.getVersion());
    154        }
    155        catch (Exception e)
    156        {
    157            optifineContainer = null;
    158        }
    159        try
    160        {
    161            Loader.instance().loadMods();
    162        }
    163        catch (WrongMinecraftVersionException wrong)
    164        {
    165            wrongMC = wrong;
    166        }
    167        catch (DuplicateModsFoundException dupes)
    168        {
    169            dupesFound = dupes;
    170        }
    171        catch (MissingModsException missing)
    172        {
    173            modsMissing = missing;
    174        }
    175        catch (CustomModLoadingErrorDisplayException custom)
    176        {
    177            FMLLog.log(Level.SEVERE, custom, "A custom exception was thrown by a mod, the game will now halt");
    178            customError = custom;
    179        }
    180        catch (LoaderException le)
    181        {
    182            haltGame("There was a severe problem during mod loading that has caused the game to fail", le);
    183            return;
    184        }
    185    }
    186
    187    @Override
    188    public void haltGame(String message, Throwable t)
    189    {
    190        client.displayCrashReport(new CrashReport(message, t));
    191        throw Throwables.propagate(t);
    192    }
    193    /**
    194     * Called a bit later on during initialization to finish loading mods
    195     * Also initializes key bindings
    196     *
    197     */
    198    @SuppressWarnings("deprecation")
    199    public void finishMinecraftLoading()
    200    {
    201        if (modsMissing != null || wrongMC != null || customError!=null || dupesFound!=null)
    202        {
    203            return;
    204        }
    205        try
    206        {
    207            Loader.instance().initializeMods();
    208        }
    209        catch (CustomModLoadingErrorDisplayException custom)
    210        {
    211            FMLLog.log(Level.SEVERE, custom, "A custom exception was thrown by a mod, the game will now halt");
    212            customError = custom;
    213            return;
    214        }
    215        catch (LoaderException le)
    216        {
    217            haltGame("There was a severe problem during mod loading that has caused the game to fail", le);
    218            return;
    219        }
    220        LanguageRegistry.reloadLanguageTable();
    221        RenderingRegistry.instance().loadEntityRenderers((Map<Class<? extends Entity>, Render>)RenderManager.instance.entityRenderMap);
    222
    223        loading = false;
    224        KeyBindingRegistry.instance().uploadKeyBindingsToGame(client.gameSettings);
    225    }
    226
    227    public void onInitializationComplete()
    228    {
    229        if (wrongMC != null)
    230        {
    231            client.displayGuiScreen(new GuiWrongMinecraft(wrongMC));
    232        }
    233        else if (modsMissing != null)
    234        {
    235            client.displayGuiScreen(new GuiModsMissing(modsMissing));
    236        }
    237        else if (dupesFound != null)
    238        {
    239            client.displayGuiScreen(new GuiDupesFound(dupesFound));
    240        }
    241        else if (customError != null)
    242        {
    243            client.displayGuiScreen(new GuiCustomModLoadingErrorScreen(customError));
    244        }
    245        else
    246        {
    247            TextureFXManager.instance().loadTextures(client.texturePackList.getSelectedTexturePack());
    248        }
    249    }
    250    /**
    251     * Get the server instance
    252     */
    253    public Minecraft getClient()
    254    {
    255        return client;
    256    }
    257
    258    /**
    259     * Get a handle to the client's logger instance
    260     * The client actually doesn't have one- so we return null
    261     */
    262    public Logger getMinecraftLogger()
    263    {
    264        return null;
    265    }
    266
    267    /**
    268     * @return the instance
    269     */
    270    public static FMLClientHandler instance()
    271    {
    272        return INSTANCE;
    273    }
    274
    275    /**
    276     * @param player
    277     * @param gui
    278     */
    279    public void displayGuiScreen(EntityPlayer player, GuiScreen gui)
    280    {
    281        if (client.thePlayer==player && gui != null) {
    282            client.displayGuiScreen(gui);
    283        }
    284    }
    285
    286    /**
    287     * @param mods
    288     */
    289    public void addSpecialModEntries(ArrayList<ModContainer> mods)
    290    {
    291        if (optifineContainer!=null) {
    292            mods.add(optifineContainer);
    293        }
    294    }
    295
    296    @Override
    297    public List<String> getAdditionalBrandingInformation()
    298    {
    299        if (optifineContainer!=null)
    300        {
    301            return Arrays.asList(String.format("Optifine %s",optifineContainer.getVersion()));
    302        } else {
    303            return ImmutableList.<String>of();
    304        }
    305    }
    306
    307    @Override
    308    public Side getSide()
    309    {
    310        return Side.CLIENT;
    311    }
    312
    313    public boolean hasOptifine()
    314    {
    315        return optifineContainer!=null;
    316    }
    317
    318    @Override
    319    public void showGuiScreen(Object clientGuiElement)
    320    {
    321        GuiScreen gui = (GuiScreen) clientGuiElement;
    322        client.displayGuiScreen(gui);
    323    }
    324
    325    @Override
    326    public Entity spawnEntityIntoClientWorld(EntityRegistration er, EntitySpawnPacket packet)
    327    {
    328        WorldClient wc = client.theWorld;
    329
    330        Class<? extends Entity> cls = er.getEntityClass();
    331
    332        try
    333        {
    334            Entity entity;
    335            if (er.hasCustomSpawning())
    336            {
    337                entity = er.doCustomSpawning(packet);
    338            }
    339            else
    340            {
    341                entity = (Entity)(cls.getConstructor(World.class).newInstance(wc));
    342                entity.entityId = packet.entityId;
    343                entity.setLocationAndAngles(packet.scaledX, packet.scaledY, packet.scaledZ, packet.scaledYaw, packet.scaledPitch);
    344                if (entity instanceof EntityLiving)
    345                {
    346                    ((EntityLiving)entity).rotationYawHead = packet.scaledHeadYaw;
    347                }
    348
    349            }
    350
    351            entity.serverPosX = packet.rawX;
    352            entity.serverPosY = packet.rawY;
    353            entity.serverPosZ = packet.rawZ;
    354
    355            if (entity instanceof IThrowableEntity)
    356            {
    357                Entity thrower = client.thePlayer.entityId == packet.throwerId ? client.thePlayer : wc.getEntityByID(packet.throwerId);
    358                ((IThrowableEntity)entity).setThrower(thrower);
    359            }
    360
    361
    362            Entity parts[] = entity.getParts();
    363            if (parts != null)
    364            {
    365                int i = packet.entityId - entity.entityId;
    366                for (int j = 0; j < parts.length; j++)
    367                {
    368                    parts[j].entityId += i;
    369                }
    370            }
    371
    372
    373            if (packet.metadata != null)
    374            {
    375                entity.getDataWatcher().updateWatchedObjectsFromList((List)packet.metadata);
    376            }
    377
    378            if (packet.throwerId > 0)
    379            {
    380                entity.setVelocity(packet.speedScaledX, packet.speedScaledY, packet.speedScaledZ);
    381            }
    382
    383            if (entity instanceof IEntityAdditionalSpawnData)
    384            {
    385                ((IEntityAdditionalSpawnData)entity).readSpawnData(packet.dataStream);
    386            }
    387
    388            wc.addEntityToWorld(packet.entityId, entity);
    389            return entity;
    390        }
    391        catch (Exception e)
    392        {
    393            FMLLog.log(Level.SEVERE, e, "A severe problem occurred during the spawning of an entity");
    394            throw Throwables.propagate(e);
    395        }
    396    }
    397
    398    @Override
    399    public void adjustEntityLocationOnClient(EntitySpawnAdjustmentPacket packet)
    400    {
    401        Entity ent = client.theWorld.getEntityByID(packet.entityId);
    402        if (ent != null)
    403        {
    404            ent.serverPosX = packet.serverX;
    405            ent.serverPosY = packet.serverY;
    406            ent.serverPosZ = packet.serverZ;
    407        }
    408        else
    409        {
    410            FMLLog.fine("Attempted to adjust the position of entity %d which is not present on the client", packet.entityId);
    411        }
    412    }
    413
    414    @Override
    415    public void beginServerLoading(MinecraftServer server)
    416    {
    417        serverShouldBeKilledQuietly = false;
    418        // NOOP
    419    }
    420
    421    @Override
    422    public void finishServerLoading()
    423    {
    424        // NOOP
    425    }
    426
    427    @Override
    428    public MinecraftServer getServer()
    429    {
    430        return client.getIntegratedServer();
    431    }
    432
    433    @Override
    434    public void sendPacket(Packet packet)
    435    {
    436        if(client.thePlayer != null)
    437        {
    438            client.thePlayer.sendQueue.addToSendQueue(packet);
    439        }
    440    }
    441
    442    @Override
    443    public void displayMissingMods(ModMissingPacket modMissingPacket)
    444    {
    445        client.displayGuiScreen(new GuiModsMissingForServer(modMissingPacket));
    446    }
    447
    448    /**
    449     * If the client is in the midst of loading, we disable saving so that custom settings aren't wiped out
    450     */
    451    public boolean isLoading()
    452    {
    453        return loading;
    454    }
    455
    456    @Override
    457    public void handleTinyPacket(NetHandler handler, Packet131MapData mapData)
    458    {
    459        ((NetClientHandler)handler).fmlPacket131Callback(mapData);
    460    }
    461
    462    @Override
    463    public void setClientCompatibilityLevel(byte compatibilityLevel)
    464    {
    465        NetClientHandler.setConnectionCompatibilityLevel(compatibilityLevel);
    466    }
    467
    468    @Override
    469    public byte getClientCompatibilityLevel()
    470    {
    471        return NetClientHandler.getConnectionCompatibilityLevel();
    472    }
    473
    474    public void warnIDMismatch(MapDifference<Integer, ItemData> idDifferences, boolean mayContinue)
    475    {
    476        GuiIdMismatchScreen mismatch = new GuiIdMismatchScreen(idDifferences, mayContinue);
    477        client.displayGuiScreen(mismatch);
    478    }
    479
    480    public void callbackIdDifferenceResponse(boolean response)
    481    {
    482        if (response)
    483        {
    484            serverShouldBeKilledQuietly = false;
    485            GameData.releaseGate(true);
    486            client.continueWorldLoading();
    487        }
    488        else
    489        {
    490            serverShouldBeKilledQuietly = true;
    491            GameData.releaseGate(false);
    492            // Reset and clear the client state
    493            client.loadWorld((WorldClient)null);
    494            client.displayGuiScreen(null);
    495        }
    496    }
    497
    498    @Override
    499    public boolean shouldServerShouldBeKilledQuietly()
    500    {
    501        return serverShouldBeKilledQuietly;
    502    }
    503
    504    @Override
    505    public void disconnectIDMismatch(MapDifference<Integer, ItemData> s, NetHandler toKill, INetworkManager mgr)
    506    {
    507        boolean criticalMismatch = !s.entriesOnlyOnLeft().isEmpty();
    508        for (Entry<Integer, ValueDifference<ItemData>> mismatch : s.entriesDiffering().entrySet())
    509        {
    510            ValueDifference<ItemData> vd = mismatch.getValue();
    511            if (!vd.leftValue().mayDifferByOrdinal(vd.rightValue()))
    512            {
    513                criticalMismatch = true;
    514            }
    515        }
    516
    517        if (!criticalMismatch)
    518        {
    519            // We'll carry on with this connection, and just log a message instead
    520            return;
    521        }
    522        // Nuke the connection
    523        ((NetClientHandler)toKill).disconnect();
    524        // Stop GuiConnecting
    525        GuiConnecting.forceTermination((GuiConnecting)client.currentScreen);
    526        // pulse the network manager queue to clear cruft
    527        mgr.processReadPackets();
    528        // Nuke the world client
    529        client.loadWorld((WorldClient)null);
    530        // Show error screen
    531        warnIDMismatch(s, false);
    532    }
    533}