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}