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 }