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}