    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        client = minecraft;
    134        if (minecraft.isDemo())
    135        {
    136            FMLLog.severe("DEMO MODE DETECTED, FML will not work. Finishing now.");
    137            haltGame("FML will not run in demo mode", new RuntimeException());
    138            return;
    139        }
    140
    141        loading = true;
    142//        TextureFXManager.instance().setClient(client);
    143        FMLCommonHandler.instance().beginLoading(this);
    144        new ModLoaderClientHelper(client);
    145        try
    146        {
    147            Class<?> optifineConfig = Class.forName("Config", false, Loader.instance().getModClassLoader());
    148            String optifineVersion = (String) optifineConfig.getField("VERSION").get(null);
    149            Map<String,Object> dummyOptifineMeta = ImmutableMap.<String,Object>builder().put("name", "Optifine").put("version", optifineVersion).build();
    150            ModMetadata optifineMetadata = MetadataCollection.from(getClass().getResourceAsStream("optifinemod.info"),"optifine").getMetadataForId("optifine", dummyOptifineMeta);
    151            optifineContainer = new DummyModContainer(optifineMetadata);
    152            FMLLog.info("Forge Mod Loader has detected optifine %s, enabling compatibility features",optifineContainer.getVersion());
    153        }
    154        catch (Exception e)
    155        {
    156            optifineContainer = null;
    157        }
    158        try
    159        {
    160            Loader.instance().loadMods();
    161        }
    162        catch (WrongMinecraftVersionException wrong)
    163        {
    164            wrongMC = wrong;
    165        }
    166        catch (DuplicateModsFoundException dupes)
    167        {
    168            dupesFound = dupes;
    169        }
    170        catch (MissingModsException missing)
    171        {
    172            modsMissing = missing;
    173        }
    174        catch (CustomModLoadingErrorDisplayException custom)
    175        {
    176            FMLLog.log(Level.SEVERE, custom, "A custom exception was thrown by a mod, the game will now halt");
    177            customError = custom;
    178        }
    179        catch (LoaderException le)
    180        {
    181            haltGame("There was a severe problem during mod loading that has caused the game to fail", le);
    182            return;
    183        }
    184    }
    185
    186    @Override
    187    public void haltGame(String message, Throwable t)
    188    {
    189        client.displayCrashReport(new CrashReport(message, t));
    190        throw Throwables.propagate(t);
    191    }
    192    /**
    193     * Called a bit later on during initialization to finish loading mods
    194     * Also initializes key bindings
    195     *
    196     */
    197    @SuppressWarnings("deprecation")
    198    public void finishMinecraftLoading()
    199    {
    200        if (modsMissing != null || wrongMC != null || customError!=null || dupesFound!=null)
    201        {
    202            return;
    203        }
    204        try
    205        {
    206            Loader.instance().initializeMods();
    207        }
    208        catch (CustomModLoadingErrorDisplayException custom)
    209        {
    210            FMLLog.log(Level.SEVERE, custom, "A custom exception was thrown by a mod, the game will now halt");
    211            customError = custom;
    212            return;
    213        }
    214        catch (LoaderException le)
    215        {
    216            haltGame("There was a severe problem during mod loading that has caused the game to fail", le);
    217            return;
    218        }
    219        LanguageRegistry.reloadLanguageTable();
    220        RenderingRegistry.instance().loadEntityRenderers((Map<Class<? extends Entity>, Render>)RenderManager.instance.entityRenderMap);
    221
    222        loading = false;
    223        KeyBindingRegistry.instance().uploadKeyBindingsToGame(client.gameSettings);
    224    }
    225
    226    public void onInitializationComplete()
    227    {
    228        if (wrongMC != null)
    229        {
    230            client.displayGuiScreen(new GuiWrongMinecraft(wrongMC));
    231        }
    232        else if (modsMissing != null)
    233        {
    234            client.displayGuiScreen(new GuiModsMissing(modsMissing));
    235        }
    236        else if (dupesFound != null)
    237        {
    238            client.displayGuiScreen(new GuiDupesFound(dupesFound));
    239        }
    240        else if (customError != null)
    241        {
    242            client.displayGuiScreen(new GuiCustomModLoadingErrorScreen(customError));
    243        }
    244        else
    245        {
    246            // Force renderengine to reload and re-initialize all textures
    247            client.renderEngine.refreshTextures();
    248//            TextureFXManager.instance().loadTextures(client.texturePackList.getSelectedTexturePack());
    249        }
    250    }
    251    /**
    252     * Get the server instance
    253     */
    254    public Minecraft getClient()
    255    {
    256        return client;
    257    }
    258
    259    /**
    260     * Get a handle to the client's logger instance
    261     * The client actually doesn't have one- so we return null
    262     */
    263    public Logger getMinecraftLogger()
    264    {
    265        return null;
    266    }
    267
    268    /**
    269     * @return the instance
    270     */
    271    public static FMLClientHandler instance()
    272    {
    273        return INSTANCE;
    274    }
    275
    276    /**
    277     * @param player
    278     * @param gui
    279     */
    280    public void displayGuiScreen(EntityPlayer player, GuiScreen gui)
    281    {
    282        if (client.thePlayer==player && gui != null) {
    283            client.displayGuiScreen(gui);
    284        }
    285    }
    286
    287    /**
    288     * @param mods
    289     */
    290    public void addSpecialModEntries(ArrayList<ModContainer> mods)
    291    {
    292        if (optifineContainer!=null) {
    293            mods.add(optifineContainer);
    294        }
    295    }
    296
    297    @Override
    298    public List<String> getAdditionalBrandingInformation()
    299    {
    300        if (optifineContainer!=null)
    301        {
    302            return Arrays.asList(String.format("Optifine %s",optifineContainer.getVersion()));
    303        } else {
    304            return ImmutableList.<String>of();
    305        }
    306    }
    307
    308    @Override
    309    public Side getSide()
    310    {
    311        return Side.CLIENT;
    312    }
    313
    314    public boolean hasOptifine()
    315    {
    316        return optifineContainer!=null;
    317    }
    318
    319    @Override
    320    public void showGuiScreen(Object clientGuiElement)
    321    {
    322        GuiScreen gui = (GuiScreen) clientGuiElement;
    323        client.displayGuiScreen(gui);
    324    }
    325
    326    @Override
    327    public Entity spawnEntityIntoClientWorld(EntityRegistration er, EntitySpawnPacket packet)
    328    {
    329        WorldClient wc = client.theWorld;
    330
    331        Class<? extends Entity> cls = er.getEntityClass();
    332
    333        try
    334        {
    335            Entity entity;
    336            if (er.hasCustomSpawning())
    337            {
    338                entity = er.doCustomSpawning(packet);
    339            }
    340            else
    341            {
    342                entity = (Entity)(cls.getConstructor(World.class).newInstance(wc));
    343                int offset = packet.entityId - entity.entityId;
    344                entity.entityId = packet.entityId;
    345                entity.setLocationAndAngles(packet.scaledX, packet.scaledY, packet.scaledZ, packet.scaledYaw, packet.scaledPitch);
    346                if (entity instanceof EntityLiving)
    347                {
    348                    ((EntityLiving)entity).rotationYawHead = packet.scaledHeadYaw;
    349                }
    350
    351                Entity parts[] = entity.getParts();
    352                if (parts != null)
    353                {
    354                    for (int j = 0; j < parts.length; j++)
    355                    {
    356                        parts[j].entityId += offset;
    357                    }
    358                }
    359            }
    360
    361            entity.serverPosX = packet.rawX;
    362            entity.serverPosY = packet.rawY;
    363            entity.serverPosZ = packet.rawZ;
    364
    365            if (entity instanceof IThrowableEntity)
    366            {
    367                Entity thrower = client.thePlayer.entityId == packet.throwerId ? client.thePlayer : wc.getEntityByID(packet.throwerId);
    368                ((IThrowableEntity)entity).setThrower(thrower);
    369            }
    370
    371            if (packet.metadata != null)
    372            {
    373                entity.getDataWatcher().updateWatchedObjectsFromList((List)packet.metadata);
    374            }
    375
    376            if (packet.throwerId > 0)
    377            {
    378                entity.setVelocity(packet.speedScaledX, packet.speedScaledY, packet.speedScaledZ);
    379            }
    380
    381            if (entity instanceof IEntityAdditionalSpawnData)
    382            {
    383                ((IEntityAdditionalSpawnData)entity).readSpawnData(packet.dataStream);
    384            }
    385
    386            wc.addEntityToWorld(packet.entityId, entity);
    387            return entity;
    388        }
    389        catch (Exception e)
    390        {
    391            FMLLog.log(Level.SEVERE, e, "A severe problem occurred during the spawning of an entity");
    392            throw Throwables.propagate(e);
    393        }
    394    }
    395
    396    @Override
    397    public void adjustEntityLocationOnClient(EntitySpawnAdjustmentPacket packet)
    398    {
    399        Entity ent = client.theWorld.getEntityByID(packet.entityId);
    400        if (ent != null)
    401        {
    402            ent.serverPosX = packet.serverX;
    403            ent.serverPosY = packet.serverY;
    404            ent.serverPosZ = packet.serverZ;
    405        }
    406        else
    407        {
    408            FMLLog.fine("Attempted to adjust the position of entity %d which is not present on the client", packet.entityId);
    409        }
    410    }
    411
    412    @Override
    413    public void beginServerLoading(MinecraftServer server)
    414    {
    415        serverShouldBeKilledQuietly = false;
    416        // NOOP
    417    }
    418
    419    @Override
    420    public void finishServerLoading()
    421    {
    422        // NOOP
    423    }
    424
    425    @Override
    426    public MinecraftServer getServer()
    427    {
    428        return client.getIntegratedServer();
    429    }
    430
    431    @Override
    432    public void sendPacket(Packet packet)
    433    {
    434        if(client.thePlayer != null)
    435        {
    436            client.thePlayer.sendQueue.addToSendQueue(packet);
    437        }
    438    }
    439
    440    @Override
    441    public void displayMissingMods(ModMissingPacket modMissingPacket)
    442    {
    443        client.displayGuiScreen(new GuiModsMissingForServer(modMissingPacket));
    444    }
    445
    446    /**
    447     * If the client is in the midst of loading, we disable saving so that custom settings aren't wiped out
    448     */
    449    public boolean isLoading()
    450    {
    451        return loading;
    452    }
    453
    454    @Override
    455    public void handleTinyPacket(NetHandler handler, Packet131MapData mapData)
    456    {
    457        ((NetClientHandler)handler).fmlPacket131Callback(mapData);
    458    }
    459
    460    @Override
    461    public void setClientCompatibilityLevel(byte compatibilityLevel)
    462    {
    463        NetClientHandler.setConnectionCompatibilityLevel(compatibilityLevel);
    464    }
    465
    466    @Override
    467    public byte getClientCompatibilityLevel()
    468    {
    469        return NetClientHandler.getConnectionCompatibilityLevel();
    470    }
    471
    472    public void warnIDMismatch(MapDifference<Integer, ItemData> idDifferences, boolean mayContinue)
    473    {
    474        GuiIdMismatchScreen mismatch = new GuiIdMismatchScreen(idDifferences, mayContinue);
    475        client.displayGuiScreen(mismatch);
    476    }
    477
    478    public void callbackIdDifferenceResponse(boolean response)
    479    {
    480        if (response)
    481        {
    482            serverShouldBeKilledQuietly = false;
    483            GameData.releaseGate(true);
    484            client.continueWorldLoading();
    485        }
    486        else
    487        {
    488            serverShouldBeKilledQuietly = true;
    489            GameData.releaseGate(false);
    490            // Reset and clear the client state
    491            client.loadWorld((WorldClient)null);
    492            client.displayGuiScreen(null);
    493        }
    494    }
    495
    496    @Override
    497    public boolean shouldServerShouldBeKilledQuietly()
    498    {
    499        return serverShouldBeKilledQuietly;
    500    }
    501
    502    @Override
    503    public void disconnectIDMismatch(MapDifference<Integer, ItemData> s, NetHandler toKill, INetworkManager mgr)
    504    {
    505        boolean criticalMismatch = !s.entriesOnlyOnLeft().isEmpty();
    506        for (Entry<Integer, ValueDifference<ItemData>> mismatch : s.entriesDiffering().entrySet())
    507        {
    508            ValueDifference<ItemData> vd = mismatch.getValue();
    509            if (!vd.leftValue().mayDifferByOrdinal(vd.rightValue()))
    510            {
    511                criticalMismatch = true;
    512            }
    513        }
    514
    515        if (!criticalMismatch)
    516        {
    517            // We'll carry on with this connection, and just log a message instead
    518            return;
    519        }
    520        // Nuke the connection
    521        ((NetClientHandler)toKill).disconnect();
    522        // Stop GuiConnecting
    523        GuiConnecting.forceTermination((GuiConnecting)client.currentScreen);
    524        // pulse the network manager queue to clear cruft
    525        mgr.processReadPackets();
    526        // Nuke the world client
    527        client.loadWorld((WorldClient)null);
    528        // Show error screen
    529        warnIDMismatch(s, false);
    530    }
    531
    532    /**
    533     * Is this GUI type open?
    534     *
    535     * @param gui The type of GUI to test for
    536     * @return if a GUI of this type is open
    537     */
    538    public boolean isGUIOpen(Class<? extends GuiScreen> gui)
    539    {
    540        return client.currentScreen != null && client.currentScreen.getClass().equals(gui);
    541    }
    542}