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}