001/* 002 * Gridarta MMORPG map editor for Crossfire, Daimonin and similar games. 003 * Copyright (C) 2000-2010 The Gridarta Developers. 004 * 005 * This program is free software; you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation; either version 2 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License along 016 * with this program; if not, write to the Free Software Foundation, Inc., 017 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 018 */ 019 020package net.sf.gridarta.model.baseobject; 021 022import java.io.IOException; 023import java.io.ObjectInputStream; 024import java.io.Serializable; 025import java.util.ArrayList; 026import java.util.Iterator; 027import java.util.List; 028import java.util.ListIterator; 029import net.sf.gridarta.model.archetype.Archetype; 030import net.sf.gridarta.model.gameobject.GameObject; 031import net.sf.gridarta.model.gameobject.NotInsideContainerException; 032import net.sf.gridarta.model.maparchobject.MapArchObject; 033import net.sf.gridarta.model.mapmodel.MapSquare; 034import org.jetbrains.annotations.NotNull; 035import org.jetbrains.annotations.Nullable; 036 037/** 038 * Base class for classes that contain GameObjects as children in the sense of 039 * containment. The interface serves 2 main purposes: <ul> <li>{@link 040 * GameObject} extends this class for containing other GameObjects, like 041 * inventory contents e.g. of a bag or attached events.</li> <li>{@link 042 * MapSquare} extends this class to list the squares on a MapSquare.</li> </ul> 043 * @author <a href="mailto:cher@riedquat.de">Christian Hujer</a> 044 * @todo In case of MapSquares, this class is most likely bogus regarding 045 * multi-part objects. This needs to be fixed. 046 */ 047@SuppressWarnings("ClassReferencesSubclass") 048public abstract class GameObjectContainer<G extends GameObject<G, A, R>, A extends MapArchObject<A>, R extends Archetype<G, A, R>> implements Cloneable, Iterable<G>, Serializable { 049 050 /** 051 * The serial version UID. 052 */ 053 private static final long serialVersionUID = 1L; 054 055 /** 056 * The contents of this container. 057 * @note the order of this container is bottom to top. 058 * @serial 059 */ 060 @NotNull 061 private List<G> contents; 062 063 /** 064 * {@link Iterable} implementation for recursive traversal. 065 */ 066 @NotNull 067 private transient Iterable<G> recursive; 068 069 /** 070 * {@link Iterable} implementation for reverse traversal. 071 */ 072 @NotNull 073 private transient Iterable<G> reverse; 074 075 /** 076 * Create a new GameObjectContainer. 077 */ 078 protected GameObjectContainer() { 079 initData(); 080 } 081 082 /** 083 * Initialize the fields. This is needed because there are two ways of 084 * object construction, <ul> <li>regular object construction via constructor 085 * invocation and</li> <li>cloning of objects via {@link #clone()}.</li> 086 * </ul> 087 */ 088 private void initData() { 089 contents = new ArrayList<G>(0); 090 recursive = new Iterable<G>() { 091 092 @Override 093 public Iterator<G> iterator() { 094 return new RecursiveGameObjectIterator<G, A, R>(GameObjectContainer.this); 095 } 096 097 }; 098 reverse = new Iterable<G>() { 099 100 @Override 101 public Iterator<G> iterator() { 102 return new ReverseIterator<G>(contents); 103 } 104 105 }; 106 } 107 108 /** 109 * {@inheritDoc} The Iterator returned does not recurse, it only contains 110 * objects on the first level. The Iterator returned is transparent, that 111 * means modifying the iterator's collection actually modifies the 112 * underlying GameObjectContainer. 113 */ 114 @NotNull 115 @Override 116 public Iterator<G> iterator() { 117 // Do not return contents.iterator() directly because otherwise the remove() operation will not properly unlink the removed GameObject from its container. 118 return new Iterator<G>() { 119 120 /** The basic iterator. */ 121 @NotNull 122 private final Iterator<G> delegate = contents.iterator(); 123 124 /** Current element (last element returned by {@link #next()}). */ 125 @Nullable 126 private G current; 127 128 @Override 129 public boolean hasNext() { 130 return delegate.hasNext(); 131 } 132 133 @Override 134 public G next() { 135 current = delegate.next(); 136 assert current != null; 137 return current; 138 } 139 140 @Override 141 public void remove() { 142 final G tmp = current; 143 if (tmp == null) { 144 throw new IllegalStateException(); 145 } 146 // keep this in sync with GameObjectContainer#remove(G) 147 // we can't simply invoke GameObjectContainer#remove(current) because that would result in a ConcurrentModificationException. 148 notifyBeginChange(); 149 try { 150 if (contents.size() >= 2 && contents.get(0) == tmp) { 151 contents.get(1).propagateElevation(tmp); 152 } 153 delegate.remove(); 154 tmp.setContainer(null, 0, 0); 155 current = null; 156 } finally { 157 notifyEndChange(); 158 } 159 } 160 }; 161 } 162 163 /** 164 * Return an object that is the reverse representation. Invoke this method 165 * if you want to iterate over the contained GameObjects in reverse order. 166 * @return reverse representation 167 */ 168 @NotNull 169 public Iterable<G> reverse() { 170 return reverse; 171 } 172 173 /** 174 * Return an object that is a recursive representation. Invoke this method 175 * if you want to iterate over the contained GameObjects recursively. 176 * @return recursive representation 177 */ 178 @NotNull 179 public Iterable<G> recursive() { 180 return recursive; 181 } 182 183 /** 184 * Check whether this square is empty. 185 * @return <code>true</code> if this square is empty, otherwise 186 * <code>false</code> 187 */ 188 public boolean isEmpty() { 189 return contents.isEmpty(); 190 } 191 192 /** 193 * Return the first GameObject contained in this container. 194 * @return first GameObject contained or <code>null</code> if {@link 195 * #isEmpty()} returns <code>true</code> 196 */ 197 @Nullable 198 public G getFirst() { 199 return contents.isEmpty() ? null : contents.get(0); 200 } 201 202 /** 203 * Return the {@link GameObject} preceding a given game object. 204 * @param gameObject the given game object 205 * @return the preceding game object or <code>null</code> if no preceding 206 * game object exists. 207 */ 208 @Nullable 209 public G getPrev(@NotNull final G gameObject) { 210 final Iterator<G> it = contents.iterator(); 211 while (it.hasNext()) { 212 if (it.next() == gameObject) { 213 return it.hasNext() ? it.next() : null; 214 } 215 } 216 return null; 217 } 218 219 /** 220 * Return the {@link GameObject} succeeding a given game object. 221 * @param gameObject the given game object 222 * @return the successor game object or <code>null</code> if no successor 223 * game object exists. 224 */ 225 @Nullable 226 public G getNext(@NotNull final G gameObject) { 227 G prevGameObject = null; 228 for (final G tmpGameObject : contents) { 229 if (tmpGameObject == gameObject) { 230 return prevGameObject; 231 } 232 prevGameObject = tmpGameObject; 233 } 234 return null; 235 } 236 237 /** 238 * Return the last GameObject contained in this container. You should not 239 * invoke this method to iterate over GameObjects, such invocation is 240 * regarded deprecated. 241 * @return last GameObject contained or <code>null</code> if {@link 242 * #isEmpty()} returns <code>true</code> 243 * @noinspection TypeMayBeWeakened 244 */ 245 @Nullable 246 public G getLast() { 247 return contents.isEmpty() ? null : contents.get(contents.size() - 1); 248 } 249 250 /** 251 * Remove a GameObject from this container. 252 * @param gameObject GameObject to remove 253 * @throws IllegalArgumentException if <var>gameObject</var> isn't in this 254 * container 255 * @fixme this implementation does not take care of multi square objects. 256 */ 257 public void remove(@NotNull final G gameObject) { 258 // keep this in sync with iterator() 259 notifyBeginChange(); 260 try { 261 if (contents.size() >= 2 && contents.get(0) == gameObject) { 262 contents.get(1).propagateElevation(gameObject); 263 } 264 if (!contents.remove(gameObject)) { 265 throw new NotInsideContainerException(this, gameObject); 266 } 267 gameObject.setContainer(null, 0, 0); 268 } finally { 269 notifyEndChange(); 270 } 271 } 272 273 /** 274 * Removes all GameObjects from this container. 275 * @fixme this implementation does not take care of multi square objects. 276 */ 277 public void removeAll() { 278 if (contents.size() <= 0) { 279 return; 280 } 281 282 notifyBeginChange(); 283 try { 284 for (final GameObject<G, A, R> gameObject : contents) { 285 gameObject.setContainer(null, 0, 0); 286 } 287 contents.clear(); 288 } finally { 289 notifyEndChange(); 290 } 291 } 292 293 /** 294 * Returns whether this game object is the top-most one. 295 * @param gameObject item to move to top 296 * @return whether this game object is the top-most one 297 * @throws IllegalArgumentException if <var>gameObject</var> isn't in this 298 * container 299 */ 300 public boolean isTop(@NotNull final G gameObject) { 301 return !contents.isEmpty() && contents.get(contents.size() - 1) == gameObject; 302 } 303 304 /** 305 * Returns whether this game object is the bottom-most one. 306 * @param gameObject item to move to top 307 * @return whether this game object is the bottom-most one 308 * @throws IllegalArgumentException if <var>gameObject</var> isn't in this 309 * container 310 */ 311 public boolean isBottom(@NotNull final G gameObject) { 312 return !contents.isEmpty() && contents.get(0) == gameObject; 313 } 314 315 /** 316 * Move an item to top. 317 * @param gameObject item to move to top 318 * @throws IllegalArgumentException if <var>gameObject</var> isn't in this 319 * container 320 */ 321 public void moveTop(@NotNull final G gameObject) { 322 final int oldIndex = contents.indexOf(gameObject); 323 if (oldIndex != contents.size() - 1) { 324 notifyBeginChange(); 325 try { 326 if (!contents.remove(gameObject)) { 327 throw new NotInsideContainerException(this, gameObject); 328 } 329 if (oldIndex == 0) { 330 assert contents.size() >= 1; 331 contents.get(0).propagateElevation(gameObject); 332 } 333 contents.add(gameObject); 334 } finally { 335 notifyEndChange(); 336 } 337 } 338 } 339 340 /** 341 * Move an item up. 342 * @param gameObject item to move up 343 * @throws IllegalArgumentException if <var>gameObject</var> isn't in this 344 * container 345 */ 346 public void moveUp(@NotNull final G gameObject) { 347 final int oldIndex = contents.indexOf(gameObject); 348 if (oldIndex < contents.size() - 1) { 349 notifyBeginChange(); 350 try { 351 if (!contents.remove(gameObject)) { 352 throw new NotInsideContainerException(this, gameObject); 353 } 354 if (oldIndex == 0) { 355 assert contents.size() >= 1; 356 contents.get(0).propagateElevation(gameObject); 357 } 358 contents.add(oldIndex + 1, gameObject); 359 } finally { 360 notifyEndChange(); 361 } 362 } 363 } 364 365 /** 366 * Move an item down. 367 * @param gameObject item to move down 368 * @throws IllegalArgumentException if <var>gameObject</var> isn't in this 369 * container 370 */ 371 public void moveDown(@NotNull final G gameObject) { 372 final int oldIndex = contents.indexOf(gameObject); 373 if (oldIndex > 0) { 374 notifyBeginChange(); 375 try { 376 if (!contents.remove(gameObject)) { 377 throw new NotInsideContainerException(this, gameObject); 378 } 379 if (oldIndex == 1) { 380 assert contents.size() >= 1; 381 gameObject.propagateElevation(contents.get(0)); 382 } 383 contents.add(oldIndex - 1, gameObject); 384 } finally { 385 notifyEndChange(); 386 } 387 } 388 } 389 390 /** 391 * Move an item to bottom. 392 * @param gameObject item to move to bottom 393 * @throws IllegalArgumentException if <var>gameObject</var> isn't in this 394 * container 395 */ 396 public void moveBottom(@NotNull final G gameObject) { 397 final int oldIndex = contents.indexOf(gameObject); 398 if (oldIndex != 0) { 399 notifyBeginChange(); 400 try { 401 if (!contents.remove(gameObject)) { 402 throw new NotInsideContainerException(this, gameObject); 403 } 404 assert contents.size() >= 1; 405 gameObject.propagateElevation(contents.get(0)); 406 contents.add(0, gameObject); 407 } finally { 408 notifyEndChange(); 409 } 410 } 411 } 412 413 /** 414 * Add the given GameObject at the end of this Container. 415 * @param gameObject the free yet unlinked <code>GameObject</code> to be 416 * placed in the inventory 417 * @throws IllegalArgumentException if <var>gameObject</var> already is 418 * inside another container 419 */ 420 public void addLast(@NotNull final G gameObject) { 421 if (gameObject.isInContainer()) { 422 throw new IllegalArgumentException("Can't add " + gameObject + " to " + this + " because it's already inside " + gameObject.getContainer()); 423 } 424 425 notifyBeginChange(); 426 try { 427 contents.add(gameObject); 428 setThisContainer(gameObject); 429 gameObject.markModified(); 430 } finally { 431 notifyEndChange(); 432 } 433 } 434 435 /** 436 * Add the given GameObject at the end of this Container. 437 * @param gameObject the free yet unlinked <code>GameObject</code> to be 438 * placed in the inventory 439 * @throws IllegalArgumentException if <var>gameObject</var> already is 440 * inside another container 441 */ 442 public void addFirst(@NotNull final G gameObject) { 443 if (gameObject.isInContainer()) { 444 throw new IllegalArgumentException("Can't add " + gameObject + " to " + this + " because it's already inside " + gameObject.getContainer()); 445 } 446 447 notifyBeginChange(); 448 try { 449 if (!contents.isEmpty()) { 450 gameObject.propagateElevation(contents.get(0)); 451 } 452 contents.add(0, gameObject); 453 setThisContainer(gameObject); 454 gameObject.markModified(); 455 } finally { 456 notifyEndChange(); 457 } 458 } 459 460 /** 461 * Add a GameObject after another. 462 * @param previousGameObject previous anchor or <code>null</code> to insert 463 * last 464 * @param gameObject GameObject to insert 465 * @throws IllegalArgumentException if <var>gameObject</var> already is 466 * inside another container or <var>previousGameObject</var> isn't inside 467 * this container 468 * @noinspection TypeMayBeWeakened 469 */ 470 public void insertAfter(@Nullable final G previousGameObject, @NotNull final G gameObject) { 471 if (gameObject.isInContainer()) { 472 throw new IllegalArgumentException("Can't add " + gameObject + " to " + this + " because it's already inside " + gameObject.getContainer()); 473 } 474 475 if (previousGameObject == null) { 476 addLast(gameObject); 477 return; 478 } 479 480 if (!previousGameObject.isHead()) { 481 throw new IllegalArgumentException(); 482 } 483 484 notifyBeginChange(); 485 try { 486 boolean added = false; 487 int index = 0; 488 for (final G tmpGameObject : contents) { 489 if (tmpGameObject.getHead() == previousGameObject) { 490 if (index == 0) { 491 gameObject.propagateElevation(contents.get(0)); 492 } 493 contents.add(index, gameObject); 494 added = true; 495 break; 496 } 497 index++; 498 } 499 if (!added) { 500 throw new IllegalArgumentException("Can't add " + gameObject + " to " + this + " because " + previousGameObject + " is not inside"); 501 } 502 setThisContainer(gameObject); 503 gameObject.markModified(); 504 } finally { 505 notifyEndChange(); 506 } 507 } 508 509 /** 510 * Add a GameObject before another. 511 * @param gameObject GameObject to insert 512 * @param nextGameObject nextGameObject anchor or <code>null</code> to add 513 * first 514 * @throws IllegalArgumentException if <var>gameObject</var> already is 515 * inside another container or <var>prev</var> isn't inside this container 516 */ 517 public void insertBefore(@NotNull final G gameObject, @Nullable final G nextGameObject) { 518 if (gameObject.isInContainer()) { 519 throw new IllegalArgumentException("Can't add " + gameObject + " to " + this + " because it's already inside " + gameObject.getContainer()); 520 } 521 522 if (nextGameObject == null) { 523 addFirst(gameObject); 524 return; 525 } 526 527 if (!nextGameObject.isHead()) { 528 throw new IllegalArgumentException(); 529 } 530 531 notifyBeginChange(); 532 try { 533 final int insertIndex = contents.indexOf(nextGameObject); 534 if (insertIndex == -1) { 535 throw new IllegalArgumentException("Can't insert " + gameObject + " before " + nextGameObject + " because that isn't inside " + this); 536 } 537 contents.add(insertIndex + 1, gameObject); 538 setThisContainer(gameObject); 539 gameObject.markModified(); 540 } finally { 541 notifyEndChange(); 542 } 543 } 544 545 /** 546 * Replace an GameObject with another one. 547 * @param oldGameObject old GameObject to be replaced 548 * @param newGameObject new GameObject that replaces oldGameObject 549 * @throws IllegalArgumentException if <var>oldGameObject</var> isn't in 550 * this container or is a multi-part object 551 */ 552 public void replace(@NotNull final G oldGameObject, @NotNull final G newGameObject) { 553 if (oldGameObject.isMulti()) { 554 throw new IllegalArgumentException(); 555 } 556 557 notifyBeginChange(); 558 try { 559 final int insertIndex = contents.indexOf(oldGameObject); 560 if (insertIndex == -1) { 561 throw new NotInsideContainerException(this, oldGameObject); 562 } 563 if (insertIndex == 0) { 564 newGameObject.propagateElevation(oldGameObject); 565 } 566 contents.remove(oldGameObject); 567 oldGameObject.setContainer(null, 0, 0); 568 contents.add(insertIndex, newGameObject); 569 setThisContainer(newGameObject); 570 newGameObject.markModified(); 571 } finally { 572 notifyEndChange(); 573 } 574 } 575 576 /** 577 * Returns the {@link MapSquare} of this container. 578 * @return the map square of this container or <code>null</code> if this 579 * GameObjectContainer is not (yet?) connected to a map (a {@link 580 * MapSquare} would return itself) 581 */ 582 @Nullable 583 public abstract MapSquare<G, A, R> getMapSquare(); 584 585 /** 586 * Notify the map model that this container is about to change. 587 */ 588 protected abstract void notifyBeginChange(); 589 590 /** 591 * Notify the map model that this container has changed. 592 */ 593 protected abstract void notifyEndChange(); 594 595 /** 596 * {@inheritDoc} 597 */ 598 @NotNull 599 @Override 600 @SuppressWarnings("unchecked") 601 protected Object clone() { 602 try { 603 //noinspection OverriddenMethodCallDuringObjectConstruction 604 final GameObjectContainer<G, A, R> clone = (GameObjectContainer<G, A, R>) super.clone(); 605 clone.initData(); 606 for (final BaseObject<G, A, R, G> gameObject : contents) { 607 final G clonedGameObject = gameObject.clone(); 608 clone.contents.add(clonedGameObject); 609 clone.setThisContainer(clonedGameObject); 610 } 611 return clone; 612 } catch (final CloneNotSupportedException e) { 613 assert false : "This class must be cloneable" + e; 614 throw new AssertionError(e); 615 } 616 } 617 618 // writeObject() is not required because this class doesn't require special handling during serialization. 619 620 private void readObject(@NotNull final ObjectInputStream stream) throws IOException, ClassNotFoundException { 621 stream.defaultReadObject(); 622 // initialize transients 623 initData(); 624 } 625 626 /** 627 * Compare this object to another game object container. 628 * @param gameObjectContainer the other game object container 629 * @return <code>true</code> if this object equals the other object 630 */ 631 public boolean hasSameContents(@NotNull final GameObjectContainer<?, ?, ?> gameObjectContainer) { 632 if (gameObjectContainer.contents.size() != contents.size()) { 633 return false; 634 } 635 636 for (int i = 0; i < contents.size(); i++) { 637 if (!gameObjectContainer.contents.get(i).isEqual(contents.get(i))) { 638 return false; 639 } 640 } 641 642 return true; 643 } 644 645 /** 646 * Sets a {@link GameObject}'s container to this container. 647 * @param gameObject the game object 648 */ 649 protected abstract void setThisContainer(@NotNull final G gameObject); 650 651 /** 652 * Returns this instance as a {@link GameObject} or <code>null</code> if 653 * this instance is not a game object. 654 * @return this instance or <code>null</code> 655 */ 656 @Nullable 657 public abstract G asGameObject(); 658 659 /** 660 * {@inheritDoc} 661 */ 662 @NotNull 663 @Override 664 public String toString() { 665 final StringBuilder sb = new StringBuilder(); 666 sb.append("("); 667 final Iterator<G> it = contents.iterator(); 668 if (it.hasNext()) { 669 sb.append(it.next()); 670 while (it.hasNext()) { 671 sb.append(","); 672 sb.append(it.next()); 673 } 674 } 675 sb.append(")"); 676 return sb.toString(); 677 } 678 679 /** 680 * An iterator for iterating over a list in reverse order. 681 * @todo move this class to JAPI 682 */ 683 private static class ReverseIterator<T extends GameObject<?, ?, ?>> implements Iterator<T> { 684 685 /** 686 * The iterator used for delegation. 687 */ 688 @NotNull 689 private final ListIterator<T> delegate; 690 691 /** 692 * The list being iterated over. 693 */ 694 @NotNull 695 private final List<T> list; 696 697 /** 698 * Create a reverse iterator. 699 * @param list to iterate over in reverse order 700 */ 701 private ReverseIterator(@NotNull final List<T> list) { 702 this.list = list; 703 delegate = list.listIterator(list.size()); 704 } 705 706 @Override 707 public boolean hasNext() { 708 return delegate.hasPrevious(); 709 } 710 711 // previous can throw NoSuchElementException but InspectionGadgets doesn't know about that. 712 @SuppressWarnings("IteratorNextCanNotThrowNoSuchElementException") 713 @NotNull 714 @Override 715 public T next() { 716 return delegate.previous(); 717 } 718 719 @Override 720 public void remove() { 721 if (delegate.nextIndex() == 0 && list.size() >= 2) { 722 list.get(1).propagateElevation(list.get(0)); 723 } 724 delegate.remove(); 725 } 726 727 } 728 729}