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     */
013    package cpw.mods.fml.client;
014    
015    import java.util.ArrayList;
016    import java.util.Arrays;
017    import java.util.Collections;
018    import java.util.List;
019    import java.util.Map;
020    import java.util.Map.Entry;
021    import java.util.logging.Level;
022    import java.util.logging.Logger;
023    
024    import net.minecraft.client.Minecraft;
025    import net.minecraft.client.gui.GuiScreen;
026    import net.minecraft.client.multiplayer.GuiConnecting;
027    import net.minecraft.client.multiplayer.NetClientHandler;
028    import net.minecraft.client.multiplayer.WorldClient;
029    import net.minecraft.client.renderer.entity.Render;
030    import net.minecraft.client.renderer.entity.RenderManager;
031    import net.minecraft.crash.CrashReport;
032    import net.minecraft.entity.Entity;
033    import net.minecraft.entity.EntityLiving;
034    import net.minecraft.entity.player.EntityPlayer;
035    import net.minecraft.network.INetworkManager;
036    import net.minecraft.network.packet.NetHandler;
037    import net.minecraft.network.packet.Packet;
038    import net.minecraft.network.packet.Packet131MapData;
039    import net.minecraft.server.MinecraftServer;
040    import net.minecraft.world.World;
041    
042    import com.google.common.base.Throwables;
043    import com.google.common.collect.ImmutableList;
044    import com.google.common.collect.ImmutableMap;
045    import com.google.common.collect.MapDifference;
046    import com.google.common.collect.MapDifference.ValueDifference;
047    
048    import cpw.mods.fml.client.modloader.ModLoaderClientHelper;
049    import cpw.mods.fml.client.registry.KeyBindingRegistry;
050    import cpw.mods.fml.client.registry.RenderingRegistry;
051    import cpw.mods.fml.common.DummyModContainer;
052    import cpw.mods.fml.common.DuplicateModsFoundException;
053    import cpw.mods.fml.common.FMLCommonHandler;
054    import cpw.mods.fml.common.FMLLog;
055    import cpw.mods.fml.common.IFMLSidedHandler;
056    import cpw.mods.fml.common.Loader;
057    import cpw.mods.fml.common.LoaderException;
058    import cpw.mods.fml.common.MetadataCollection;
059    import cpw.mods.fml.common.MissingModsException;
060    import cpw.mods.fml.common.ModContainer;
061    import cpw.mods.fml.common.ModMetadata;
062    import cpw.mods.fml.common.ObfuscationReflectionHelper;
063    import cpw.mods.fml.common.WrongMinecraftVersionException;
064    import cpw.mods.fml.common.network.EntitySpawnAdjustmentPacket;
065    import cpw.mods.fml.common.network.EntitySpawnPacket;
066    import cpw.mods.fml.common.network.ModMissingPacket;
067    import cpw.mods.fml.common.registry.EntityRegistry.EntityRegistration;
068    import cpw.mods.fml.common.registry.GameData;
069    import cpw.mods.fml.common.registry.GameRegistry;
070    import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
071    import cpw.mods.fml.common.registry.IThrowableEntity;
072    import cpw.mods.fml.common.registry.ItemData;
073    import cpw.mods.fml.common.registry.LanguageRegistry;
074    import 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     */
096    public 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    }