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.logging.Level;
021    import java.util.logging.Logger;
022    
023    import net.minecraft.client.Minecraft;
024    import net.minecraft.server.MinecraftServer;
025    import net.minecraft.src.CrashReport;
026    import net.minecraft.src.Entity;
027    import net.minecraft.src.EntityLiving;
028    import net.minecraft.src.EntityPlayer;
029    import net.minecraft.src.GuiConnecting;
030    import net.minecraft.src.GuiScreen;
031    import net.minecraft.src.INetworkManager;
032    import net.minecraft.src.NetClientHandler;
033    import net.minecraft.src.NetHandler;
034    import net.minecraft.src.Packet;
035    import net.minecraft.src.Packet131MapData;
036    import net.minecraft.src.Render;
037    import net.minecraft.src.RenderManager;
038    import net.minecraft.src.World;
039    import net.minecraft.src.WorldClient;
040    
041    import com.google.common.base.Throwables;
042    import com.google.common.collect.ImmutableList;
043    import com.google.common.collect.ImmutableMap;
044    import com.google.common.collect.MapDifference;
045    
046    import cpw.mods.fml.client.modloader.ModLoaderClientHelper;
047    import cpw.mods.fml.client.registry.KeyBindingRegistry;
048    import cpw.mods.fml.client.registry.RenderingRegistry;
049    import cpw.mods.fml.common.DummyModContainer;
050    import cpw.mods.fml.common.DuplicateModsFoundException;
051    import cpw.mods.fml.common.FMLCommonHandler;
052    import cpw.mods.fml.common.FMLLog;
053    import cpw.mods.fml.common.IFMLSidedHandler;
054    import cpw.mods.fml.common.Loader;
055    import cpw.mods.fml.common.LoaderException;
056    import cpw.mods.fml.common.MetadataCollection;
057    import cpw.mods.fml.common.MissingModsException;
058    import cpw.mods.fml.common.ModContainer;
059    import cpw.mods.fml.common.ModMetadata;
060    import cpw.mods.fml.common.ObfuscationReflectionHelper;
061    import cpw.mods.fml.common.Side;
062    import cpw.mods.fml.common.WrongMinecraftVersionException;
063    import cpw.mods.fml.common.network.EntitySpawnAdjustmentPacket;
064    import cpw.mods.fml.common.network.EntitySpawnPacket;
065    import cpw.mods.fml.common.network.ModMissingPacket;
066    import cpw.mods.fml.common.registry.EntityRegistry.EntityRegistration;
067    import cpw.mods.fml.common.registry.GameData;
068    import cpw.mods.fml.common.registry.GameRegistry;
069    import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
070    import cpw.mods.fml.common.registry.IThrowableEntity;
071    import cpw.mods.fml.common.registry.ItemData;
072    import cpw.mods.fml.common.registry.LanguageRegistry;
073    
074    
075    /**
076     * Handles primary communication from hooked code into the system
077     *
078     * The FML entry point is {@link #beginMinecraftLoading(Minecraft)} called from
079     * {@link Minecraft}
080     *
081     * Obfuscated code should focus on this class and other members of the "server"
082     * (or "client") code
083     *
084     * The actual mod loading is handled at arms length by {@link Loader}
085     *
086     * It is expected that a similar class will exist for each target environment:
087     * Bukkit and Client side.
088     *
089     * It should not be directly modified.
090     *
091     * @author cpw
092     *
093     */
094    public class FMLClientHandler implements IFMLSidedHandler
095    {
096        /**
097         * The singleton
098         */
099        private static final FMLClientHandler INSTANCE = new FMLClientHandler();
100    
101        /**
102         * A reference to the server itself
103         */
104        private Minecraft client;
105    
106        private DummyModContainer optifineContainer;
107    
108        private boolean guiLoaded;
109    
110        private boolean serverIsRunning;
111    
112        private MissingModsException modsMissing;
113    
114        private boolean loading;
115    
116        private WrongMinecraftVersionException wrongMC;
117    
118        private CustomModLoadingErrorDisplayException customError;
119    
120        private DuplicateModsFoundException dupesFound;
121    
122        private boolean serverShouldBeKilledQuietly;
123    
124        /**
125         * Called to start the whole game off
126         *
127         * @param minecraft The minecraft instance being launched
128         */
129        public void beginMinecraftLoading(Minecraft minecraft)
130        {
131            if (minecraft.isDemo())
132            {
133                FMLLog.severe("DEMO MODE DETECTED, FML will not work. Finishing now.");
134                haltGame("FML will not run in demo mode", new RuntimeException());
135                return;
136            }
137    
138            loading = true;
139            client = minecraft;
140            ObfuscationReflectionHelper.detectObfuscation(World.class);
141            TextureFXManager.instance().setClient(client);
142            FMLCommonHandler.instance().beginLoading(this);
143            new ModLoaderClientHelper(client);
144            try
145            {
146                Class<?> optifineConfig = Class.forName("Config", false, Loader.instance().getModClassLoader());
147                String optifineVersion = (String) optifineConfig.getField("VERSION").get(null);
148                Map<String,Object> dummyOptifineMeta = ImmutableMap.<String,Object>builder().put("name", "Optifine").put("version", optifineVersion).build();
149                ModMetadata optifineMetadata = MetadataCollection.from(getClass().getResourceAsStream("optifinemod.info"),"optifine").getMetadataForId("optifine", dummyOptifineMeta);
150                optifineContainer = new DummyModContainer(optifineMetadata);
151                FMLLog.info("Forge Mod Loader has detected optifine %s, enabling compatibility features",optifineContainer.getVersion());
152            }
153            catch (Exception e)
154            {
155                optifineContainer = null;
156            }
157            try
158            {
159                Loader.instance().loadMods();
160            }
161            catch (WrongMinecraftVersionException wrong)
162            {
163                wrongMC = wrong;
164            }
165            catch (DuplicateModsFoundException dupes)
166            {
167                dupesFound = dupes;
168            }
169            catch (MissingModsException missing)
170            {
171                modsMissing = missing;
172            }
173            catch (CustomModLoadingErrorDisplayException custom)
174            {
175                FMLLog.log(Level.SEVERE, custom, "A custom exception was thrown by a mod, the game will now halt");
176                customError = custom;
177            }
178            catch (LoaderException le)
179            {
180                haltGame("There was a severe problem during mod loading that has caused the game to fail", le);
181                return;
182            }
183        }
184    
185        @Override
186        public void haltGame(String message, Throwable t)
187        {
188            client.displayCrashReport(new CrashReport(message, t));
189            throw Throwables.propagate(t);
190        }
191        /**
192         * Called a bit later on during initialization to finish loading mods
193         * Also initializes key bindings
194         *
195         */
196        @SuppressWarnings("deprecation")
197        public void finishMinecraftLoading()
198        {
199            if (modsMissing != null || wrongMC != null || customError!=null || dupesFound!=null)
200            {
201                return;
202            }
203            try
204            {
205                Loader.instance().initializeMods();
206            }
207            catch (CustomModLoadingErrorDisplayException custom)
208            {
209                FMLLog.log(Level.SEVERE, custom, "A custom exception was thrown by a mod, the game will now halt");
210                customError = custom;
211                return;
212            }
213            catch (LoaderException le)
214            {
215                haltGame("There was a severe problem during mod loading that has caused the game to fail", le);
216                return;
217            }
218            LanguageRegistry.reloadLanguageTable();
219            RenderingRegistry.instance().loadEntityRenderers((Map<Class<? extends Entity>, Render>)RenderManager.instance.entityRenderMap);
220    
221            loading = false;
222            KeyBindingRegistry.instance().uploadKeyBindingsToGame(client.gameSettings);
223        }
224    
225        public void onInitializationComplete()
226        {
227            if (wrongMC != null)
228            {
229                client.displayGuiScreen(new GuiWrongMinecraft(wrongMC));
230            }
231            else if (modsMissing != null)
232            {
233                client.displayGuiScreen(new GuiModsMissing(modsMissing));
234            }
235            else if (dupesFound != null)
236            {
237                client.displayGuiScreen(new GuiDupesFound(dupesFound));
238            }
239            else if (customError != null)
240            {
241                client.displayGuiScreen(new GuiCustomModLoadingErrorScreen(customError));
242            }
243            else
244            {
245                TextureFXManager.instance().loadTextures(client.texturePackList.getSelectedTexturePack());
246            }
247        }
248        /**
249         * Get the server instance
250         */
251        public Minecraft getClient()
252        {
253            return client;
254        }
255    
256        /**
257         * Get a handle to the client's logger instance
258         * The client actually doesn't have one- so we return null
259         */
260        public Logger getMinecraftLogger()
261        {
262            return null;
263        }
264    
265        /**
266         * @return the instance
267         */
268        public static FMLClientHandler instance()
269        {
270            return INSTANCE;
271        }
272    
273        /**
274         * @param player
275         * @param gui
276         */
277        public void displayGuiScreen(EntityPlayer player, GuiScreen gui)
278        {
279            if (client.thePlayer==player && gui != null) {
280                client.displayGuiScreen(gui);
281            }
282        }
283    
284        /**
285         * @param mods
286         */
287        public void addSpecialModEntries(ArrayList<ModContainer> mods)
288        {
289            if (optifineContainer!=null) {
290                mods.add(optifineContainer);
291            }
292        }
293    
294        @Override
295        public List<String> getAdditionalBrandingInformation()
296        {
297            if (optifineContainer!=null)
298            {
299                return Arrays.asList(String.format("Optifine %s",optifineContainer.getVersion()));
300            } else {
301                return ImmutableList.<String>of();
302            }
303        }
304    
305        @Override
306        public Side getSide()
307        {
308            return Side.CLIENT;
309        }
310    
311        public boolean hasOptifine()
312        {
313            return optifineContainer!=null;
314        }
315    
316        @Override
317        public void showGuiScreen(Object clientGuiElement)
318        {
319            GuiScreen gui = (GuiScreen) clientGuiElement;
320            client.displayGuiScreen(gui);
321        }
322    
323        @Override
324        public Entity spawnEntityIntoClientWorld(EntityRegistration er, EntitySpawnPacket packet)
325        {
326            WorldClient wc = client.theWorld;
327    
328            Class<? extends Entity> cls = er.getEntityClass();
329    
330            try
331            {
332                Entity entity;
333                if (er.hasCustomSpawning())
334                {
335                    entity = er.doCustomSpawning(packet);
336                }
337                else
338                {
339                    entity = (Entity)(cls.getConstructor(World.class).newInstance(wc));
340                    entity.entityId = packet.entityId;
341                    entity.setLocationAndAngles(packet.scaledX, packet.scaledY, packet.scaledZ, packet.scaledYaw, packet.scaledPitch);
342                    if (entity instanceof EntityLiving)
343                    {
344                        ((EntityLiving)entity).rotationYawHead = packet.scaledHeadYaw;
345                    }
346    
347                }
348    
349                entity.serverPosX = packet.rawX;
350                entity.serverPosY = packet.rawY;
351                entity.serverPosZ = packet.rawZ;
352    
353                if (entity instanceof IThrowableEntity)
354                {
355                    Entity thrower = client.thePlayer.entityId == packet.throwerId ? client.thePlayer : wc.getEntityByID(packet.throwerId);
356                    ((IThrowableEntity)entity).setThrower(thrower);
357                }
358    
359    
360                Entity parts[] = entity.getParts();
361                if (parts != null)
362                {
363                    int i = packet.entityId - entity.entityId;
364                    for (int j = 0; j < parts.length; j++)
365                    {
366                        parts[j].entityId += i;
367                    }
368                }
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            // Nuke the connection
506            ((NetClientHandler)toKill).disconnect();
507            // Stop GuiConnecting
508            GuiConnecting.forceTermination((GuiConnecting)client.currentScreen);
509            // pulse the network manager queue to clear cruft
510            mgr.processReadPackets();
511            // Nuke the world client
512            client.loadWorld((WorldClient)null);
513            // Show error screen
514            warnIDMismatch(s, false);
515        }
516    }