commit c2117ee86d4f8b59fd9201d51987714ab1b8d7e1 Author: Michael Bayne Date: Fri Jun 23 18:07:28 2006 +0000 Behold, Nenya, Ring of Water and repository for our media and animation related goodies, both Java 2D and LWJGL/JME 3D. git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@1 ed5b42cb-e716-0410-a449-f6a68f950b19 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..3b204400 --- /dev/null +++ b/LICENSE @@ -0,0 +1,458 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/README b/README new file mode 100644 index 00000000..cf9565e5 --- /dev/null +++ b/README @@ -0,0 +1,69 @@ +The Nenya library +----------------- + +The Nenya library provides various facilities for making networked multiplayer +games. It's various packages include: + + * geom, util - basic tools for doing data structure manipulation and some + geometry math + * resource - tools for bundling, deploying and managing media (images, + sounds, etc.) with a game + * media - a framework for doing "active" rendering in Java + * media.image - tools for loading, caching, manipulating and displaying images + * media.sound - tools for loading, caching, and playing audio + * media.animation, media.sprite - works in concert with the active + rendering system and provides tools for defining and manipulating + sprites (graphical entities that follow paths) and animations + (graphical entities that affect the display in other ways) + * miso - a framework for defining and displaying isometrically rendered scenes + * cast - a framework for defining and using recolorable, composited + characters with different poses and actions + +Documentation is somewhat sparse at the moment, but inspection of the code in +the tests/ directory shows examples of use of many features of the library. + +Building +-------- + +Building the library is very simple. First ensure that the necessary third +party jar files are available in the lib/ directory. See lib/README for a list +of the necessary third party jar files and how to get them. + +The library is built using Ant, a modern build tool written in and for Java. If +you aren't already using Ant for other projects, it can be found here: + + http://ant.apache.org/ + +Invoke ant with any of the following targets: + + all: builds the distribution files and javadoc documentation + compile: builds only the class files (dist/classes) + javadoc: builds only the javadoc documentation (dist/docs) + dist: builds the distribution jar files (dist/*.jar) + +Distribution +------------ + +The Nenya library is released under the LGPL. The most recent version of the +library is available here: + + http://www.threerings.net/code/nenya/ + +Contribution +------------ + +Contributions to Nenya are welcome. Read-only access to the subversion +repository is available at: + +svn://code.threerings.net/narya/trunk + +Patches can be mailed to Michael Bayne . + +Contact Information +------------------- + +Nenya is actively developed by the scurvy dogs at Three Rings Design, Inc. +Contact Michael Bayne with questions, comments and other +wordly endeavors. + +$Id$ diff --git a/build.xml b/build.xml new file mode 100644 index 00000000..71808eec --- /dev/null +++ b/build.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/code b/code new file mode 120000 index 00000000..4c91d264 --- /dev/null +++ b/code @@ -0,0 +1 @@ +src/java/com/threerings \ No newline at end of file diff --git a/docs/media/design.txt b/docs/media/design.txt new file mode 100644 index 00000000..91ea2506 --- /dev/null +++ b/docs/media/design.txt @@ -0,0 +1,44 @@ +Media Design Notes -*- mode: outline -*- + +* Tile system +** Classes +Tile +TileSet + +TileManager +TileSetRepository + +TileSetIDBroker + +XMLTileSetParser + +TileSetBundle +BundledTileSetRepository +TileSetBundler + +** Tile management +Managing tilesets by id involves creating tileset bundles from XML tileset +descriptions. + +Tileset bundles contain both the tileset images and the tileset metadata +(in the form of serialized tileset classes?) + +Tileset bundle creation maintains a stable tileset name to tileset id +mapping through the use of a TileSetIDBroker which maintains the name to +id mapping (in a database or however it likes) + +Sample tileset XML description (for uniform tiles): + + + foo/bar/tiles.png + 64 + 48 + 16 + + +* 01/07/2003 +** Image management refactor +x TileSet:ImageProvider needs to provide colorized images +! Probably give tilesets the ImageManager since they have to give the + ImageManager reference to the Tile so that it can lock its colorized + source image diff --git a/docs/media/notes.txt b/docs/media/notes.txt new file mode 100644 index 00000000..6bb95835 --- /dev/null +++ b/docs/media/notes.txt @@ -0,0 +1,14 @@ +Media Notes -*- mode: outline -*- + +* Old notes +- Possibly modify TileSet to load images via image manager (move image + provider up to media so that image manager can use tileset's image + provider to load the image data) so that they can be cached (and more + importantly, flushed from the cache). Presently tilesets are loaded into + memory when tileset bundles are loaded and remain loaded forever. + +* 07/19/2002 +** Virtual media panel +- Make sure doLayout() is getting called soon enough to ensure that we + don't improperly scroll the wrong thing if we're hijacked while in the + middle of scrolling diff --git a/docs/miso/design.txt b/docs/miso/design.txt new file mode 100644 index 00000000..7d07b42e --- /dev/null +++ b/docs/miso/design.txt @@ -0,0 +1,139 @@ +# Miso Notes -*- mode: outline -*- +# +# $Id: design.txt 2236 2003-02-04 03:33:38Z mdb $ + +* Notes +Consider making MISO sufficiently sophisticated to allow the cluster stuff +to be implemented as an extension. This would included allowing the XML +scene stuff to allow extensions, the scene editor to allow extensions and +of course extending the MisoScene class itself. + +* Discussion +Date: 07/10/01 +Venue: General discussion in vast sweeping loft area. +Participants: mdb, shaper. + +Main game play uses an isometric graphical view to display the current +game area. This section of the screen is referred to as the "scene" +hereafter. + +Targetting a screen resolution of 800x600. Toolbar and info areas may +be on the right side of the screen, below, or both, still TBD. Scene +display area will be either 600x600 or 640x480 correspondingly. Will +use JDK 1.2 or 1.3 allowing us to make use of Java 2D and the new Java +Sound APIs, among other happy things. + +Scenes are non-scrollable. Exits are expected to be placed largely, +though not exclusively, around the borders of the scene (esp. at +building entrances.) Moving the mouse over an exit causes an arrow to +appear flashing to illustrate that clicking on the exit will move the +player to a new scene. + +Characters will be displayed in the scene using "fine" coordinates (an +additional grid within each tile) to allow them to move smoothly. + +TBD: art details, how many frames of animation, what kinds of things +can be carried, paper-doll-ing of character clothing, possibility of +color-replacement to allow increased re-use of clothing art. + +* Design +** Scene Description +- sid: unique scene identifier +- name: scene name +- version: scene description file format version +- tile array: full NxM set of tiles for L0, sparse for L1+ +- hot spots: the "zones" wherein people congregate in each scene, a set + of scene coords +- exits: set of scene coords and associated sids that the exits lead to + +** Potential Classes +SceneIDBroker: dynamic scene creation and obtaining/tracking scene ids +SceneManager: scene retrieval, caching +TileManager: tile retrieval, caching +Config: tileset/tile info + +** Tile Sets and Tiles +General high-level categories of tiles: + +3 * outside (three prospective themes) + ship } item (potentially present in all) + house + +Tiles will be identified by a combination of a 16-bit tile set id and +a 16-bit tile id within that set. + +Estimating: + +400 tiles per scene +1000 scenes = (approx) 1.6 MB (highly compressible) + +Tileset config info associated with each tile set includes, for each +row of tiles in the tile set, the "row height" (height of the tiles in +that row), and the "tile count" (number of tiles in that row). + +Buildings in the scene, as an example, will then be comprised of a +contiguous grouping of tiles that are the same width as all other +tiles, but of greater height than the ground tiles. The scene editor +tool could automate the placement of buildings by allowing pre-defined +groupings of tiles and facilitating rotating building tile entities, +etc. as needed, but we'll wait on doing fancy things like this +until/unless we decide we need it since it's not yet clear that we +need particularly fancy scene design tools. + +Note that the scene editing tools should be designed such that they +can be used in part within the game itself, since players will be +allowed some degree of freedom w.r.t. designing and placing their own +homes (and perhaps landscaping?). + +Tilesets are intended to be used as an entity that contains tiles +corresponding to whatever logical groupings seem reasonable. E.g., +perhaps one tileset will contain all "ground" tiles; "spanish house" +tiles; etc. Split tilesets as necessary (multiple ground tilesets +if/when we have enough to warrant it.) Perhaps number the tileset +identifiers into meaningful groupings, i.e., all ground tilesets have +a tileset identifier in the range of 0 - 999, all building in 2000 - +3000, etc. + +Character animation frames may be performed by passing fully qualified +tile id and number of frames to animate, frames must be contiguous +tiles thereafter. character images will likely be handled as a +special case above and beyond our general tile antics. + +Look into using PNG file format for images. PNG supported in JDK 1.3+ +and probably JAI 1.1 for earlier JDK versions. use PNG alpha-channel +rather than separate masks for handling image transparency. + +Look into perhaps using bitmap masks to reduce the drawable area +needed for drawing tiles (e.g. exclude corners of iso squares), but +only if noteworthy perf gain achieved. Perhaps other similar +worthwhile uses. + +** TBD +*** Resource mgmt +When is scene data and art loaded, how big are the chunks of data +downloaded, what's the user experience during download like. + +*** On-screen "map" display of the surrounding scenes +The scenes comprising a town, etc. potentially impacts how we allow +placement of exits in a scene since allowing folks to place exits to their +houses/bldgs willy-nilly may make map generation wacky. perhaps useful +for game designer use as well when laying things out and "surveying the +landscape." + +* 1/14/2003 +** Ground zero +- Render all ground and fringe tiles (unprepared) into a single offscreen + buffer and use that to repaint the ground instead of going from the + original tile images + +* 1/28/2003 +** Scene revamp +- Objects without actions are largely inconsequential (in the model, they + exist only as fqtid and coordinates) +- Objects with actions have extra data: + + the action string + + a location and orientation associated with the object (for building + facades this would be the entrance portal location, for "stations" + this would be where the user stands when occupying that station) +- Scene model is not used by anyone but the scene impl (so that more + sophisticated models and impls can come to exist) diff --git a/docs/miso/fringebits.png b/docs/miso/fringebits.png new file mode 100644 index 00000000..7601e231 Binary files /dev/null and b/docs/miso/fringebits.png differ diff --git a/docs/miso/render_sort_diagram.dia b/docs/miso/render_sort_diagram.dia new file mode 100644 index 00000000..3c784f63 Binary files /dev/null and b/docs/miso/render_sort_diagram.dia differ diff --git a/docs/miso/render_sort_diagram.png b/docs/miso/render_sort_diagram.png new file mode 100644 index 00000000..b0ee784d Binary files /dev/null and b/docs/miso/render_sort_diagram.png differ diff --git a/docs/miso/scene.ps b/docs/miso/scene.ps new file mode 100644 index 00000000..24cd531e --- /dev/null +++ b/docs/miso/scene.ps @@ -0,0 +1,51 @@ +% sort out scaling and translating +36 36 translate +/sf 540 640 div def +sf sf scale + +% width/height ratio +/whr 10 7.5 div def + +% center things vertically +0 whr 640 mul 576 sub 2 div translate + +% translate off to the left one tile and clip +64 neg 0 translate +64 0 640 576 rectclip + +% draw the up moving diagnals +/sx 0 def +/sy 288 def +/ex 384 def +/ey 576 def + +1 1 13 { + sx sy moveto + ex ey lineto + stroke + /sx sx 32 add def + /ex ex 32 add def + /sy sy 24 sub def + /ey ey 24 sub def +} for + +% draw the down moving diagnals +/sx 0 def +/sy 288 def +/ex 384 def +/ey 0 def + +1 1 13 { + sx sy moveto + ex ey lineto + stroke + /sx sx 32 add def + /ex ex 32 add def + /sy sy 24 add def + /ey ey 24 add def +} for + +% box the whole thing in +64 0 640 576 rectstroke + +showpage diff --git a/docs/miso/scenegroup.xml b/docs/miso/scenegroup.xml new file mode 100644 index 00000000..47086b22 --- /dev/null +++ b/docs/miso/scenegroup.xml @@ -0,0 +1,21 @@ + + + + + + + + + + Town Square + South + + + + + Harbour + West + + + + diff --git a/docs/stylesheet.css b/docs/stylesheet.css new file mode 100644 index 00000000..3f0592b8 --- /dev/null +++ b/docs/stylesheet.css @@ -0,0 +1,52 @@ +/* Javadoc style sheet */ + +/* Page background color */ +body { + background-color: #FFFFFF; + font-family: Arial, Helvetica, sans-serif; +} + +pre { + font-family: Courier New, monospace +} + +code { + font-family: Courier New, monospace +} + +/* Headings */ +h1 { font-size: 145% } + +/* Table colors */ +.TableHeadingColor { background: #CCCCFF } /* Dark mauve */ +.TableSubHeadingColor { background: #EEEEFF } /* Light mauve */ +.TableRowColor { background: #FFFFFF } /* White */ + +/* Font used in left-hand frame lists */ +.FrameTitleFont { font-size: 100%; font-family: Helvetica, Arial, sans-serif } +.FrameHeadingFont { font-size: 90%; font-family: Helvetica, Arial, sans-serif } +.FrameItemFont { font-size: 90%; font-family: Helvetica, Arial, sans-serif } + +/* Navigation bar fonts and colors */ +.NavBarCell1 { background-color:#EEEEFF;} /* Light mauve */ +.NavBarCell1Rev { background-color:#00008B;} /* Dark Blue */ +.NavBarFont1 { font-family: Arial, Helvetica, sans-serif; color:#000000;} +.NavBarFont1Rev { font-family: Arial, Helvetica, sans-serif; color:#FFFFFF;} + +.NavBarCell2 { + font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF; +} +.NavBarCell3 { + font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF; +} + +.example { + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + padding-right: 10px; + margin-left: 20px; + margin-right: 20px; + border: 1px solid black; + background-color: #FFFF99 +} diff --git a/lib/LIBS b/lib/LIBS new file mode 100644 index 00000000..01710013 --- /dev/null +++ b/lib/LIBS @@ -0,0 +1,17 @@ +commons-collections.jar +commons-digester.jar +commons-io.jar +jme-awt.jar +jme-bui.jar +jme-effects.jar +jme.jar +jme-model.jar +jme-sound.jar +jme-terrain.jar +lwjgl.jar +lwjgl_util.jar +narya-base.jar +narya-distrib.jar +retroweaver-all-1.2.2.jar +samskivert.jar +xml-writer.jar diff --git a/lib/README b/lib/README new file mode 100644 index 00000000..ebb92aa2 --- /dev/null +++ b/lib/README @@ -0,0 +1,26 @@ +Required external libraries +--------------------------- + +The following external Java libraries (JAR files) must be placed (or symlinked) +here in the lib/ directory, unless specifically noted as optional. + +* The samskivert library: + http://samskivert.com/code/samskivert/ + +* Apache Commons libraries: collections, digester and io + http://jakarta.apache.org/commons/ + +* Megginson Technologies' XMLWriter 0.2 library: + http://www.megginson.com/Software/xml-writer-0.2.zip + +* Retroweaver libraries for creating 1.4 compatible versions: + http://retroweaver.sourceforge.net/ + +* (Optional) jMonkeyEngine 3D scene graph toolkit built on LWJGL: + http://www.jmonkeyengine.com/ + +Once the jar files are in place (regardless of what they are named), the build +system will automatically include them in the compile-time classpath. + +-- +$Id$ diff --git a/lib/SOURCE_HEADER b/lib/SOURCE_HEADER new file mode 100644 index 00000000..1b4ae8af --- /dev/null +++ b/lib/SOURCE_HEADER @@ -0,0 +1,21 @@ +// +// $Id$ +// +// Nenya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/nenya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + diff --git a/src/java/com/threerings/cast/ActionCache.java b/src/java/com/threerings/cast/ActionCache.java new file mode 100644 index 00000000..fbf7a35f --- /dev/null +++ b/src/java/com/threerings/cast/ActionCache.java @@ -0,0 +1,42 @@ +// +// $Id: ActionCache.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +/** + * A mechanism for caching composited character action animations on disk. + */ +public interface ActionCache +{ + /** + * Fetches from the cache a composited set of images for a particular + * character for a particular action. + */ + public ActionFrames getActionFrames ( + CharacterDescriptor descrip, String action); + + /** + * Requests that the specified set of action frames for the specified + * character be cached. + */ + public void cacheActionFrames ( + CharacterDescriptor descrip, String action, ActionFrames frames); +} diff --git a/src/java/com/threerings/cast/ActionFrames.java b/src/java/com/threerings/cast/ActionFrames.java new file mode 100644 index 00000000..8fe2bb82 --- /dev/null +++ b/src/java/com/threerings/cast/ActionFrames.java @@ -0,0 +1,67 @@ +// +// $Id: ActionFrames.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import com.threerings.media.image.Colorization; +import com.threerings.util.DirectionCodes; + +/** + * Encapsulates a set of frames in each of {@link + * DirectionCodes#DIRECTION_COUNT} orientations that are used to render a + * character sprite. + */ +public interface ActionFrames +{ + /** + * Returns the number of orientations available in this set of action + * frames. + */ + public int getOrientationCount (); + + /** + * Returns the multi-frame image that comprises the frames for the + * specified orientation. + */ + public TrimmedMultiFrameImage getFrames (int orient); + + /** + * Returns the x offset from the upper left of the image to the + * "origin" for this character frame. A sprite with location (x, y) + * will be rendered such that its origin is coincident with that + * location. + */ + public int getXOrigin (int orient, int frameIdx); + + /** + * Returns the y offset from the upper left of the image to the + * "origin" for this character frame. A sprite with location (x, y) + * will be rendered such that its origin is coincident with that + * location. + */ + public int getYOrigin (int orient, int frameIdx); + + /** + * Creates a clone of these action frames which will have the supplied + * colorizations applied to the frame images. + */ + public ActionFrames cloneColorized (Colorization[] zations); +} diff --git a/src/java/com/threerings/cast/ActionSequence.java b/src/java/com/threerings/cast/ActionSequence.java new file mode 100644 index 00000000..a05b6fa1 --- /dev/null +++ b/src/java/com/threerings/cast/ActionSequence.java @@ -0,0 +1,72 @@ +// +// $Id: ActionSequence.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.awt.Point; +import java.io.Serializable; + +/** + * The action sequence class describes a particular character animation + * sequence. An animation sequence consists of one or more frames of + * animation, renders at a particular frame rate, and has an origin point + * that specifies the location of the base of the character in relation to + * the bounds of the animation images. + */ +public class ActionSequence implements Serializable +{ + /** + * Defines the name of the default action sequence. When component + * tilesets are loaded to build a set of composited images for a + * particular action sequence, a check is first made for a component + * tileset specific to the action sequence and then for the + * component's default tileset if the action specific tileset did not + * exist. + */ + public static final String DEFAULT_SEQUENCE = "default"; + + /** The action sequence name. */ + public String name; + + /** The number of frames per second to show when animating. */ + public float framesPerSecond; + + /** The position of the character's base for this sequence. */ + public Point origin = new Point(); + + /** Orientation codes for the orientations available for this + * action. */ + public int[] orients; + + /** + * Returns a string representation of this action sequence. + */ + public String toString () + { + return "[name=" + name + ", framesPerSecond=" + framesPerSecond + + ", origin=" + origin + + ", orients=" + (orients == null ? 0 : orients.length) + "]"; + } + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/cast/CastPrefs.java b/src/java/com/threerings/cast/CastPrefs.java new file mode 100644 index 00000000..c45a3053 --- /dev/null +++ b/src/java/com/threerings/cast/CastPrefs.java @@ -0,0 +1,35 @@ +// +// $Id: CastPrefs.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import com.samskivert.util.Config; + +/** + * Provides access to runtime configuration parameters for this package + * and its subpackages. + */ +public class CastPrefs +{ + /** Used to load our preferences from a properties file and map them + * to the persistent Java preferences repository. */ + public static Config config = new Config("rsrc/config/cast"); +} diff --git a/src/java/com/threerings/cast/CharacterComponent.java b/src/java/com/threerings/cast/CharacterComponent.java new file mode 100644 index 00000000..fc519d2d --- /dev/null +++ b/src/java/com/threerings/cast/CharacterComponent.java @@ -0,0 +1,100 @@ +// +// $Id: CharacterComponent.java 3740 2005-10-20 22:12:45Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.io.Serializable; + +import com.samskivert.util.StringUtil; + +import com.threerings.media.sprite.Sprite; + +/** + * The character component represents a single component that can be + * composited with other character components to generate an image + * representing a complete character displayable in any of the eight + * compass directions as detailed in the {@link Sprite} class direction + * constants. + */ +public class CharacterComponent implements Serializable +{ + /** The unique component identifier. */ + public int componentId; + + /** The component's name. */ + public String name; + + /** The class of components to which this one belongs. */ + public ComponentClass componentClass; + + /** + * Constructs a character component with the specified id of the + * specified class. + */ + public CharacterComponent (int componentId, String name, + ComponentClass compClass, FrameProvider fprov) + { + this.componentId = componentId; + this.name = name; + this.componentClass = compClass; + _frameProvider = fprov; + } + + /** + * Returns the image frames for the specified action animation or null if + * no animation for the specified action is available for this component. + * + * @param type null for the normal action frames or one of the custom + * action sub-types: {@link StandardActions#SHADOW_TYPE}, etc. + */ + public ActionFrames getFrames (String action, String type) + { + return _frameProvider.getFrames(this, action, type); + } + + /** + * Returns true if this component is equal to the other component. The + * comparison is made on componentId. + */ + public boolean equals (Object other) + { + if (other instanceof CharacterComponent) { + return componentId == ((CharacterComponent)other).componentId; + } else { + return false; + } + } + + /** + * Returns a string representation of this character component. + */ + public String toString () + { + return StringUtil.fieldsToString(this); + } + + /** The entity from which we obtain our animation frames. */ + protected FrameProvider _frameProvider; + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/cast/CharacterDescriptor.java b/src/java/com/threerings/cast/CharacterDescriptor.java new file mode 100644 index 00000000..b27b2e5e --- /dev/null +++ b/src/java/com/threerings/cast/CharacterDescriptor.java @@ -0,0 +1,144 @@ +// +// $Id: CharacterDescriptor.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.util.Arrays; +import com.samskivert.util.StringUtil; +import com.threerings.media.image.Colorization; + +/** + * The character descriptor object details the components that are + * pieced together to create a single character image. + */ +public class CharacterDescriptor +{ + /** + * Constructs a character descriptor. + * + * @param components the component ids of the individual components + * that make up this character. + * @param zations the colorizations to apply to each of the character + * component images when compositing actions (an array of + * colorizations for each component, elements of which can be null to + * make it easier to support per-component specialized colorizations). + * This can be null if image recolorization is not desired. + */ + public CharacterDescriptor (int[] components, Colorization[][] zations) + { + _components = components; + _zations = zations; + } + + /** + * Returns an array of the component identifiers comprising the + * character described by this descriptor. + */ + public int[] getComponentIds () + { + return _components; + } + + /** + * Returns an array of colorization arrays to be applied to the + * components when compositing action images (one array per + * component). + */ + public Colorization[][] getColorizations () + { + return _zations; + } + + /** + * Updates the colorizations to be used by this character descriptor. + */ + public void setColorizations (Colorization[][] zations) + { + _zations = zations; + } + + /** + * Compute a sensible hashcode for this object. + */ + public int hashCode () + { + int code = 0, clength = _components.length; + for (int i = 0; i < clength; i++) { + code ^= _components[i]; + } + return code; + } + + /** + * Compares this character descriptor to another. + */ + public boolean equals (Object other) + { + if (!(other instanceof CharacterDescriptor)) { + return false; + } + + // both the component ids and the colorizations must be equal + CharacterDescriptor odesc = (CharacterDescriptor)other; + if (!Arrays.equals(_components, odesc._components)) { + return false; + } + + Colorization[][] zations = odesc._zations; + if (zations == null && _zations == null) { + // if neither has colorizations, we're clear + return true; + + } else if (zations == null || _zations == null) { + // if one has colorizations whilst the other doesn't, they + // can't be equal + return false; + } + + // otherwise, all of the colorizations must be equal as well + int zlength = zations.length; + if (zlength != _zations.length) { + return false; + } + for (int ii = 0; ii < zlength; ii++) { + if (!Arrays.equals(_zations[ii], zations[ii])) { + return false; + } + } + + return true; + } + + /** + * Returns a string representation of this character descriptor. + */ + public String toString () + { + return "[cids=" + StringUtil.toString(_components) + + ", colors=" + StringUtil.toString(_zations) + "]"; + } + + /** The component identifiers comprising the character. */ + protected int[] _components; + + /** The colorizations to apply when compositing this character. */ + protected Colorization[][] _zations; +} diff --git a/src/java/com/threerings/cast/CharacterManager.java b/src/java/com/threerings/cast/CharacterManager.java new file mode 100644 index 00000000..55767c0e --- /dev/null +++ b/src/java/com/threerings/cast/CharacterManager.java @@ -0,0 +1,430 @@ +// +// $Id: CharacterManager.java 4022 2006-04-14 21:49:11Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.samskivert.swing.RuntimeAdjust; +import com.samskivert.util.LRUHashMap; +import com.samskivert.util.StringUtil; +import com.samskivert.util.Throttle; +import com.samskivert.util.Tuple; + +import com.threerings.media.image.Colorization; +import com.threerings.media.image.ImageManager; +import com.threerings.util.DirectionCodes; + +import com.threerings.cast.CompositedActionFrames.ComponentFrames; +import com.threerings.cast.Log; + +/** + * The character manager provides facilities for constructing sprites that + * are used to represent characters in a scene. It also handles the + * compositing and caching of composited character animations. + */ +public class CharacterManager + implements DirectionCodes +{ + /** + * Constructs the character manager. + */ + public CharacterManager (ImageManager imgr, ComponentRepository crepo) + { + // keep these around + _imgr = imgr; + _crepo = crepo; + + // populate our actions table + Iterator iter = crepo.enumerateActionSequences(); + while (iter.hasNext()) { + ActionSequence action = (ActionSequence)iter.next(); + _actions.put(action.name, action); + } + + // create a cache for our composited action frames + int acsize = _cacheSize.getValue(); + Log.debug("Creating action cache [size=" + acsize + "k]."); + _frameCache = new LRUHashMap(acsize*1024, new LRUHashMap.ItemSizer() { + public int computeSize (Object value) { + return (int)((CompositedMultiFrameImage) + value).getEstimatedMemoryUsage(); + } + }); + _frameCache.setTracking(true); // TODO + } + + /** + * Returns the component repository being used by this manager. + */ + public ComponentRepository getComponentRepository () + { + return _crepo; + } + + /** + * Instructs the character manager to construct instances of this + * derived class of {@link CharacterSprite} when creating new sprites. + * + * @exception IllegalArgumentException thrown if the supplied class + * does not derive from {@link CharacterSprite}. + */ + public void setCharacterClass (Class charClass) + { + // sanity check + if (!CharacterSprite.class.isAssignableFrom(charClass)) { + String errmsg = "Requested to use character sprite class that " + + "does not derive from CharacterSprite " + + "[class=" + charClass.getName() + "]."; + throw new IllegalArgumentException(errmsg); + } + + // make a note of it + _charClass = charClass; + } + + /** + * Instructs the character manager to use the provided cache for + * composited action animations. + */ + public void setActionCache (ActionCache cache) + { + _acache = cache; + } + + /** + * Returns a {@link CharacterSprite} representing the character + * described by the given {@link CharacterDescriptor}, or + * null if an error occurs. + * + * @param desc the character descriptor. + */ + public CharacterSprite getCharacter (CharacterDescriptor desc) + { + return getCharacter(desc, _charClass); + } + + /** + * Returns a {@link CharacterSprite} representing the character + * described by the given {@link CharacterDescriptor}, or + * null if an error occurs. + * + * @param desc the character descriptor. + * @param charClass the {@link CharacterSprite} derived class that + * should be instantiated instead of the configured default (which is + * set via {@link #setCharacterClass}). + */ + public CharacterSprite getCharacter (CharacterDescriptor desc, + Class charClass) + { + try { + CharacterSprite sprite = (CharacterSprite) + charClass.newInstance(); + sprite.init(desc, this); + return sprite; + + } catch (Exception e) { + Log.warning("Failed to instantiate character sprite " + + "[e=" + e + "]."); + Log.logStackTrace(e); + return null; + } + } + + /** + * Obtains the composited animation frames for the specified action for a + * character with the specified descriptor. The resulting composited + * animation will be cached. + * + * @exception NoSuchComponentException thrown if any of the components in + * the supplied descriptor do not exist. + * @exception IllegalArgumentException thrown if any of the components + * referenced in the descriptor do not support the specified action. + */ + public ActionFrames getActionFrames ( + CharacterDescriptor descrip, String action) + throws NoSuchComponentException + { + Tuple key = new Tuple(descrip, action); + ActionFrames frames = (ActionFrames)_actionFrames.get(key); + if (frames == null) { + // this doesn't actually composite the images, but prepares an + // object to be able to do so + frames = createCompositeFrames(descrip, action); + _actionFrames.put(key, frames); + } + + // periodically report our frame image cache performance + if (!_cacheStatThrottle.throttleOp()) { + long size = getEstimatedCacheMemoryUsage(); + int[] eff = _frameCache.getTrackedEffectiveness(); + Log.debug("CharacterManager LRU [mem=" + (size / 1024) + "k" + + ", size=" + _frameCache.size() + ", hits=" + eff[0] + + ", misses=" + eff[1] + "]."); + } + + return frames; + } + + /** + * Informs the character manager that the action sequence for the + * given character descriptor is likely to be needed in the near + * future and so any efforts that can be made to load it into the + * action sequence cache in advance should be undertaken. + * + *

This will eventually be revamped to spiffily load action + * sequences in the background. + */ + public void resolveActionSequence (CharacterDescriptor desc, String action) + { + try { + if (getActionFrames(desc, action) == null) { + Log.warning("Failed to resolve action sequence " + + "[desc=" + desc + ", action=" + action + "]."); + } + + } catch (NoSuchComponentException nsce) { + Log.warning("Failed to resolve action sequence " + + "[nsce=" + nsce + "]."); + } + } + + /** + * Returns the action sequence instance with the specified name or + * null if no such sequence exists. + */ + public ActionSequence getActionSequence (String action) + { + return (ActionSequence)_actions.get(action); + } + + /** + * Returns the estimated memory usage in bytes for all images + * currently cached by the cached action frames. + */ + protected long getEstimatedCacheMemoryUsage () + { + long size = 0; + Iterator iter = _frameCache.values().iterator(); + while (iter.hasNext()) { + size += ((CompositedMultiFrameImage) + iter.next()).getEstimatedMemoryUsage(); + } + return size; + } + + /** + * Generates the composited animation frames for the specified action + * for a character with the specified descriptor. + * + * @exception NoSuchComponentException thrown if any of the components + * in the supplied descriptor do not exist. + * @exception IllegalArgumentException thrown if any of the components + * referenced in the descriptor do not support the specified action. + */ + protected ActionFrames createCompositeFrames ( + CharacterDescriptor descrip, String action) + throws NoSuchComponentException + { + int[] cids = descrip.getComponentIds(); + int ccount = cids.length; + Colorization[][] zations = descrip.getColorizations(); + + Log.debug("Compositing action [action=" + action + + ", descrip=" + descrip + "]."); + + // this will be used to construct any shadow layers + HashMap shadows = null; + + // maps components by class name for masks + HashMap ccomps = new HashMap(); + + // create colorized versions of all of the source action frames + ArrayList sources = new ArrayList(ccount); + for (int ii = 0; ii < ccount; ii++) { + ComponentFrames cframes = new ComponentFrames(); + sources.add(cframes); + CharacterComponent ccomp = + (cframes.ccomp = _crepo.getComponent(cids[ii])); + ccomps.put(ccomp.componentClass.name, ccomp); + + // load up the main component images + ActionFrames source = ccomp.getFrames(action, null); + if (source == null) { + String errmsg = "Cannot composite action frames; no such " + + "action for component [action=" + action + + ", desc=" + descrip + ", comp=" + ccomp + "]"; + throw new RuntimeException(errmsg); + } + cframes.frames = (zations == null || zations[ii] == null) ? + source : source.cloneColorized(zations[ii]); + + // if this component has a shadow, make a note of it + if (ccomp.componentClass.isShadowed()) { + if (shadows == null) { + shadows = new HashMap(); + } + ArrayList shadlist = (ArrayList) + shadows.get(ccomp.componentClass.shadow); + if (shadlist == null) { + shadows.put(ccomp.componentClass.shadow, + shadlist = new ArrayList()); + } + shadlist.add(ccomp); + } + } + + // add any necessary masks + for (int ii = 0; ii < ccount; ii++) { + ComponentFrames cframes = (ComponentFrames)sources.get(ii); + CharacterComponent mcomp = (CharacterComponent)ccomps.get( + cframes.ccomp.componentClass.mask); + if (mcomp != null) { + cframes.frames = compositeMask(action, cframes.ccomp, + cframes.frames, mcomp); + } + } + + // now create any necessary shadow layers + if (shadows != null) { + Iterator iter = shadows.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = (Map.Entry)iter.next(); + String sclass = (String)entry.getKey(); + ArrayList scomps = (ArrayList)entry.getValue(); + ComponentFrames scf = compositeShadow(action, sclass, scomps); + if (scf != null) { + sources.add(scf); + } + } + } + + // use those to create an entity that will lazily composite things + // together as they are needed + ComponentFrames[] cfvec = (ComponentFrames[])sources.toArray( + new ComponentFrames[sources.size()]); + return new CompositedActionFrames(_imgr, _frameCache, action, cfvec); + } + + protected ActionFrames compositeMask ( + String action, CharacterComponent ccomp, ActionFrames cframes, + CharacterComponent mcomp) + { + ActionFrames mframes = mcomp.getFrames(action, + StandardActions.CROP_TYPE); + if (mframes == null) { + return cframes; + } + return new CompositedActionFrames( + _imgr, _frameCache, action, new ComponentFrames[] { + new ComponentFrames(ccomp, cframes), + new ComponentFrames(mcomp, mframes) }) { + protected CompositedMultiFrameImage createFrames (int orient) { + return new CompositedMaskedImage( + _imgr, _sources, _action, orient); + } + }; + } + + protected ComponentFrames compositeShadow ( + String action, String sclass, ArrayList scomps) + { + final ComponentClass cclass = _crepo.getComponentClass(sclass); + if (cclass == null) { + Log.warning("Components reference non-existent shadow layer " + + "class [sclass=" + sclass + + ", scomps=" + StringUtil.toString(scomps) + "]."); + return null; + } + + ComponentFrames cframes = new ComponentFrames(); + // create a fake component for the shadow layer + cframes.ccomp = new CharacterComponent(-1, "shadow", cclass, null); + + ArrayList sources = new ArrayList(); + for (int ii = 0, ll = scomps.size(); ii < ll; ii++) { + ComponentFrames source = new ComponentFrames(); + source.ccomp = (CharacterComponent)scomps.get(ii); + source.frames = source.ccomp.getFrames( + action, StandardActions.SHADOW_TYPE); + if (source.frames == null) { + // skip this shadow component + continue; + } + sources.add(source); + } + + // if we ended up with no shadow, no problem! + if (sources.size() == 0) { + return null; + } + + // create custom action frames that use a special compositing + // multi-frame image that does the necessary shadow magic + ComponentFrames[] svec = (ComponentFrames[]) + sources.toArray(new ComponentFrames[sources.size()]); + cframes.frames = new CompositedActionFrames( + _imgr, _frameCache, action, svec) { + protected CompositedMultiFrameImage createFrames (int orient) { + return new CompositedShadowImage( + _imgr, _sources, _action, orient, cclass.shadowAlpha); + } + }; + + return cframes; + } + + /** The image manager with whom we interact. */ + protected ImageManager _imgr; + + /** The component repository. */ + protected ComponentRepository _crepo; + + /** A table of our action sequences. */ + protected HashMap _actions = new HashMap(); + + /** A table of composited action sequences (these don't reference the + * actual image data directly and thus take up little memory). */ + protected HashMap _actionFrames = new HashMap(); + + /** A cache of composited animation frames. */ + protected LRUHashMap _frameCache; + + /** The character class to be created. */ + protected Class _charClass = CharacterSprite.class; + + /** The action animation cache, if we have one. */ + protected ActionCache _acache; + + /** Throttle our cache status logging to once every 30 seconds. */ + protected Throttle _cacheStatThrottle = new Throttle(1, 30000L); + + /** Register our image cache size with the runtime adjustments + * framework. */ + protected static RuntimeAdjust.IntAdjust _cacheSize = + new RuntimeAdjust.IntAdjust( + "Size (in kb of memory used) of the character manager LRU " + + "action cache [requires restart]", "narya.cast.action_cache_size", + CastPrefs.config, 1024); +} diff --git a/src/java/com/threerings/cast/CharacterSprite.java b/src/java/com/threerings/cast/CharacterSprite.java new file mode 100644 index 00000000..801452e3 --- /dev/null +++ b/src/java/com/threerings/cast/CharacterSprite.java @@ -0,0 +1,402 @@ +// +// $Id: CharacterSprite.java 3785 2005-12-15 22:14:37Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import javax.swing.SwingUtilities; + +import com.threerings.media.sprite.ImageSprite; + +/** + * A character sprite is a sprite that animates itself while walking + * about in a scene. + */ +public class CharacterSprite extends ImageSprite + implements StandardActions +{ + /** + * Initializes this character sprite with the specified character + * descriptor and character manager. It will obtain animation data + * from the supplied character manager. + */ + public void init (CharacterDescriptor descrip, CharacterManager charmgr) + { + // keep track of this stuff + _descrip = descrip; + _charmgr = charmgr; + + // sanity check our values + sanityCheckDescrip(); + + // assign an arbitrary starting orientation + _orient = SOUTHWEST; + + // pass the buck to derived classes + didInit(); + } + + /** + * Called after this sprite has been initialized with its character + * descriptor and character manager. Derived classes can do post-init + * business here. + */ + protected void didInit () + { + } + + /** + * Reconfigures this sprite to use the specified character descriptor. + */ + public void setCharacterDescriptor (CharacterDescriptor descrip) + { + // keep the new descriptor + _descrip = descrip; + + // sanity check our values + sanityCheckDescrip(); + + // update our action frames + updateActionFrames(); + } + + /** + * Specifies the action to use when the sprite is at rest. The default + * is STANDING. + */ + public void setRestingAction (String action) + { + _restingAction = action; + } + + /** + * Returns the action to be used when the sprite is at rest. Derived + * classes may wish to override this method and vary the action based + * on external parameters (or randomly). + */ + public String getRestingAction () + { + return _restingAction; + } + + /** + * Specifies the action to use when the sprite is following a path. + * The default is WALKING. + */ + public void setFollowingPathAction (String action) + { + _followingPathAction = action; + } + + /** + * Returns the action to be used when the sprite is following a path. + * Derived classes may wish to override this method and vary the + * action based on external parameters (or randomly). + */ + public String getFollowingPathAction () + { + return _followingPathAction; + } + + /** + * Sets the action sequence used when rendering the character, from + * the set of available sequences. + */ + public void setActionSequence (String action) + { + // sanity check + if (action == null) { + Log.warning("Refusing to set null action sequence " + this + "."); + Thread.dumpStack(); + return; + } + + // no need to noop + if (action.equals(_action)) { + return; + } + _action = action; + updateActionFrames(); + } + + // documentation inherited + public void setOrientation (int orient) + { + if (orient < 0 || orient >= FINE_DIRECTION_COUNT) { + Log.info("Refusing to set invalid orientation [sprite=" + this + + ", orient=" + orient + "]."); + Thread.dumpStack(); + return; + } + + int oorient = _orient; + super.setOrientation(orient); + if (_orient != oorient) { + _frames = null; + } + } + + // documentation inherited + public boolean hitTest (int x, int y) + { + // the irect adjustments are to account for our decorations + return (_frames != null && _ibounds.contains(x, y) && + _frames.hitTest(_frameIdx, x - _ibounds.x, y - _ibounds.y)); + } + + // documentation inherited + public void tick (long tickStamp) + { + // composite our action frames if something since the last call to + // tick caused them to become invalid + compositeActionFrames(); + super.tick(tickStamp); + // composite our action frames if something during tick() caused + // them to become invalid + compositeActionFrames(); + } + + // documentation inherited + public void cancelMove () + { + super.cancelMove(); + halt(); + } + + // documentation inherited + public void pathBeginning () + { + super.pathBeginning(); + + // enable walking animation + setAnimationMode(TIME_BASED); + setActionSequence(getFollowingPathAction()); + } + + // documentation inherited + public void pathCompleted (long timestamp) + { + super.pathCompleted(timestamp); + halt(); + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + if (_frames != null) { + decorateBehind(gfx); + // paint the image using _ibounds rather than _bounds which + // has been modified to include the bounds of our decorations + _frames.paintFrame(gfx, _frameIdx, _ibounds.x, _ibounds.y); + decorateInFront(gfx); + + } else { + super.paint(gfx); + } + } + + /** + * Called to paint any decorations that should appear behind the + * character sprite image. + */ + protected void decorateBehind (Graphics2D gfx) + { + } + + /** + * Called to paint any decorations that should appear in front of the + * character sprite image. + */ + protected void decorateInFront (Graphics2D gfx) + { + } + + /** + * Rebuilds our action frames given our current character descriptor + * and action sequence. This is called when either of those two things + * changes. + */ + protected void updateActionFrames () + { + // get a reference to the action sequence so that we can obtain + // our animation frames and configure our frames per second + ActionSequence actseq = _charmgr.getActionSequence(_action); + if (actseq == null) { + String errmsg = "No such action '" + _action + "'."; + throw new IllegalArgumentException(errmsg); + } + + try { + // obtain our animation frames for this action sequence + _aframes = _charmgr.getActionFrames(_descrip, _action); + + // clear out our frames so that we recomposite on next tick + _frames = null; + + // update the sprite render attributes + setFrameRate(actseq.framesPerSecond); + + } catch (NoSuchComponentException nsce) { + Log.warning("Character sprite references non-existent " + + "component [sprite=" + this + ", err=" + nsce + "]."); + + } catch (Exception e) { + Log.warning("Failed to obtain action frames [sprite=" + this + + ", descrip=" + _descrip + ", action=" + _action + "]."); + Log.logStackTrace(e); + } + } + + /** Called to recomposite our action frames if needed. */ + protected final void compositeActionFrames () + { + if (_frames == null && _aframes != null) { + setFrames(_aframes.getFrames(_orient)); + } + } + + /** + * Makes it easier to track down problems with bogus character descriptors. + */ + protected void sanityCheckDescrip () + { + if (_descrip.getComponentIds() == null || + _descrip.getComponentIds().length == 0) { + Log.warning("Invalid character descriptor [sprite=" + this + + ", descrip=" + _descrip + "]."); + Thread.dumpStack(); + } + } + + // documentation inherited + protected boolean tickPath (long tickStamp) + { + boolean moved = super.tickPath(tickStamp); + // composite our action frames if our path caused them to become + // invalid + compositeActionFrames(); + return moved; + } + + // documentation inherited + protected void updateRenderOrigin () + { + super.updateRenderOrigin(); + + // adjust our image bounds to reflect the new location + _ibounds.x = _bounds.x + _ioff.x; + _ibounds.y = _bounds.y + _ioff.y; + } + + // documentation inherited + protected void accomodateFrame (int frameIdx, int width, int height) + { + // this will update our width and height + super.accomodateFrame(frameIdx, width, height); + + // we now need to update the render offset for this frame + if (_aframes == null) { + Log.warning("Have no action frames! " + _aframes + "."); + } else { + _oxoff = _aframes.getXOrigin(_orient, frameIdx); + _oyoff = _aframes.getYOrigin(_orient, frameIdx); + } + + // and cause those changes to be reflected in our bounds + updateRenderOrigin(); + + // start out with our bounds the same as our image bounds + _ibounds.setBounds(_bounds); + + // now we can call down and incorporate the dimensions of any + // decorations that will be rendered along with our image + unionDecorationBounds(_bounds); + + // compute our new render origin + _oxoff = _ox - _bounds.x; + _oyoff = _oy - _bounds.y; + + // track the offset from our expanded bounds to our image bounds + _ioff.x = _ibounds.x - _bounds.x; + _ioff.y = _ibounds.y - _bounds.y; + } + + /** + * Called by {@link #accomodateFrame} to give derived classes an + * opportunity to incorporate the bounds of any decorations that will + * be drawn along with this sprite. The {@link #_ibounds} rectangle + * will contain the bounds of the image that comprises the undecorated + * sprite. From that the position and size of decorations can be + * computed and unioned with the supplied bounds rectangle (most + * likely by using {@link SwingUtilities#computeUnion} which applies + * the union in place rather than creating a new rectangle). + */ + protected void unionDecorationBounds (Rectangle bounds) + { + } + + /** + * Updates the sprite animation frame to reflect the cessation of + * movement and disables any further animation. + */ + protected void halt () + { + // only do something if we're actually animating + if (_animMode != NO_ANIMATION) { + // disable animation + setAnimationMode(NO_ANIMATION); + // come to a halt looking settled and at peace + String rest = getRestingAction(); + if (rest != null) { + setActionSequence(rest); + } + } + } + + /** The action to use when at rest. */ + protected String _restingAction = STANDING; + + /** The action to use when following a path. */ + protected String _followingPathAction = WALKING; + + /** A reference to the descriptor for the character that we're + * visualizing. */ + protected CharacterDescriptor _descrip; + + /** A reference to the character manager that created us. */ + protected CharacterManager _charmgr; + + /** The action we are currently displaying. */ + protected String _action; + + /** The animation frames for the active action sequence in each + * orientation. */ + protected ActionFrames _aframes; + + /** The offset from the upper-left of the total sprite bounds to the + * upper-left of the image within those bounds. */ + protected Point _ioff = new Point(); + + /** The bounds of the current sprite image. */ + protected Rectangle _ibounds = new Rectangle(); +} diff --git a/src/java/com/threerings/cast/ComponentClass.java b/src/java/com/threerings/cast/ComponentClass.java new file mode 100644 index 00000000..200e1241 --- /dev/null +++ b/src/java/com/threerings/cast/ComponentClass.java @@ -0,0 +1,241 @@ +// +// $Id: ComponentClass.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.awt.Color; +import java.io.Serializable; + +import com.samskivert.util.ArrayIntSet; +import com.samskivert.util.ComparableArrayList; +import com.samskivert.util.StringUtil; + +/** + * Denotes a class of components to which {@link CharacterComponent} + * objects belong. Examples include "Hat", "Head", and "Feet". A component + * class dictates a component's rendering priority so that components can + * be rendered in an order that causes them to overlap properly. + * + *

Components support render priority overrides for particular + * actions, orientations or combinations of actions and orientations. The + * system is currently structured with the expectation that the overrides + * will be relatively few (less than fifteen, say) for any given component + * class. A system that relied on many overrides for its components would + * want to implement a more scalable algorithm for determining which, if + * any, override matches a particular action and orientation combination. + */ +public class ComponentClass implements Serializable +{ + /** Used to effect custom render orders for particular actions, + * orientations, etc. */ + public static class PriorityOverride + implements Comparable, Serializable + { + /** The overridden render priority value. */ + public int renderPriority; + + /** The action, if any, for which this override is appropriate. */ + public String action; + + /** The orientations, if any, for which this override is + * appropriate. */ + public ArrayIntSet orients; + + /** + * Determines whether this priority override matches the specified + * action and orientation combination. + */ + public boolean matches (String action, int orient) + { + return (((orients == null) || orients.contains(orient)) && + ((this.action == null) || this.action.equals(action))); + } + + // documentation inherited from interface + public int compareTo (Object other) + { + // overrides with both an action and an orientation should + // come first in the list + PriorityOverride po = (PriorityOverride)other; + int pri = priority(), opri = po.priority(); + if (pri == opri) { + return hashCode() - po.hashCode(); + } else { + return pri - opri; + } + } + + protected int priority () + { + int priority = 0; + if (action != null) { + priority++; + } + if (orients != null) { + priority++; + } + return priority; + } + + /** Generates a string representation of this instance. */ + public String toString () + { + return "[pri=" + renderPriority + ", action=" + action + + ", orients=" + orients + "]"; + } + + /** Increase this value when object's serialized state is impacted + * by a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; + } + + /** The component class name. */ + public String name; + + /** The default render priority. */ + public int renderPriority; + + /** The color classes to use when recoloring components of this class. May + * be null if a system does not use recolorable components. */ + public String[] colors; + + /** The class name of the layer from which this component class obtains a + * mask to limit rendering to certain areas. */ + public String mask; + + /** Indicates the class name of the shadow layer to which this component + * class contributes a shadow. */ + public String shadow; + + /** 1.0 for a normal component, the alpha value of the pre-composited + * shadow for the special "shadow" component class. */ + public float shadowAlpha = 1.0f; + + /** + * Creates an uninitialized instance suitable for unserialization or + * population during XML parsing. + */ + public ComponentClass () + { + } + + /** + * Returns the render priority appropriate for the specified action + * and orientation. + */ + public int getRenderPriority (String action, int orientation) + { + // because we expect there to be relatively few priority + // overrides, we simply search linearly through the list for the + // closest match + int ocount = (_overrides != null) ? _overrides.size() : 0; + for (int ii = 0; ii < ocount; ii++) { + PriorityOverride over = (PriorityOverride)_overrides.get(ii); + // based on the way the overrides are sorted, the first match + // is the most specific and the one we want + if (over.matches(action, orientation)) { + return over.renderPriority; + } + } + + return renderPriority; + } + + /** + * Adds the supplied render priority override record to this component + * class. + */ + public void addPriorityOverride (PriorityOverride override) + { + if (_overrides == null) { + _overrides = new ComparableArrayList(); + } + _overrides.insertSorted(override); + } + + /** + * Returns true if this component class contributes a shadow to a + * particular shadow layer. Note: this is different from being a + * shadow layer which is determined by calling {@link #isShadow}. + */ + public boolean isShadowed () + { + return (shadow != null); + } + + /** + * Returns true if this component class is a shadow layer rather than a + * normal component class. + */ + public boolean isShadow () + { + return (shadowAlpha != 1.0f); + } + + /** + * Classes with the same name are the same. + */ + public boolean equals (Object other) + { + if (other instanceof ComponentClass) { + return name.equals(((ComponentClass)other).name); + } else { + return false; + } + } + + /** + * Hashcode is based on component class name. + */ + public int hashCode () + { + return name.hashCode(); + } + + /** + * Returns a string representation of this component class. + */ + public String toString () + { + StringBuilder buf = new StringBuilder("["); + buf.append("name=").append(name); + buf.append(", pri=").append(renderPriority); + if (colors != null) { + buf.append(", colors=").append(StringUtil.toString(colors)); + } + if (mask != null) { + buf.append(", mask=").append(mask); + } + if (shadowAlpha != 1.0f) { + buf.append(", shadow=").append(shadowAlpha); + } else if (shadow != null) { + buf.append(", shadow=").append(shadow); + } + return buf.append("]").toString(); + } + + /** A list of render priority overrides. */ + protected ComparableArrayList _overrides; + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 3; +} diff --git a/src/java/com/threerings/cast/ComponentIDBroker.java b/src/java/com/threerings/cast/ComponentIDBroker.java new file mode 100644 index 00000000..c42d17f5 --- /dev/null +++ b/src/java/com/threerings/cast/ComponentIDBroker.java @@ -0,0 +1,63 @@ +// +// $Id: ComponentIDBroker.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import com.samskivert.io.PersistenceException; + +/** + * Brokers component ids. The component repository interface makes + * available a collection of components based on a unique identifier. The + * expectation is that a collection of components will be used to populate + * a repository and in that population process, component ids will be + * assigned to the components. The component id broker system provides a + * means by which named components can be mapped consistently to a set of + * component ids. Humans can then be responsible for assigning unique + * names to the components and the broker will ensure that those names map + * to unique ids that won't change if the repository is rebuilt from the + * source components. + */ +public interface ComponentIDBroker +{ + /** + * Returns the unique identifier for the named component. If no + * identifier has yet been assigned to the specified named component, + * one should be assigned and returned. + * + * @param cclass the name of the class to which the component belongs. + * @param cname the name of the component. + * + * @exception PersistenceException thrown if an error occurs + * communicating with the underlying persistence mechanism used to + * store the name to id mappings. + */ + public int getComponentID (String cclass, String cname) + throws PersistenceException; + + /** + * When the user of a component id broker is done obtaining component + * ids, it must call this method to give the component id broker an + * opportunity to flush any newly created component ids back to its + * persistent store. + */ + public void commit () + throws PersistenceException; +} diff --git a/src/java/com/threerings/cast/ComponentRepository.java b/src/java/com/threerings/cast/ComponentRepository.java new file mode 100644 index 00000000..17ca2f41 --- /dev/null +++ b/src/java/com/threerings/cast/ComponentRepository.java @@ -0,0 +1,71 @@ +// +// $Id: ComponentRepository.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.util.Iterator; + +/** + * Makes available a collection of character components and associated + * metadata. Character components are animated sequences that can be + * composited together to create a complete character visualization + * (imagine interchanging pairs of boots, torsos, hats, etc.). + */ +public interface ComponentRepository +{ + /** + * Returns the {@link CharacterComponent} object for the given + * component identifier. + */ + public CharacterComponent getComponent (int componentId) + throws NoSuchComponentException; + + /** + * Returns the {@link CharacterComponent} object with the given + * component class and name. + */ + public CharacterComponent getComponent (String className, String compName) + throws NoSuchComponentException; + + /** + * Returns the {@link ComponentClass} with the specified name or null + * if none exists with that name. + */ + public ComponentClass getComponentClass (String className); + + /** + * Iterates over the {@link ComponentClass} instances representing all + * available character component classes. + */ + public Iterator enumerateComponentClasses (); + + /** + * Iterates over the {@link ActionSequence} instances representing + * every available action sequence. + */ + public Iterator enumerateActionSequences (); + + /** + * Iterates over the component ids of all components in the specified + * class. + */ + public Iterator enumerateComponentIds (ComponentClass compClass); +} diff --git a/src/java/com/threerings/cast/CompositedActionFrames.java b/src/java/com/threerings/cast/CompositedActionFrames.java new file mode 100644 index 00000000..92e911ad --- /dev/null +++ b/src/java/com/threerings/cast/CompositedActionFrames.java @@ -0,0 +1,186 @@ +// +// $Id: CompositedActionFrames.java 3967 2006-03-22 02:41:15Z andrzej $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import com.samskivert.util.LRUHashMap; +import com.samskivert.util.StringUtil; + +import com.threerings.media.image.Colorization; +import com.threerings.media.image.ImageManager; +import com.threerings.media.util.MultiFrameImage; + +import com.threerings.cast.CharacterComponent; +import com.threerings.util.DirectionCodes; + +/** + * An implementation of the {@link MultiFrameImage} interface that is used + * to lazily create composited character frames when they are requested. + */ +public class CompositedActionFrames + implements ActionFrames, DirectionCodes +{ + /** Used to associate a {@link CharacterComponent} with its {@link + * ActionFrames} for a particular action. */ + public static class ComponentFrames + { + public CharacterComponent ccomp; + + public ActionFrames frames; + + public ComponentFrames () { + } + + public ComponentFrames ( + CharacterComponent ccomp, ActionFrames frames) { + this.ccomp = ccomp; + this.frames = frames; + } + + public String toString () { + return ccomp + ":" + frames; + } + } + + /** + * Constructs a set of composited action frames with the supplied + * source frames and colorization configuration. The actual component + * frame images will not be composited until they are requested. + */ + public CompositedActionFrames (ImageManager imgr, LRUHashMap frameCache, + String action, ComponentFrames[] sources) + { + // sanity check + if (sources == null || sources.length == 0) { + String errmsg = "Requested to composite invalid set of source " + + "frames! [action=" + action + + ", sources=" + StringUtil.toString(sources) + "]."; + throw new RuntimeException(errmsg); + } + + _imgr = imgr; + _frameCache = frameCache; + _sources = sources; + _action = action; + + // the sources must all have the same orientation count, so we + // just use the first + _orientCount = _sources[0].frames.getOrientationCount(); + } + + // documentation inherited from interface + public int getOrientationCount () + { + return _orientCount; + } + + // documentation inherited from interface + public TrimmedMultiFrameImage getFrames (int orient) + { + _key.setOrient(orient); + CompositedMultiFrameImage cmfi = + (CompositedMultiFrameImage)_frameCache.get(_key); + if (cmfi == null) { + cmfi = createFrames(orient); + _frameCache.put(new CompositedFramesKey(orient), cmfi); + } + return cmfi; + } + + // documentation inherited from interface + public int getXOrigin (int orient, int frameIdx) + { + CompositedMultiFrameImage cmfi = (CompositedMultiFrameImage) + getFrames(orient); + return cmfi.getXOrigin(frameIdx); + } + + // documentation inherited from interface + public int getYOrigin (int orient, int frameIdx) + { + CompositedMultiFrameImage cmfi = (CompositedMultiFrameImage) + getFrames(orient); + return cmfi.getYOrigin(frameIdx); + } + + // documentation inherited from interface + public ActionFrames cloneColorized (Colorization[] zations) + { + throw new RuntimeException("What you talkin' about Willis?"); + } + + /** + * Creates our underlying multi-frame image for a particular orientation. + */ + protected CompositedMultiFrameImage createFrames (int orient) + { + return new CompositedMultiFrameImage(_imgr, _sources, _action, orient); + } + + /** Used to cache composited frames for a particular action and + * orientation. */ + protected class CompositedFramesKey + { + public CompositedFramesKey (int orient) { + _orient = orient; + } + + public void setOrient (int orient) { + _orient = orient; + } + + public CompositedActionFrames getOwner () { + return CompositedActionFrames.this; + } + + public boolean equals (Object other) { + CompositedFramesKey okey = (CompositedFramesKey)other; + return ((getOwner() == okey.getOwner()) && + (_orient == okey._orient)); + } + + public int hashCode () { + return CompositedActionFrames.this.hashCode() ^ _orient; + } + + protected int _orient; + } + + /** The image manager from whom we can obtain prepared volatile images + * onto which to render our composited actions. */ + protected ImageManager _imgr; + + /** Used to cache our composited action frame images. */ + protected LRUHashMap _frameCache; + + /** The action for which we're compositing frames. */ + protected String _action; + + /** The number of orientations. */ + protected int _orientCount; + + /** Our source components and action frames. */ + protected ComponentFrames[] _sources; + + /** Used to avoid creating a new key object every time we do a cache + * lookup. */ + protected CompositedFramesKey _key = new CompositedFramesKey(0); +} diff --git a/src/java/com/threerings/cast/CompositedMaskedImage.java b/src/java/com/threerings/cast/CompositedMaskedImage.java new file mode 100644 index 00000000..21d31e89 --- /dev/null +++ b/src/java/com/threerings/cast/CompositedMaskedImage.java @@ -0,0 +1,131 @@ +// +// $Id: CompositedMultiFrameImage.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Transparency; + +import com.threerings.media.image.ImageManager; +import com.threerings.media.image.Mirage; +import com.threerings.media.image.VolatileMirage; + +import com.threerings.cast.CompositedActionFrames.ComponentFrames; + +/** + * Used to composite action frames with mask frames. + */ +public class CompositedMaskedImage extends CompositedMultiFrameImage +{ + public CompositedMaskedImage ( + ImageManager imgr, ComponentFrames[] sources, String action, + int orient) + { + super(imgr, sources, action, orient); + } + + // documentation inherited from interface + public int getWidth (int index) { + return _sources[0].frames.getFrames(_orient).getWidth(index); + } + + // documentation inherited from interface + public int getHeight (int index) { + return _sources[0].frames.getFrames(_orient).getHeight(index); + } + + public int getXOrigin (int index) { + return _sources[0].frames.getXOrigin(_orient, index); + } + + public int getYOrigin (int index) { + return _sources[0].frames.getYOrigin(_orient, index); + } + + // documentation inherited from interface + public void paintFrame (Graphics2D g, int index, int x, int y) { + _images[index].paint(g, x + _images[index].getX(), + y + _images[index].getY()); + } + + // documentation inherited from interface + public boolean hitTest (int index, int x, int y) { + return _images[index].hitTest(x + _images[index].getX(), + y + _images[index].getY()); + } + + // documentation inherited from interface TrimmedMultiFrameImage + public void getTrimmedBounds (int index, Rectangle bounds) { + bounds.setBounds(_images[index].getX(), _images[index].getY(), + _images[index].getWidth(), _images[index].getHeight()); + } + + // documentation inherited + protected CompositedMirage createCompositedMirage (int index) + { + return new MaskedMirage(index); + } + + /** + * Combines the image in the first source with the mask in the second. */ + protected class MaskedMirage extends CompositedMirage + { + public MaskedMirage (int index) + { + super(index); + } + + // documentation inherited + protected Rectangle combineBounds (Rectangle bounds, Rectangle tbounds) + { + if (bounds.width == 0 && bounds.height == 0) { + bounds.setBounds(tbounds); + } else { + bounds = bounds.intersection(tbounds); + } + return bounds; + } + + // documentation inherited + protected void refreshVolatileImage () + { + Graphics2D g = (Graphics2D)_image.getGraphics(); + try { + TrimmedMultiFrameImage source = + _sources[0].frames.getFrames(_orient), + mask = _sources[1].frames.getFrames(_orient); + source.paintFrame(g, _index, -_bounds.x, -_bounds.y); + g.setComposite(AlphaComposite.DstIn); + mask.paintFrame(g, _index, -_bounds.x, -_bounds.y); + + } finally { + // clean up after ourselves + if (g != null) { + g.dispose(); + } + } + } + } +}; diff --git a/src/java/com/threerings/cast/CompositedMultiFrameImage.java b/src/java/com/threerings/cast/CompositedMultiFrameImage.java new file mode 100644 index 00000000..545547d3 --- /dev/null +++ b/src/java/com/threerings/cast/CompositedMultiFrameImage.java @@ -0,0 +1,254 @@ +// +// $Id: CompositedMultiFrameImage.java 3967 2006-03-22 02:41:15Z andrzej $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Transparency; + +import java.util.Arrays; +import java.util.Comparator; + +import com.threerings.media.image.ImageManager; +import com.threerings.media.image.Mirage; +import com.threerings.media.image.VolatileMirage; + +import com.threerings.cast.CompositedActionFrames.ComponentFrames; +import com.threerings.cast.TrimmedMultiFrameImage; + +/** + * Used to composite the action frames for a particular orientation of a + * {@link CompositedActionFrames}. + */ +public class CompositedMultiFrameImage + implements TrimmedMultiFrameImage +{ + public CompositedMultiFrameImage ( + ImageManager imgr, ComponentFrames[] sources, + String action, int orient) + { + _imgr = imgr; + _sources = sources; + _action = action; + _orient = orient; + + // create our frame images (which will do the compositing) + int fcount = sources[0].frames.getFrames(orient).getFrameCount(); + _images = new CompositedMirage[fcount]; + for (int ii = 0; ii < fcount; ii++) { + _images[ii] = createCompositedMirage(ii); + } + } + + // documentation inherited + public int getFrameCount () { + return _images.length; + } + + // documentation inherited from interface + public int getWidth (int index) { + return _images[index].getWidth(); + } + + // documentation inherited from interface + public int getHeight (int index) { + return _images[index].getHeight(); + } + + public int getXOrigin (int index) { + return _images[index].getXOrigin(); + } + + public int getYOrigin (int index) { + return _images[index].getYOrigin(); + } + + // documentation inherited from interface + public void paintFrame (Graphics2D g, int index, int x, int y) { + _images[index].paint(g, x, y); + } + + // documentation inherited from interface + public boolean hitTest (int index, int x, int y) { + return _images[index].hitTest(x, y); + } + + // documentation inherited from interface + public void getTrimmedBounds (int index, Rectangle bounds) { + bounds.setBounds(0, 0, getWidth(index), getHeight(index)); + } + + /** + * Returns the estimated memory usage of our composited frame images. + */ + public long getEstimatedMemoryUsage () + { + long size = 0; + for (int ii = 0; ii < _images.length; ii++) { + size += _images[ii].getEstimatedMemoryUsage(); + } + return size; + } + + /** + * Creates a composited image for the specified frame. + */ + protected CompositedMirage createCompositedMirage (int index) + { + return new CompositedMirage(index); + } + + // documentation inherited + protected Mirage getFrame (int orient, int index) + { + return _images[index]; + } + + /** The image manager from whom we load our images. */ + protected ImageManager _imgr; + + /** The action frames from which we obtain our source imagery. */ + protected ComponentFrames[] _sources; + + /** The action we're compositing. */ + protected String _action; + + /** The orientation we're compositing. */ + protected int _orient; + + /** Our composited action frame images. */ + protected CompositedMirage[] _images; + + /** + * Used to create our mirage using the source action frame images. + */ + protected class CompositedMirage extends VolatileMirage + implements Comparator + { + public CompositedMirage (int index) + { + super(CompositedMultiFrameImage.this._imgr, + new Rectangle(0, 0, 0, 0)); + + // keep this for later + _index = index; + + // first we need to determine the bounds of the rectangle that + // will enclose all of our various components + Rectangle tbounds = new Rectangle(); + int scount = _sources.length; + for (int ii = 0; ii < scount; ii++) { + TrimmedMultiFrameImage source = + _sources[ii].frames.getFrames(_orient); + source.getTrimmedBounds(index, tbounds); + _bounds = combineBounds(_bounds, tbounds); + } + + // compute our new origin + _origin.x = (_sources[0].frames.getXOrigin(_orient, index) - + _bounds.x); + _origin.y = (_sources[0].frames.getYOrigin(_orient, index) - + _bounds.y); +// Log.info("New origin [x=" + _origin.x + ", y=" + _origin.y + "]."); + + // render our volatile image for the first time + createVolatileImage(); + } + + public int getXOrigin () + { + return _origin.x; + } + + public int getYOrigin () + { + return _origin.y; + } + + // documentation inherited from interface + public int compare (Object o1, Object o2) + { + ComponentFrames cf1 = (ComponentFrames)o1, + cf2 = (ComponentFrames)o2; + return (cf1.ccomp.componentClass.getRenderPriority( + _action, _orient) - + cf2.ccomp.componentClass.getRenderPriority( + _action, _orient)); + } + + /** + * Combines the working bounds with a new set of bounds. + */ + protected Rectangle combineBounds (Rectangle bounds, Rectangle tbounds) + { + // the first one defines our initial bounds + if (bounds.width == 0 && bounds.height == 0) { + bounds.setBounds(tbounds); + } else { + bounds.add(tbounds); + } + return bounds; + } + + // documentation inherited + protected int getTransparency () + { + return Transparency.BITMASK; + } + + // documentation inherited + protected void refreshVolatileImage () + { +// long start = System.currentTimeMillis(); + + // sort the sources appropriately for this orientation + Arrays.sort(_sources, this); + + // now render each of the components into a composited frame + int scount = _sources.length; + Graphics2D g = (Graphics2D)_image.getGraphics(); + try { + for (int ii = 0; ii < scount; ii++) { + TrimmedMultiFrameImage source = + _sources[ii].frames.getFrames(_orient); + source.paintFrame(g, _index, -_bounds.x, -_bounds.y); + } + } finally { + // clean up after ourselves + if (g != null) { + g.dispose(); + } + } + +// Log.info("Composited [orient=" + _orient + ", index=" + _index + +// ", tbounds=" + StringUtil.toString(_bounds) + "]."); + +// long now = System.currentTimeMillis(); +// Log.info("Composited " + scount + " frames in " + +// (now-start) + " millis."); + } + + protected int _index; + protected Point _origin = new Point(); + } +}; diff --git a/src/java/com/threerings/cast/CompositedShadowImage.java b/src/java/com/threerings/cast/CompositedShadowImage.java new file mode 100644 index 00000000..3c75d8c3 --- /dev/null +++ b/src/java/com/threerings/cast/CompositedShadowImage.java @@ -0,0 +1,92 @@ +// +// $Id: CompositedMultiFrameImage.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.threerings.media.image.ImageManager; +import com.threerings.media.image.Mirage; +import com.threerings.media.image.VolatileMirage; + +import com.threerings.cast.CompositedActionFrames.ComponentFrames; + +/** + * Used to composite the special shadow action frames for a particular + * orientation of a {@link CompositedActionFrames}. + */ +public class CompositedShadowImage extends CompositedMultiFrameImage +{ + public CompositedShadowImage (ImageManager imgr, ComponentFrames[] sources, + String action, int orient, float shadowAlpha) + { + super(imgr, sources, action, orient); + + // create the appropriate alpha composite for rendering the shadow + _shadowAlpha = AlphaComposite.getInstance( + AlphaComposite.SRC_OVER, shadowAlpha); + } + + // documentation inherited from interface + public int getWidth (int index) { + return _sources[0].frames.getFrames(_orient).getWidth(index); + } + + // documentation inherited from interface + public int getHeight (int index) { + return _sources[0].frames.getFrames(_orient).getHeight(index); + } + + public int getXOrigin (int index) { + return _sources[0].frames.getXOrigin(_orient, index); + } + + public int getYOrigin (int index) { + return _sources[0].frames.getYOrigin(_orient, index); + } + + // documentation inherited from interface + public void paintFrame (Graphics2D g, int index, int x, int y) { + Composite ocomp = g.getComposite(); + g.setComposite(_shadowAlpha); + _images[index].paint(g, x + _images[index].getX(), + y + _images[index].getY()); + g.setComposite(ocomp); + } + + // documentation inherited from interface + public boolean hitTest (int index, int x, int y) { + return _images[index].hitTest(x + _images[index].getX(), + y + _images[index].getY()); + } + + // documentation inherited from interface TrimmedMultiFrameImage + public void getTrimmedBounds (int index, Rectangle bounds) { + bounds.setBounds(_images[index].getX(), _images[index].getY(), + _images[index].getWidth(), _images[index].getHeight()); + } + + /** The alpha value at which we render our shadow. */ + protected AlphaComposite _shadowAlpha; +}; diff --git a/src/java/com/threerings/cast/FrameProvider.java b/src/java/com/threerings/cast/FrameProvider.java new file mode 100644 index 00000000..e1080f95 --- /dev/null +++ b/src/java/com/threerings/cast/FrameProvider.java @@ -0,0 +1,37 @@ +// +// $Id: FrameProvider.java 3740 2005-10-20 22:12:45Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +/** + * Provides a mechanism where by a character component can obtain access + * to its image frames for a particular action in an on demand manner. + */ +public interface FrameProvider +{ + /** + * Returns the animation frames (in the eight sprite directions) for + * the specified action of the specified component. May return null if + * the specified action does not exist for the specified component. + */ + public ActionFrames getFrames ( + CharacterComponent component, String action, String type); +} diff --git a/src/java/com/threerings/cast/Log.java b/src/java/com/threerings/cast/Log.java new file mode 100644 index 00000000..3e54857f --- /dev/null +++ b/src/java/com/threerings/cast/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +/** + * A placeholder class that contains a reference to the log object used by + * the cast package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("cast"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/cast/NoSuchComponentException.java b/src/java/com/threerings/cast/NoSuchComponentException.java new file mode 100644 index 00000000..a18c2f4f --- /dev/null +++ b/src/java/com/threerings/cast/NoSuchComponentException.java @@ -0,0 +1,50 @@ +// +// $Id: NoSuchComponentException.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +/** + * Thrown when an attempt is made to retrieve a non-existent character + * component from the component repository. + */ +public class NoSuchComponentException extends Exception +{ + public NoSuchComponentException (int componentId) + { + super("No such component [componentId=" + componentId + "]"); + _componentId = componentId; + } + + public NoSuchComponentException ( + String componentClass, String componentName) + { + super("No such component [class=" + componentClass + + ", name=" + componentName + "]"); + _componentId = -1; + } + + public int getComponentId () + { + return _componentId; + } + + protected int _componentId; +} diff --git a/src/java/com/threerings/cast/StandardActions.java b/src/java/com/threerings/cast/StandardActions.java new file mode 100644 index 00000000..19e23a4a --- /dev/null +++ b/src/java/com/threerings/cast/StandardActions.java @@ -0,0 +1,42 @@ +// +// $Id: StandardActions.java 3740 2005-10-20 22:12:45Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +/** + * Actions are referenced by name and this interface defines constants for + * standard actions and action suffixes used by the shadow and cropping + * support. + */ +public interface StandardActions +{ + /** The name of the standard standing action. */ + public static final String STANDING = "standing"; + + /** The name of the standard walking action. */ + public static final String WALKING = "walking"; + + /** A special action sub-type for shadow imagery. */ + public static final String SHADOW_TYPE = "shadow"; + + /** A special action sub-type for crop imagery. */ + public static final String CROP_TYPE = "crop"; +} diff --git a/src/java/com/threerings/cast/TrimmedMultiFrameImage.java b/src/java/com/threerings/cast/TrimmedMultiFrameImage.java new file mode 100644 index 00000000..e7d88ae5 --- /dev/null +++ b/src/java/com/threerings/cast/TrimmedMultiFrameImage.java @@ -0,0 +1,41 @@ +// +// $Id: TrimmedMultiFrameImage.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.awt.Rectangle; + +import com.threerings.media.util.MultiFrameImage; + +/** + * Used to generate more memory efficient composited images in + * circumstances where we have trimmed underlying component images. + */ +public interface TrimmedMultiFrameImage extends MultiFrameImage +{ + /** + * Fills in the minimum bounding rectangle for this image that + * contains all non-transparent pixels. If this information is + * unavailable, the bounds of the entire image may be returned in + * exchange for improved performance. + */ + public void getTrimmedBounds (int index, Rectangle bounds); +} diff --git a/src/java/com/threerings/cast/builder/BuilderModel.java b/src/java/com/threerings/cast/builder/BuilderModel.java new file mode 100644 index 00000000..04a71e10 --- /dev/null +++ b/src/java/com/threerings/cast/builder/BuilderModel.java @@ -0,0 +1,151 @@ +// +// $Id: BuilderModel.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.builder; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import com.samskivert.util.CollectionUtil; + +import com.threerings.cast.ComponentRepository; +import com.threerings.cast.ComponentClass; + +/** + * The builder model represents the current state of the character the + * user is building. + */ +public class BuilderModel +{ + /** + * Constructs a builder model. + */ + public BuilderModel (ComponentRepository crepo) + { + gatherComponentInfo(crepo); + } + + /** + * Adds a builder model listener. + * + * @param l the listener. + */ + public void addListener (BuilderModelListener l) + { + if (!_listeners.contains(l)) { + _listeners.add(l); + } + } + + /** + * Notifies all model listeners that the builder model has changed. + */ + protected void notifyListeners (int event) + { + int size = _listeners.size(); + for (int ii = 0; ii < size; ii++) { + ((BuilderModelListener)_listeners.get(ii)).modelChanged(event); + } + } + + /** + * Returns a list of the available component classes. + */ + public List getComponentClasses () + { + return Collections.unmodifiableList(_classes); + } + + /** + * Returns the list of components available in the specified class. + */ + public List getComponents (ComponentClass cclass) + { + List list = (List)_components.get(cclass); + if (list == null) { + list = new ArrayList(); + } + return list; + } + + /** + * Returns the selected components in an array. + */ + public int[] getSelectedComponents () + { + int[] values = new int[_selected.size()]; + Iterator iter = _selected.values().iterator(); + for (int i = 0; iter.hasNext(); i++) { + values[i] = ((Integer)iter.next()).intValue(); + } + return values; + } + + /** + * Sets the selected component for the given component class. + */ + public void setSelectedComponent (ComponentClass cclass, int cid) + { + _selected.put(cclass, Integer.valueOf(cid)); + notifyListeners(BuilderModelListener.COMPONENT_CHANGED); + } + + /** + * Gathers component class and component information from the + * character manager for later reference by others. + */ + protected void gatherComponentInfo (ComponentRepository crepo) + { + // get the list of all component classes + CollectionUtil.addAll(_classes, crepo.enumerateComponentClasses()); + + for (int ii = 0; ii < _classes.size(); ii++) { + // get the list of components available for this class + ComponentClass cclass = (ComponentClass)_classes.get(ii); + Iterator iter = crepo.enumerateComponentIds(cclass); + + while (iter.hasNext()) { + Integer cid = (Integer)iter.next(); + ArrayList clist = (ArrayList)_components.get(cclass); + if (clist == null) { + _components.put(cclass, clist = new ArrayList()); + } + + clist.add(cid); + } + } + } + + /** The currently selected character components. */ + protected HashMap _selected = new HashMap(); + + /** The hashtable of available component ids for each class. */ + protected HashMap _components = new HashMap(); + + /** The list of all available component classes. */ + protected ArrayList _classes = new ArrayList(); + + /** The model listeners. */ + protected ArrayList _listeners = new ArrayList(); +} diff --git a/src/java/com/threerings/cast/builder/BuilderModelListener.java b/src/java/com/threerings/cast/builder/BuilderModelListener.java new file mode 100644 index 00000000..514218d1 --- /dev/null +++ b/src/java/com/threerings/cast/builder/BuilderModelListener.java @@ -0,0 +1,40 @@ +// +// $Id: BuilderModelListener.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.builder; + +/** + * The builder model listener interface should be implemented by + * classes that would like to be notified when the builder model is + * changed. + * + * @see BuilderModel + */ +public interface BuilderModelListener +{ + /** + * Called by the {@link BuilderModel} when the model is changed. + */ + public void modelChanged (int event); + + /** Notification event constants. */ + public static final int COMPONENT_CHANGED = 0; +} diff --git a/src/java/com/threerings/cast/builder/BuilderPanel.java b/src/java/com/threerings/cast/builder/BuilderPanel.java new file mode 100644 index 00000000..5417e4ad --- /dev/null +++ b/src/java/com/threerings/cast/builder/BuilderPanel.java @@ -0,0 +1,60 @@ +// +// $Id: BuilderPanel.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.builder; + +import javax.swing.*; +import com.samskivert.swing.*; + +import com.threerings.cast.CharacterManager; +import com.threerings.cast.ComponentRepository; + +/** + * The builder panel presents the user with an overview of a composited + * character and facilities for altering the individual components that + * comprise the character's display image. + */ +public class BuilderPanel extends JPanel +{ + /** + * Constructs the builder panel. + */ + public BuilderPanel (CharacterManager charmgr, + ComponentRepository crepo, String cprefix) + { + setLayout(new VGroupLayout()); + + // give ourselves a wee bit of a border + setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + GroupLayout gl = new HGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + + // create the builder model + BuilderModel model = new BuilderModel(crepo); + + // create the component selection and sprite display panels + JPanel sub = new JPanel(gl); + sub.add(new ComponentPanel(model, cprefix)); + sub.add(new SpritePanel(charmgr, model)); + add(sub); + } +} diff --git a/src/java/com/threerings/cast/builder/ClassEditor.java b/src/java/com/threerings/cast/builder/ClassEditor.java new file mode 100644 index 00000000..e72c0477 --- /dev/null +++ b/src/java/com/threerings/cast/builder/ClassEditor.java @@ -0,0 +1,109 @@ +// +// $Id: ClassEditor.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.builder; + +import java.util.List; + +import javax.swing.*; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.samskivert.swing.*; + +import com.threerings.cast.ComponentClass; + +/** + * The class editor displays a label and a slider that allow the user to + * select the desired component for a given component class. + */ +public class ClassEditor extends JPanel implements ChangeListener +{ + /** + * Constructs a class editor. + */ + public ClassEditor ( + BuilderModel model, ComponentClass cclass, List components) + { + _model = model; + _components = components; + _cclass = cclass; + + GroupLayout gl = new VGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + setLayout(gl); + + gl = new HGroupLayout(); + gl.setJustification(GroupLayout.LEFT); + JPanel sub = new JPanel(gl); + + sub.add(new JLabel(cclass.name + ": ")); + sub.add(_clabel = new JLabel("0")); + add(sub); + + // create the slider allowing selection of available components + int max = components.size() - 1; + JSlider slider = new JSlider(JSlider.HORIZONTAL, 0, max, 0); + slider.setSnapToTicks(true); + slider.addChangeListener(this); + add(slider); + + // set the starting component for this class + setSelectedComponent(0); + } + + // documentation inherited + public void stateChanged (ChangeEvent e) + { + JSlider source = (JSlider)e.getSource(); + if (!source.getValueIsAdjusting()) { + int val = source.getValue(); + // update the model with the newly selected component + setSelectedComponent(val); + // update the label with the new value + _clabel.setText(Integer.toString(val)); + } + } + + /** + * Sets the selected component in the builder model for the + * component class associated with this editor to the component at + * the given index in this editor's list of available components. + */ + protected void setSelectedComponent (int idx) + { + int cid = ((Integer)_components.get(idx)).intValue(); + _model.setSelectedComponent(_cclass, cid); + } + + /** The component class associated with this editor. */ + protected ComponentClass _cclass; + + /** The components selectable via this editor. */ + protected List _components; + + /** The label denoting the currently selected component index. */ + protected JLabel _clabel; + + /** The builder model. */ + protected BuilderModel _model; +} diff --git a/src/java/com/threerings/cast/builder/ComponentPanel.java b/src/java/com/threerings/cast/builder/ComponentPanel.java new file mode 100644 index 00000000..f932aba9 --- /dev/null +++ b/src/java/com/threerings/cast/builder/ComponentPanel.java @@ -0,0 +1,74 @@ +// +// $Id: ComponentPanel.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.builder; + +import java.util.List; + +import javax.swing.*; + +import com.samskivert.swing.*; + +import com.threerings.cast.Log; +import com.threerings.cast.*; + +/** + * The component panel displays the available components for all + * component classes and allows the user to choose a set of components + * for compositing into a character image. + */ +public class ComponentPanel extends JPanel +{ + /** + * Constructs the component panel. + */ + public ComponentPanel (BuilderModel model, String cprefix) + { + setLayout(new VGroupLayout(GroupLayout.STRETCH)); + // set up a border + setBorder(BorderFactory.createEtchedBorder()); + // add the component editors to the panel + addClassEditors(model, cprefix); + } + + /** + * Adds editor user interface elements for each component class to + * allow the user to select the desired component. + */ + protected void addClassEditors (BuilderModel model, String cprefix) + { + List classes = model.getComponentClasses(); + int size = classes.size(); + for (int ii = 0; ii < size; ii++) { + ComponentClass cclass = (ComponentClass)classes.get(ii); + if (!cclass.name.startsWith(cprefix)) { + continue; + } + List ccomps = model.getComponents(cclass); + if (ccomps.size() > 0) { + add(new ClassEditor(model, cclass, ccomps)); + } else { + Log.info("Not creating editor for empty class " + + "[class=" + cclass + "]."); + } + } + } +} diff --git a/src/java/com/threerings/cast/builder/SpritePanel.java b/src/java/com/threerings/cast/builder/SpritePanel.java new file mode 100644 index 00000000..173050b2 --- /dev/null +++ b/src/java/com/threerings/cast/builder/SpritePanel.java @@ -0,0 +1,131 @@ +// +// $Id: SpritePanel.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.builder; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; + +import javax.swing.JPanel; + +import com.threerings.util.DirectionCodes; + +import com.threerings.cast.CharacterDescriptor; +import com.threerings.cast.CharacterManager; +import com.threerings.cast.CharacterSprite; +import com.threerings.cast.StandardActions; + +/** + * The sprite panel displays a character sprite centered in the panel + * suitable for user perusal. + */ +public class SpritePanel extends JPanel + implements DirectionCodes, BuilderModelListener +{ + /** + * Constructs the sprite panel. + */ + public SpritePanel (CharacterManager charmgr, BuilderModel model) + { + // save off references + _charmgr = charmgr; + _model = model; + + // listen to the builder model so that we can update the + // sprite when a new component is selected + _model.addListener(this); + } + + // documentation inherited + public void paintComponent (Graphics g) + { + super.paintComponent(g); + Graphics2D gfx = (Graphics2D)g; + + if (_sprite != null) { + // render the sprite + _sprite.paint(gfx); + } + } + + // documentation inherited + public void doLayout () + { + super.doLayout(); + generateSprite(); + centerSprite(); + } + + // documentation inherited + public void modelChanged (int event) + { + if (event == COMPONENT_CHANGED) { + generateSprite(); + } + } + + /** + * Generates a new character sprite for display to reflect the + * currently selected character components. + */ + protected void generateSprite () + { + int components[] = _model.getSelectedComponents(); + CharacterDescriptor desc = new CharacterDescriptor(components, null); + CharacterSprite sprite = _charmgr.getCharacter(desc); + setSprite(sprite); + } + + /** + * Sets the sprite to be displayed. + */ + protected void setSprite (CharacterSprite sprite) + { + sprite.setActionSequence(StandardActions.STANDING); + sprite.setOrientation(WEST); + _sprite = sprite; + centerSprite(); + repaint(); + } + + /** + * Sets the sprite's location to render it centered within the panel. + */ + protected void centerSprite () + { + if (_sprite != null) { + Dimension d = getSize(); + int swid = _sprite.getWidth(), shei = _sprite.getHeight(); + int x = d.width / 2, y = (d.height + shei) / 2; + _sprite.setLocation(x, y); + } + } + + /** The sprite displayed by the panel. */ + protected CharacterSprite _sprite; + + /** The character manager. */ + protected CharacterManager _charmgr; + + /** The builder model. */ + protected BuilderModel _model; +} diff --git a/src/java/com/threerings/cast/bundle/BundleUtil.java b/src/java/com/threerings/cast/bundle/BundleUtil.java new file mode 100644 index 00000000..79856f52 --- /dev/null +++ b/src/java/com/threerings/cast/bundle/BundleUtil.java @@ -0,0 +1,110 @@ +// +// $Id: BundleUtil.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.bundle; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidClassException; +import java.io.ObjectInputStream; + +import com.samskivert.io.StreamUtil; + +import com.threerings.cast.Log; +import com.threerings.resource.ResourceBundle; + +/** + * Utility functions related to creating and manipulating component + * bundles. + */ +public class BundleUtil +{ + /** The path in the metadata bundle to the serialized action table. */ + public static final String ACTIONS_PATH = "actions.dat"; + + /** The path in the metadata bundle to the serialized action tile sets + * table. */ + public static final String ACTION_SETS_PATH = "action_sets.dat"; + + /** The path in the metadata bundle to the serialized component class + * table. */ + public static final String CLASSES_PATH = "classes.dat"; + + /** The path in the component bundle to the serialized component id to + * class/type mapping. */ + public static final String COMPONENTS_PATH = "components.dat"; + + /** The file extension of our action tile images. */ + public static final String IMAGE_EXTENSION = ".png"; + + /** The serialized tileset extension for our action tilesets. */ + public static final String TILESET_EXTENSION = ".dat"; + + /** + * Attempts to load an object from the supplied resource bundle with + * the specified path. + * + * @param wipeBundleOnFailure if there is an error reading the object + * from the bundle and this parameter is true, we will instruct the + * bundle to delete its underlying jar file before propagating the + * exception with the expectation that it will be redownloaded and + * repaired the next time the application is run. + * + * @return the unserialized object in question. + * + * @exception IOException thrown if an I/O error occurs while reading + * the object from the bundle. + */ + public static Object loadObject (ResourceBundle bundle, String path, + boolean wipeBundleOnFailure) + throws IOException, ClassNotFoundException + { + InputStream bin = null; + try { + bin = bundle.getResource(path); + if (bin == null) { + return null; + } + return new ObjectInputStream(bin).readObject(); + + } catch (InvalidClassException ice) { + Log.warning("Aiya! Serialized object is hosed " + + "[bundle=" + bundle.getSource().getPath() + + ", element=" + path + + ", error=" + ice.getMessage() + "]."); + return null; + + } catch (IOException ioe) { + Log.warning("Error reading resource from bundle " + + "[bundle=" + bundle + ", path=" + path + + ", wiping?=" + wipeBundleOnFailure + "]."); + if (wipeBundleOnFailure) { + StreamUtil.close(bin); + bin = null; + bundle.wipeBundle(false); + } + throw ioe; + + } finally { + StreamUtil.close(bin); + } + } +} diff --git a/src/java/com/threerings/cast/bundle/BundledComponentRepository.java b/src/java/com/threerings/cast/bundle/BundledComponentRepository.java new file mode 100644 index 00000000..931dd7ed --- /dev/null +++ b/src/java/com/threerings/cast/bundle/BundledComponentRepository.java @@ -0,0 +1,528 @@ +// +// $Id: BundledComponentRepository.java 4023 2006-04-14 21:52:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.bundle; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +import com.samskivert.util.HashIntMap; +import com.samskivert.util.IntIntMap; +import com.samskivert.util.Tuple; + +import org.apache.commons.collections.iterators.FilterIterator; +import org.apache.commons.collections.Predicate; + +import com.threerings.resource.ResourceBundle; +import com.threerings.resource.ResourceManager; + +import com.threerings.media.image.Colorization; +import com.threerings.media.image.FastImageIO; +import com.threerings.media.image.ImageDataProvider; +import com.threerings.media.image.ImageManager; + +import com.threerings.media.tile.IMImageProvider; +import com.threerings.media.tile.Tile; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TrimmedTile; + +import com.threerings.util.DirectionCodes; + +import com.threerings.cast.ActionFrames; +import com.threerings.cast.ActionSequence; +import com.threerings.cast.CharacterComponent; +import com.threerings.cast.ComponentClass; +import com.threerings.cast.ComponentRepository; +import com.threerings.cast.FrameProvider; +import com.threerings.cast.Log; +import com.threerings.cast.NoSuchComponentException; +import com.threerings.cast.StandardActions; +import com.threerings.cast.TrimmedMultiFrameImage; + +/** + * A component repository implementation that obtains information from + * resource bundles. + * + * @see ResourceManager + */ +public class BundledComponentRepository + implements DirectionCodes, ComponentRepository +{ + /** + * Constructs a repository which will obtain its resource set from the + * supplied resource manager. + * + * @param rmgr the resource manager from which to obtain our resource + * set. + * @param imgr the image manager that we'll use to decode and cache + * images. + * @param name the name of the resource set from which we will be + * loading our component data. + * + * @exception IOException thrown if an I/O error occurs while reading + * our metadata from the resource bundles. + */ + public BundledComponentRepository ( + ResourceManager rmgr, ImageManager imgr, String name) + throws IOException + { + // keep this guy around + _imgr = imgr; + + // first we obtain the resource set from whence will come our + // bundles + ResourceBundle[] rbundles = rmgr.getResourceSet(name); + + // look for our metadata info in each of the bundles + try { + int rcount = (rbundles == null) ? 0 : rbundles.length; + for (int i = 0; i < rcount; i++) { + if (_actions == null) { + _actions = (HashMap)BundleUtil.loadObject( + rbundles[i], BundleUtil.ACTIONS_PATH, true); + } + if (_actionSets == null) { + _actionSets = (HashMap)BundleUtil.loadObject( + rbundles[i], BundleUtil.ACTION_SETS_PATH, true); + } + if (_classes == null) { + _classes = (HashMap)BundleUtil.loadObject( + rbundles[i], BundleUtil.CLASSES_PATH, true); + } + } + + // now go back and load up all of the component information + for (int i = 0; i < rcount; i++) { + HashIntMap comps = null; + comps = (HashIntMap)BundleUtil.loadObject( + rbundles[i], BundleUtil.COMPONENTS_PATH, true); + if (comps == null) { + continue; + } + + // create a frame provider for this bundle + FrameProvider fprov = + new ResourceBundleProvider(_imgr, rbundles[i]); + + // now create character component instances for each component + // in the serialized table + Iterator iter = comps.keySet().iterator(); + while (iter.hasNext()) { + int componentId = ((Integer)iter.next()).intValue(); + Tuple info = (Tuple)comps.get(componentId); + createComponent(componentId, (String)info.left, + (String)info.right, fprov); + } + } + + } catch (ClassNotFoundException cnfe) { + throw (IOException) new IOException( + "Internal error unserializing metadata").initCause(cnfe); + } + + // if we failed to load our classes or actions, create empty + // hashtables so that we can safely enumerate our emptiness + if (_actions == null) { + _actions = new HashMap(); + } + if (_classes == null) { + _classes = new HashMap(); + } + } + + /** + * Configures the bundled component repository to wipe any bundles + * that report certain kinds of failure. In the event that an unpacked + * bundle becomes corrupt, this is useful in that it will force the + * bundle to be unpacked on the next application invocation, + * potentially remedying the problem of a corrupt unpacking. + */ + public void setWipeOnFailure (boolean wipeOnFailure) + { + _wipeOnFailure = true; + } + + // documentation inherited + public CharacterComponent getComponent (int componentId) + throws NoSuchComponentException + { + CharacterComponent component = (CharacterComponent) + _components.get(componentId); + if (component == null) { + throw new NoSuchComponentException(componentId); + } + return component; + } + + // documentation inherited + public CharacterComponent getComponent (String className, String compName) + throws NoSuchComponentException + { + // look up the list for that class + ArrayList comps = (ArrayList)_classComps.get(className); + if (comps != null) { + // scan the list for the named component + int ccount = comps.size(); + for (int i = 0; i < ccount; i++) { + CharacterComponent comp = (CharacterComponent)comps.get(i); + if (comp.name.equals(compName)) { + return comp; + } + } + } + throw new NoSuchComponentException(className, compName); + } + + // documentation inherited + public ComponentClass getComponentClass (String className) + { + return (ComponentClass)_classes.get(className); + } + + // documentation inherited + public Iterator enumerateComponentClasses () + { + return _classes.values().iterator(); + } + + // documentation inherited + public Iterator enumerateActionSequences () + { + return _actions.values().iterator(); + } + + // documentation inherited + public Iterator enumerateComponentIds (final ComponentClass compClass) + { + Predicate classP = new Predicate() { + public boolean evaluate (Object input) { + CharacterComponent comp = (CharacterComponent) + _components.get(input); + return comp.componentClass.equals(compClass); + } + }; + return new FilterIterator(_components.keySet().iterator(), classP); + } + + /** + * Creates a component and inserts it into the component table. + */ + protected void createComponent ( + int componentId, String cclass, String cname, FrameProvider fprov) + { + // look up the component class information + ComponentClass clazz = (ComponentClass)_classes.get(cclass); + if (clazz == null) { + Log.warning("Non-existent component class " + + "[class=" + cclass + ", name=" + cname + + ", id=" + componentId + "]."); + return; + } + + // create the component + CharacterComponent component = new CharacterComponent( + componentId, cname, clazz, fprov); + + // stick it into the appropriate tables + _components.put(componentId, component); + + // we have a hash of lists for mapping components by class/name + ArrayList comps = (ArrayList)_classComps.get(cclass); + if (comps == null) { + comps = new ArrayList(); + _classComps.put(cclass, comps); + } + if (!comps.contains(component)) { + comps.add(component); + } else { + Log.info("Requested to register the same component twice? " + + "[component=" + component + "]."); + } + } + + /** + * Instances of these provide images to our component action tilesets + * and frames to our components. + */ + protected class ResourceBundleProvider extends IMImageProvider + implements ImageDataProvider, FrameProvider + { + /** + * Constructs an instance that will obtain image data from the + * specified resource bundle. + */ + public ResourceBundleProvider (ImageManager imgr, ResourceBundle bundle) + { + super(imgr, (String)null); + _dprov = this; + _bundle = bundle; + } + + // documentation inherited from interface + public String getIdent () + { + return "bcr:" + _bundle.getSource(); + } + + // documentation inherited from interface + public BufferedImage loadImage (String path) throws IOException + { + return FastImageIO.read(_bundle.getResourceFile(path)); + } + + // documentation inherited + public ActionFrames getFrames ( + CharacterComponent component, String action, String type) + { + // obtain the action sequence definition for this action + ActionSequence actseq = (ActionSequence)_actions.get(action); + if (actseq == null) { + Log.warning("Missing action sequence definition " + + "[action=" + action + + ", component=" + component + "]."); + return null; + } + + // determine our image path name + String imgpath = action, dimgpath = ActionSequence.DEFAULT_SEQUENCE; + if (type != null) { + imgpath += "_" + type; + dimgpath += "_" + type; + } + + String root = component.componentClass.name + "/" + + component.name + "/"; + String cpath = root + imgpath + BundleUtil.TILESET_EXTENSION; + String dpath = root + dimgpath + BundleUtil.TILESET_EXTENSION; + + // look to see if this tileset is already cached (as the + // custom action or the default action) + TileSet aset = (TileSet)_setcache.get(cpath); + if (aset == null) { + aset = (TileSet)_setcache.get(dpath); + if (aset != null) { + // save ourselves a lookup next time + _setcache.put(cpath, aset); + } + } + + try { + // then try loading up a tileset customized for this action + if (aset == null) { + aset = (TileSet)BundleUtil.loadObject( + _bundle, cpath, false); + } + + // if that failed, try loading the default tileset + if (aset == null) { + aset = (TileSet)BundleUtil.loadObject( + _bundle, dpath, false); + _setcache.put(dpath, aset); + } + + // if that failed too, we're hosed + if (aset == null) { + // if this is a shadow image, no need to freak out as they + // are optional + if (!StandardActions.SHADOW_TYPE.equals(type)) { + Log.warning("Unable to locate tileset for action '" + + imgpath + "' " + component + "."); + if (_wipeOnFailure) { + _bundle.wipeBundle(false); + } + } + return null; + } + + aset.setImageProvider(this); + _setcache.put(cpath, aset); + return new TileSetFrameImage(aset, actseq); + + } catch (Exception e) { + Log.warning("Error loading tileset for action '" + imgpath + + "' " + component + "."); + Log.logStackTrace(e); + return null; + } + } + + /** The resource bundle from which we obtain image data. */ + protected ResourceBundle _bundle; + + /** Cache of tilesets loaded from our bundle. */ + protected HashMap _setcache = new HashMap(); + } + + /** + * Used to provide multiframe images using data obtained from a + * tileset. + */ + protected static class TileSetFrameImage implements ActionFrames + { + /** + * Constructs a tileset frame image with the specified tileset and + * for the specified orientation. + */ + public TileSetFrameImage (TileSet set, ActionSequence actseq) + { + _set = set; + _actseq = actseq; + + // compute these now to avoid pointless recomputation later + _ocount = actseq.orients.length; + _fcount = set.getTileCount() / _ocount; + + // create our mapping from orientation to animation sequence + // index + for (int ii = 0; ii < _ocount; ii++) { + _orients.put(actseq.orients[ii], ii); + } + } + + // documentation inherited from interface + public int getOrientationCount () + { + return _ocount; + } + + // documentation inherited from interface + public TrimmedMultiFrameImage getFrames (final int orient) + { + return new TrimmedMultiFrameImage() { + // documentation inherited + public int getFrameCount () + { + return _fcount; + } + + // documentation inherited from interface + public int getWidth (int index) + { + Tile tile = getTile(orient, index); + return (tile == null) ? 0 : tile.getWidth(); + } + + // documentation inherited from interface + public int getHeight (int index) + { + Tile tile = getTile(orient, index); + return (tile == null) ? 0 : tile.getHeight(); + } + + // documentation inherited from interface + public void paintFrame (Graphics2D g, int index, int x, int y) + { + Tile tile = getTile(orient, index); + if (tile != null) { + tile.paint(g, x, y); + } + } + + // documentation inherited from interface + public boolean hitTest (int index, int x, int y) + { + Tile tile = getTile(orient, index); + return (tile != null) ? tile.hitTest(x, y) : false; + } + + // documentation inherited from interface + public void getTrimmedBounds (int index, Rectangle bounds) + { + Tile tile = getTile(orient, index); + if (tile instanceof TrimmedTile) { + ((TrimmedTile)tile).getTrimmedBounds(bounds); + } else { + bounds.setBounds( + 0, 0, tile.getWidth(), tile.getHeight()); + } + } + }; + } + + // documentation inherited from interface + public int getXOrigin (int orient, int index) + { + return _actseq.origin.x; + } + + // documentation inherited from interface + public int getYOrigin (int orient, int index) + { + return _actseq.origin.y; + } + + // documentation inherited from interface + public ActionFrames cloneColorized (Colorization[] zations) + { + return new TileSetFrameImage(_set.clone(zations), _actseq); + } + + /** + * Fetches the requested tile. + */ + protected Tile getTile (int orient, int index) + { + int tileIndex = _orients.get(orient) * _fcount + index; + return _set.getTile(tileIndex); + } + + /** The tileset from which we obtain our frame images. */ + protected TileSet _set; + + /** The action sequence for which we're providing frame images. */ + protected ActionSequence _actseq; + + /** Frame and orientation counts. */ + protected int _fcount, _ocount; + + /** A mapping from orientation code to animation sequence + * index. */ + protected IntIntMap _orients = new IntIntMap(); + } + + /** We use the image manager to decode and cache images. */ + protected ImageManager _imgr; + + /** A table of action sequences. */ + protected HashMap _actions; + + /** A table of action sequence tilesets. */ + protected HashMap _actionSets; + + /** A table of component classes. */ + protected HashMap _classes; + + /** A table of component lists indexed on classname. */ + protected HashMap _classComps = new HashMap(); + + /** The component table. */ + protected HashIntMap _components = new HashIntMap(); + + /** Whether or not we wipe our bundles on any failure. */ + protected boolean _wipeOnFailure; +} diff --git a/src/java/com/threerings/cast/bundle/tools/ComponentBundlerTask.java b/src/java/com/threerings/cast/bundle/tools/ComponentBundlerTask.java new file mode 100644 index 00000000..5b2b8449 --- /dev/null +++ b/src/java/com/threerings/cast/bundle/tools/ComponentBundlerTask.java @@ -0,0 +1,624 @@ +// +// $Id: ComponentBundlerTask.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.bundle.tools; + +import java.awt.image.BufferedImage; +import javax.imageio.ImageIO; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.ObjectOutputStream; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +import java.util.jar.JarOutputStream; +import java.util.jar.JarEntry; +import java.util.zip.Deflater; + +import org.apache.commons.digester.Digester; + +import com.samskivert.io.PersistenceException; +import com.samskivert.util.ComparableArrayList; +import com.samskivert.util.FileUtil; +import com.samskivert.util.HashIntMap; +import com.samskivert.util.Tuple; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.FileSet; + +import com.threerings.media.tile.ImageProvider; +import com.threerings.media.tile.SimpleCachingImageProvider; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TrimmedTileSet; +import com.threerings.media.tile.tools.xml.SwissArmyTileSetRuleSet; + +import com.threerings.cast.ComponentIDBroker; +import com.threerings.cast.StandardActions; +import com.threerings.cast.bundle.BundleUtil; +import com.threerings.cast.tools.xml.ActionRuleSet; + +/** + * Ant task for creating component bundles. This task must be configured + * with a number of parameters: + * + *

+ * target=[path to bundle file, which will be created]
+ * mapfile=[path to the component map file which maintains a mapping from
+ *          component id to component class/name, it will be created the
+ *          first time and updated as new components are mapped]
+ * 
+ * + * It should also contain one or more nested <fileset> elements that + * enumerate the action tileset images that should be included in the + * component bundle. + */ +public class ComponentBundlerTask extends Task +{ + /** + * Sets the path to the bundle file that we'll be creating. + */ + public void setTarget (File target) + { + _target = target; + } + + /** + * Sets the path to the component map file that we'll use to obtain + * component ids for the bundled components. + */ + public void setMapfile (File mapfile) + { + _mapfile = mapfile; + } + + /** + * Sets the path to the action tilesets definition file. + */ + public void setActiondef (File actiondef) + { + _actionDef = actiondef; + } + + /** + * Sets the root path which will be stripped from the image paths + * prior to parsing them to obtain the component class, type and + * action names. + */ + public void setRoot (File root) + { + _root = root.getPath(); + } + + /** + * Adds a nested <fileset> element. + */ + public void addFileset (FileSet set) + { + _filesets.add(set); + } + + /** + * Performs the actual work of the task. + */ + public void execute () throws BuildException + { + // make sure everything was set up properly + ensureSet(_target, "Must specify the path to the target bundle " + + "file via the 'target' attribute."); + ensureSet(_mapfile, "Must specify the path to the component map " + + "file via the 'mapfile' attribute."); + ensureSet(_actionDef, "Must specify the action sequence " + + "definitions via the 'actiondef' attribute."); + + // parse in the action tilesets + HashMap actsets = parseActionTileSets(); + + // load up our component ID broker + ComponentIDBroker broker = loadBroker(_mapfile); + + // check to see if any of the source files are newer than the + // target file + ArrayList sources = (ArrayList)_filesets.clone(); + sources.add(_mapfile); + sources.add(_actionDef); + if (!outOfDate(sources, _target)) { + System.out.println(_target.getPath() + " is up to date."); + return; + } + + // create an image provider for loading our component images + ImageProvider improv = new SimpleCachingImageProvider() { + protected BufferedImage loadImage (String path) + throws IOException { + return ImageIO.read(new File(path)); + } + }; + + System.out.println("Generating " + _target.getPath() + "..."); + + try { + // make sure we can create our bundle file + FileOutputStream fout = new FileOutputStream(_target); + JarOutputStream jout = new JarOutputStream(fout); + jout.setLevel(Deflater.BEST_COMPRESSION); + + // we'll fill this with component id to tuple mappings + HashIntMap mapping = new HashIntMap(); + + // herein we'll insert trimmed tileset objects that go along + // with each of the trimmed action images + HashIntMap actionSets = new HashIntMap(); + + // deal with the filesets + for (int i = 0; i < _filesets.size(); i++) { + FileSet fs = (FileSet)_filesets.get(i); + DirectoryScanner ds = fs.getDirectoryScanner(getProject()); + File fromDir = fs.getDir(getProject()); + String[] srcFiles = ds.getIncludedFiles(); + + for (int f = 0; f < srcFiles.length; f++) { + File cfile = new File(fromDir, srcFiles[f]); + // determine the [class, name, action] triplet + String[] info = decomposePath(cfile.getPath()); + + // make sure we have an action tileset definition + TileSet aset = (TileSet)actsets.get(info[2]); + if (aset == null) { + System.err.println( + "No tileset definition for component action " + + "[class=" + info[0] + ", name=" + info[1] + + ", action=" + info[2] + "]."); + continue; + } + aset.setImageProvider(improv); + + // obtain the component id from our id broker + int cid = broker.getComponentID(info[0], info[1]); + // add a mapping for this component + mapping.put(cid, new Tuple(info[0], info[1])); + + // process and store the main component image + processComponent(info, aset, cfile, jout); + + // pick up any auxiliary images as well like the shadow or + // crop files + String action = info[2]; + String ext = BundleUtil.IMAGE_EXTENSION; + for (int aa = 0; aa < AUX_EXTS.length; aa++) { + File afile = new File( + FileUtil.resuffix(cfile, ext, AUX_EXTS[aa] + ext)); + if (afile.exists()) { + info[2] = action + AUX_EXTS[aa]; + processComponent(info, aset, afile, jout); + } + } + } + } + + // write our mapping table to the jar file as well + jout.putNextEntry(new JarEntry(BundleUtil.COMPONENTS_PATH)); + ObjectOutputStream oout = new ObjectOutputStream(jout); + oout.writeObject(mapping); + oout.flush(); + + // seal up our jar file + jout.close(); + + } catch (IOException ioe) { + String errmsg = "Unable to create component bundle."; + throw new BuildException(errmsg, ioe); + + } catch (PersistenceException pe) { + String errmsg = "Unable to obtain component ID mapping."; + throw new BuildException(errmsg, pe); + } + + // save our updated component ID broker + saveBroker(_mapfile, broker); + } + + protected void processComponent ( + String[] info, TileSet aset, File cfile, JarOutputStream jout) + throws IOException + { + // construct the path that'll go in the jar file + String ipath = composePath( + info, BundleUtil.IMAGE_EXTENSION); + jout.putNextEntry(new JarEntry(ipath)); + + // create a trimmed tileset based on the source action tileset and + // stuff the new trimmed image into the jar file at the same time + aset.setImagePath(cfile.getPath()); + + TrimmedTileSet tset = null; + try { + tset = TrimmedTileSet.trimTileSet(aset, jout); + tset.setImagePath(ipath); + } catch (Throwable t) { + System.err.println( + "Failure trimming tileset " + + "[class=" + info[0] + ", name=" + info[1] + + ", action=" + info[2] + + ", srcimg=" + aset.getImagePath() + "]."); + t.printStackTrace(System.err); + } + + // then write our trimmed tileset to the jar file + if (tset != null) { + String tpath = composePath( + info, BundleUtil.TILESET_EXTENSION); + jout.putNextEntry(new JarEntry(tpath)); + ObjectOutputStream oout = new ObjectOutputStream(jout); + oout.writeObject(tset); + oout.flush(); + } + } + + protected boolean outOfDate (ArrayList sources, File target) + { + for (int ii = 0; ii < sources.size(); ii++) { + if (outOfDate(sources.get(ii), target)) { + return true; + } + } + return false; + } + + protected boolean outOfDate (Object source, File target) + { + if (source instanceof FileSet) { + FileSet fs = (FileSet)source; + DirectoryScanner ds = fs.getDirectoryScanner(getProject()); + File fromDir = fs.getDir(getProject()); + String[] srcFiles = ds.getIncludedFiles(); + for (int f = 0; f < srcFiles.length; f++) { + File cfile = new File(fromDir, srcFiles[f]); + if (newer(cfile, target)) { + return true; + } + } + return false; + + } else if (source instanceof File) { + return newer((File)source, target); + + } else { + System.err.println("Can't compare " + source + + " to " + target + "."); + return true; + } + } + + /** + * Returns true if source is newer than + * target. + */ + protected boolean newer (File source, File target) + { + return source.lastModified() > target.lastModified(); + } + + /** + * Decomposes the full path to a component image into a [class, name, + * action] triplet. + */ + protected String[] decomposePath (String path) + throws BuildException + { + // first strip off the root + if (!path.startsWith(_root)) { + throw new BuildException("Can't bundle images outside the " + + "root directory [root=" + _root + + ", path=" + path + "]."); + } + path = path.substring(_root.length()); + + // strip off any preceding slash + if (path.startsWith("/")) { + path = path.substring(1); + } + + // now strip off the file extension + if (!path.endsWith(BundleUtil.IMAGE_EXTENSION)) { + throw new BuildException("Can't bundle malformed image file " + + "[path=" + path + "]."); + } + path = path.substring(0, path.length() - + BundleUtil.IMAGE_EXTENSION.length()); + + // now decompose the path; the component type and action must + // always be a single string but the class can span multiple + // directories for easier component organization; thus + // "male/head/goatee/standing" will be parsed as [class=male/head, + // type=goatee, action=standing] + String malmsg = "Can't decode malformed image path: '" + path + "'"; + String[] info = new String[3]; + int lsidx = path.lastIndexOf(File.separator); + if (lsidx == -1) { + throw new BuildException(malmsg); + } + info[2] = path.substring(lsidx+1); + int slsidx = path.lastIndexOf(File.separator, lsidx-1); + if (slsidx == -1) { + throw new BuildException(malmsg); + } + info[1] = path.substring(slsidx+1, lsidx); + info[0] = path.substring(0, slsidx); + return info; + } + + /** + * Composes a triplet of [class, name, action] into the path that + * should be supplied to the JarEntry that contains the associated + * image data. + */ + protected String composePath (String[] info, String extension) + { + return (info[0] + File.separator + info[1] + + File.separator + info[2] + extension); + } + + protected void ensureSet (Object value, String errmsg) + throws BuildException + { + if (value == null) { + throw new BuildException(errmsg); + } + } + + /** + * Parses the action tileset definitions and puts them into a hash + * map, keyed on action name. + */ + protected HashMap parseActionTileSets () + { + Digester digester = new Digester(); + SwissArmyTileSetRuleSet srules = new SwissArmyTileSetRuleSet(); + String aprefix = "actions" + ActionRuleSet.ACTION_PATH; + srules.setPrefix(aprefix); + + digester.addRuleSet(srules); + digester.addSetProperties(aprefix); + digester.addSetNext(aprefix + SwissArmyTileSetRuleSet.TILESET_PATH, + "addTileSet", TileSet.class.getName()); + + HashMap actsets = new ActionMap(); + digester.push(actsets); + + try { + FileInputStream fin = new FileInputStream(_actionDef); + BufferedInputStream bin = new BufferedInputStream(fin); + digester.parse(bin); + + } catch (FileNotFoundException fnfe) { + String errmsg = "Unable to load action definition file " + + "[path=" + _actionDef.getPath() + "]."; + throw new BuildException(errmsg, fnfe); + + } catch (Exception e) { + throw new BuildException("Parsing error.", e); + } + + return actsets; + } + + /** Used when parsing action tilesets. */ + public static class ActionMap extends HashMap + { + public void setName (String name) { + _name = name; + } + public void addTileSet (TileSet set) { + set.setName(_name); + put(_name, set); + } + protected String _name; + } + + /** + * Loads the hashmap ID broker from its persistent representation in + * the specified file. + */ + protected HashMapIDBroker loadBroker (File mapfile) + throws BuildException + { + HashMapIDBroker broker = new HashMapIDBroker(); + + try { + BufferedReader bin = new BufferedReader(new FileReader(mapfile)); + broker.readFrom(bin); + bin.close(); + + } catch (FileNotFoundException fnfe) { + // if the file doesn't yet exist, start with a blank broker + + } catch (Exception e) { + throw new BuildException("Error loading component ID map " + + "[mapfile=" + mapfile + "]", e); + } + + return broker; + } + + /** + * Stores a persistent representation of the supplied hashmap ID + * broker in the specified file. + */ + protected void saveBroker (File mapfile, ComponentIDBroker broker) + throws BuildException + { + HashMapIDBroker hbroker = (HashMapIDBroker)broker; + + // bail if the broker wasn't modified + if (!hbroker.isModified()) { + return; + } + + try { + BufferedWriter bout = new BufferedWriter(new FileWriter(mapfile)); + hbroker.writeTo(bout); + bout.close(); + } catch (IOException ioe) { + throw new BuildException("Unable to store component ID map " + + "[mapfile=" + mapfile + "]", ioe); + } + } + + protected static class HashMapIDBroker + extends HashMap implements ComponentIDBroker + { + public int getComponentID (String cclass, String cname) + throws PersistenceException + { + Tuple key = new Tuple(cclass, cname); + Integer cid = (Integer)get(key); + if (cid == null) { + cid = Integer.valueOf(++_nextCID); + put(key, cid); + } + return cid.intValue(); + } + + public void commit () + throws PersistenceException + { + // nothing doing + } + + public boolean isModified () + { + return _nextCID != _startCID; + } + + public void writeTo (BufferedWriter bout) + throws IOException + { + // write out our most recently assigned component id + String cidline = "" + _nextCID; + bout.write(cidline, 0, cidline.length()); + bout.newLine(); + + // write out the keys and values + ComparableArrayList lines = new ComparableArrayList(); + Iterator keys = keySet().iterator(); + while (keys.hasNext()) { + Tuple key = (Tuple)keys.next(); + Integer value = (Integer)get(key); + String line = key.left + SEP_STR + key.right + SEP_STR + value; + lines.add(line); + } + + // sort the output + lines.sort(); + + // now write it to the file + int lcount = lines.size(); + for (int ii = 0; ii < lcount; ii++) { + String line = (String)lines.get(ii); + bout.write(line, 0, line.length()); + bout.newLine(); + } + } + + public void readFrom (BufferedReader bin) + throws IOException + { + // read in our most recently assigned component id + _nextCID = readInt(bin); + // keep track of this so that we can tell if we were modified + _startCID = _nextCID; + + // now read in our keys and values + String line; + while ((line = bin.readLine()) != null) { + String orig = line; + int sidx = line.indexOf(SEP_STR); + if (sidx == -1) { + throw new IOException("Malformed line '" + orig + "'"); + } + String cclass = line.substring(0, sidx); + line = line.substring(sidx + SEP_STR.length()); + sidx = line.indexOf(SEP_STR); + if (sidx == -1) { + throw new IOException("Malformed line '" + orig + "'"); + } + String cname = line.substring(0, sidx); + line = line.substring(sidx + SEP_STR.length()); + try { + put(new Tuple(cclass, cname), Integer.valueOf(line)); + } catch (NumberFormatException nfe) { + String err = "Malformed line, invalid code '" + orig + "'"; + throw new IOException(err); + } + } + } + + protected int readInt (BufferedReader bin) + throws IOException + { + String line = bin.readLine(); + try { + return Integer.parseInt(line); + } catch (NumberFormatException nfe) { + throw new IOException("Expected number, got '" + line + "'"); + } + } + + protected int _nextCID = 0; + protected int _startCID = 0; + } + + /** The path to our component bundle file. */ + protected File _target; + + /** The path to our component map file. */ + protected File _mapfile; + + /** The path to our action tilesets definition file. */ + protected File _actionDef; + + /** The component directory root. */ + protected String _root; + + /** A list of filesets that contain tile images. */ + protected ArrayList _filesets = new ArrayList(); + + /** Used to separate keys and values in the map file. */ + protected static final String SEP_STR = " := "; + + /** Used to process auxilliary tilesets. */ + protected static final String[] AUX_EXTS = { + "_" + StandardActions.SHADOW_TYPE, + "_" + StandardActions.CROP_TYPE }; +} diff --git a/src/java/com/threerings/cast/bundle/tools/DumpBundle.java b/src/java/com/threerings/cast/bundle/tools/DumpBundle.java new file mode 100644 index 00000000..82f0f800 --- /dev/null +++ b/src/java/com/threerings/cast/bundle/tools/DumpBundle.java @@ -0,0 +1,84 @@ +// +// $Id: DumpBundle.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.bundle.tools; + +import java.io.File; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.samskivert.util.HashIntMap; +import com.samskivert.util.StringUtil; + +import com.threerings.resource.ResourceBundle; +import com.threerings.cast.bundle.BundleUtil; + +/** + * Dumps the contents of a component bundle to stdout. + */ +public class DumpBundle +{ + public static void main (String[] args) + { + if (args.length < 1) { + String usage = "Usage: DumpBundle bundle.jar [bundle.jar ...]"; + System.err.println(usage); + System.exit(-1); + } + + for (int i = 0; i < args.length; i++) { + File file = new File(args[i]); + try { + ResourceBundle bundle = new ResourceBundle(file); + + HashMap actions = (HashMap)BundleUtil.loadObject( + bundle, BundleUtil.ACTIONS_PATH, false); + dumpTable("actions: ", actions); + + HashMap actionSets = (HashMap)BundleUtil.loadObject( + bundle, BundleUtil.ACTION_SETS_PATH, false); + dumpTable("actionSets: ", actionSets); + + HashMap classes = (HashMap)BundleUtil.loadObject( + bundle, BundleUtil.CLASSES_PATH, false); + dumpTable("classes: ", classes); + + HashIntMap comps = (HashIntMap)BundleUtil.loadObject( + bundle, BundleUtil.COMPONENTS_PATH, false); + dumpTable("components: ", comps); + + } catch (Exception e) { + System.err.println("Error dumping bundle [path=" + args[i] + + ", error=" + e + "]."); + e.printStackTrace(); + } + } + } + + protected static void dumpTable (String prefix, Map table) + { + if (table != null) { + Iterator iter = table.entrySet().iterator(); + System.out.println(prefix + StringUtil.toString(iter)); + } + } +} diff --git a/src/java/com/threerings/cast/bundle/tools/MetadataBundlerTask.java b/src/java/com/threerings/cast/bundle/tools/MetadataBundlerTask.java new file mode 100644 index 00000000..186cde0a --- /dev/null +++ b/src/java/com/threerings/cast/bundle/tools/MetadataBundlerTask.java @@ -0,0 +1,267 @@ +// +// $Id: MetadataBundlerTask.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.bundle.tools; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; + +import java.util.ArrayList; +import java.util.HashMap; + +import java.util.jar.JarOutputStream; +import java.util.jar.JarEntry; +import java.util.zip.Deflater; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Task; + +import org.apache.commons.digester.Digester; + +import com.samskivert.util.Tuple; + +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.tools.xml.SwissArmyTileSetRuleSet; + +import com.threerings.cast.ActionSequence; +import com.threerings.cast.ComponentClass; +import com.threerings.cast.bundle.BundleUtil; + +import com.threerings.cast.tools.xml.ActionRuleSet; +import com.threerings.cast.tools.xml.ClassRuleSet; + +/** + * Ant task for creating metadata bundles, which contain action sequence + * and component class definition information. This task must be + * configured with a number of parameters: + * + *
+ * actiondef=[path to actions.xml]
+ * classdef=[path to classes.xml]
+ * file=[path to metadata bundle, which will be created]
+ * 
+ */ +public class MetadataBundlerTask extends Task +{ + public void setActiondef (String actiondef) + { + _actionDef = actiondef; + } + + public void setClassdef (String classdef) + { + _classDef = classdef; + } + + public void setTarget (File target) + { + _target = target; + } + + /** + * Performs the actual work of the task. + */ + public void execute () + throws BuildException + { + // make sure everythign was set up properly + ensureSet(_actionDef, "Must specify the action sequence " + + "definitions via the 'actiondef' attribute."); + ensureSet(_classDef, "Must specify the component class definitions " + + "via the 'classdef' attribute."); + ensureSet(_target, "Must specify the path to the target bundle " + + "file via the 'target' attribute."); + + // make sure we can write to the target bundle file + FileOutputStream fout = null; + try { + fout = new FileOutputStream(_target); + + // parse our metadata + Tuple tuple = parseActions(); + HashMap actions = (HashMap)tuple.left; + HashMap actionSets = (HashMap)tuple.right; + HashMap classes = parseClasses(); + + // and create the bundle file + JarOutputStream jout = new JarOutputStream(fout); + jout.setLevel(Deflater.BEST_COMPRESSION); + + // throw the serialized actions table in there + JarEntry aentry = new JarEntry(BundleUtil.ACTIONS_PATH); + jout.putNextEntry(aentry); + ObjectOutputStream oout = new ObjectOutputStream(jout); + oout.writeObject(actions); + oout.flush(); + + // throw the serialized action tilesets table in there + JarEntry sentry = new JarEntry(BundleUtil.ACTION_SETS_PATH); + jout.putNextEntry(sentry); + oout = new ObjectOutputStream(jout); + oout.writeObject(actionSets); + oout.flush(); + + // throw the serialized classes table in there + JarEntry centry = new JarEntry(BundleUtil.CLASSES_PATH); + jout.putNextEntry(centry); + oout = new ObjectOutputStream(jout); + oout.writeObject(classes); + oout.flush(); + + // close it up and we're done + jout.close(); + + } catch (IOException ioe) { + String errmsg = "Unable to output to target bundle " + + "[path=" + _target.getPath() + "]."; + throw new BuildException(errmsg, ioe); + + } finally { + if (fout != null) { + try { + fout.close(); + } catch (IOException ioe) { + // nothing to complain about here + } + } + } + } + + protected Tuple parseActions () + throws BuildException + { + // scan through the XML once to read the actions + Digester digester = new Digester(); + ActionRuleSet arules = new ActionRuleSet(); + arules.setPrefix("actions"); + digester.addRuleSet(arules); + digester.addSetNext("actions" + ActionRuleSet.ACTION_PATH, + "add", Object.class.getName()); + ArrayList actlist = parseList(digester, _actionDef); + + // now go through a second time reading the tileset info + digester = new Digester(); + SwissArmyTileSetRuleSet srules = new SwissArmyTileSetRuleSet(); + srules.setPrefix("actions" + ActionRuleSet.ACTION_PATH); + digester.addRuleSet(srules); + digester.addSetNext("actions" + ActionRuleSet.ACTION_PATH + + SwissArmyTileSetRuleSet.TILESET_PATH, "add", + Object.class.getName()); + ArrayList setlist = parseList(digester, _actionDef); + + // sanity check + if (actlist.size() != setlist.size()) { + String errmsg = "An action is missing its tileset " + + "definition, or something even wackier is going on."; + throw new BuildException(errmsg); + } + + // now create our mappings + HashMap actmap = new HashMap(); + HashMap setmap = new HashMap(); + + // create the action map + for (int i = 0; i < setlist.size(); i++) { + TileSet set = (TileSet)setlist.get(i); + ActionSequence act = (ActionSequence)actlist.get(i); + // make sure nothing was missing in the action sequence + // definition parsed from XML + String errmsg = ActionRuleSet.validate(act); + if (errmsg != null) { + errmsg = "Action sequence invalid [seq=" + act + + ", error=" + errmsg + "]."; + throw new BuildException(errmsg); + } + actmap.put(act.name, act); + setmap.put(act.name, set); + } + + return new Tuple(actmap, setmap); + } + + protected HashMap parseClasses () + throws BuildException + { + // load up our action and class info + Digester digester = new Digester(); + + // add our action rule set and a a rule to grab parsed actions + ClassRuleSet crules = new ClassRuleSet(); + crules.setPrefix("classes"); + digester.addRuleSet(crules); + digester.addSetNext("classes" + ClassRuleSet.CLASS_PATH, + "add", Object.class.getName()); + + ArrayList setlist = parseList(digester, _classDef); + HashMap clmap = new HashMap(); + + // create the action map + for (int i = 0; i < setlist.size(); i++) { + ComponentClass cl = (ComponentClass)setlist.get(i); + clmap.put(cl.name, cl); + } + + return clmap; + } + + protected ArrayList parseList (Digester digester, String path) + throws BuildException + { + try { + FileInputStream fin = new FileInputStream(path); + BufferedInputStream bin = new BufferedInputStream(fin); + + ArrayList setlist = new ArrayList(); + digester.push(setlist); + + // now fire up the digester to parse the stream + try { + digester.parse(bin); + } catch (Exception e) { + throw new BuildException("Parsing error.", e); + } + + return setlist; + + } catch (FileNotFoundException fnfe) { + String errmsg = "Unable to load metadata definition file " + + "[path=" + path + "]."; + throw new BuildException(errmsg, fnfe); + } + } + + protected void ensureSet (Object value, String errmsg) + throws BuildException + { + if (value == null) { + throw new BuildException(errmsg); + } + } + + protected String _actionDef; + protected String _classDef; + protected File _target; +} diff --git a/src/java/com/threerings/cast/tools/xml/ActionRuleSet.java b/src/java/com/threerings/cast/tools/xml/ActionRuleSet.java new file mode 100644 index 00000000..79fb326e --- /dev/null +++ b/src/java/com/threerings/cast/tools/xml/ActionRuleSet.java @@ -0,0 +1,146 @@ +// +// $Id: ActionRuleSet.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.tools.xml; + +import org.apache.commons.digester.Digester; +import org.apache.commons.digester.RuleSetBase; + +import com.samskivert.util.StringUtil; + +import com.samskivert.xml.CallMethodSpecialRule; +import com.samskivert.xml.SetFieldRule; +import com.samskivert.xml.SetPropertyFieldsRule; + +import com.threerings.util.DirectionCodes; +import com.threerings.util.DirectionUtil; + +import com.threerings.cast.ActionSequence; + +/** + * The action rule set is used to parse the attributes of an action + * sequence instance. + */ +public class ActionRuleSet extends RuleSetBase +{ + /** The component of the digester path that is appended by the action + * rule set to match a action. This is appended to whatever prefix is + * provided to the action rule set to obtain the complete XML path to + * a matched action. */ + public static final String ACTION_PATH = "/action"; + + /** + * Instructs the action rule set to match actions with the supplied + * prefix. For example, passing a prefix of actions will + * match actions in the following XML file: + * + *
+     * <actions>
+     *   <action>
+     *     // ...
+     *   </action>
+     * </actions>
+     * 
+ * + * This must be called before adding the ruleset to a digester. + */ + public void setPrefix (String prefix) + { + _prefix = prefix; + } + + /** + * Adds the necessary rules to the digester to parse our actions. + */ + public void addRuleInstances (Digester digester) + { + // this creates the appropriate instance when we encounter a + // tag + digester.addObjectCreate(_prefix + ACTION_PATH, + ActionSequence.class.getName()); + + // grab the name attribute from the tag + digester.addRule(_prefix + ACTION_PATH, new SetPropertyFieldsRule()); + + // grab the other attributes from their respective tags + digester.addRule(_prefix + ACTION_PATH + "/framesPerSecond", + new SetFieldRule("framesPerSecond")); + + CallMethodSpecialRule origin = new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + throws Exception { + int[] coords = StringUtil.parseIntArray(bodyText); + if (coords.length != 2) { + String errmsg = "Invalid specification '" + + bodyText + "'."; + throw new Exception(errmsg); + } + ((ActionSequence)target).origin.setLocation( + coords[0], coords[1]); + } + }; + digester.addRule(_prefix + ACTION_PATH + "/origin", origin); + + CallMethodSpecialRule orient = new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + throws Exception { + ActionSequence seq = ((ActionSequence)target); + String[] ostrs = StringUtil.parseStringArray(bodyText); + seq.orients = new int[ostrs.length]; + for (int ii = 0; ii < ostrs.length; ii++) { + int orient = DirectionUtil.fromShortString(ostrs[ii]); + if (orient != DirectionCodes.NONE) { + seq.orients[ii] = orient; + } else { + String errmsg = "Invalid orientation specification " + + "[index=" + ii + ", orient=" + ostrs[ii] + "]."; + throw new Exception(errmsg); + } + } + } + }; + digester.addRule(_prefix + ACTION_PATH + "/orients", orient); + } + + /** + * Validates that all necessary fields have been parsed and set in + * this action sequence object and are valid. + * + * @return null if the sequence is valid, a string explaining the + * invalidity if it is not. + */ + public static String validate (ActionSequence seq) + { + if (StringUtil.isBlank(seq.name)) { + return "Missing 'name' definition."; + } + if (seq.framesPerSecond == 0) { + return "Missing 'framesPerSecond' definition."; + } + if (seq.orients == null) { + return "Missing 'orients' definition."; + } + return null; + } + + /** The prefix at which me match our actions. */ + protected String _prefix; +} diff --git a/src/java/com/threerings/cast/tools/xml/ClassRuleSet.java b/src/java/com/threerings/cast/tools/xml/ClassRuleSet.java new file mode 100644 index 00000000..49020b32 --- /dev/null +++ b/src/java/com/threerings/cast/tools/xml/ClassRuleSet.java @@ -0,0 +1,110 @@ +// +// $Id: ClassRuleSet.java 3729 2005-10-13 00:00:33Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.tools.xml; + +import java.awt.Color; + +import org.apache.commons.digester.Digester; +import org.apache.commons.digester.RuleSetBase; + +import com.samskivert.util.ArrayIntSet; +import com.samskivert.util.StringUtil; +import com.samskivert.xml.SetPropertyFieldsRule; +import com.samskivert.xml.SetPropertyFieldsRule.FieldParser; + +import com.threerings.cast.ComponentClass.PriorityOverride; +import com.threerings.cast.ComponentClass; +import com.threerings.util.DirectionUtil; + +/** + * The class rule set is used to parse the attributes of a component class + * instance. + */ +public class ClassRuleSet extends RuleSetBase +{ + /** The component of the digester path that is appended by the class + * rule set to match a component class. This is appended to whatever + * prefix is provided to the class rule set to obtain the complete XML + * path to a matched class. */ + public static final String CLASS_PATH = "/class"; + + /** + * Instructs the class rule set to match component classes with the + * supplied prefix. For example, passing a prefix of + * classes will match classes in the following XML file: + * + *
+     * <classes>
+     *   <class .../>
+     * </classes>
+     * 
+ * + * This must be called before adding the ruleset to a digester. + */ + public void setPrefix (String prefix) + { + _prefix = prefix; + } + + /** + * Adds the necessary rules to the digester to parse our classes. + */ + public void addRuleInstances (Digester digester) + { + // this creates the appropriate instance when we encounter a + // tag + digester.addObjectCreate(_prefix + CLASS_PATH, + ComponentClass.class.getName()); + + // grab the attributes from the tag + SetPropertyFieldsRule rule = new SetPropertyFieldsRule(); + rule.addFieldParser("shadowColor", new FieldParser() { + public Object parse (String text) { + int[] values = StringUtil.parseIntArray(text); + return new Color(values[0], values[1], values[2], values[3]); + } + }); + digester.addRule(_prefix + CLASS_PATH, rule); + + // parse render priority overrides + String opath = _prefix + CLASS_PATH + "/override"; + digester.addObjectCreate(opath, PriorityOverride.class.getName()); + rule = new SetPropertyFieldsRule(); + rule.addFieldParser("orients", new FieldParser() { + public Object parse (String text) { + String[] orients = StringUtil.parseStringArray(text); + ArrayIntSet oset = new ArrayIntSet(); + for (int ii = 0; ii < orients.length; ii++) { + oset.add(DirectionUtil.fromShortString(orients[ii])); + } + return oset; + } + }); + digester.addRule(opath, rule); + + digester.addSetNext(opath, "addPriorityOverride", + PriorityOverride.class.getName()); + } + + /** The prefix at which me match our component classes. */ + protected String _prefix; +} diff --git a/src/java/com/threerings/cast/util/CastUtil.java b/src/java/com/threerings/cast/util/CastUtil.java new file mode 100644 index 00000000..22621c3b --- /dev/null +++ b/src/java/com/threerings/cast/util/CastUtil.java @@ -0,0 +1,97 @@ +// +// $Id: CastUtil.java 4188 2006-06-13 18:03:48Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.util; + +import java.util.ArrayList; +import java.util.Iterator; + +import com.samskivert.util.CollectionUtil; +import com.samskivert.util.RandomUtil; + +import com.threerings.cast.CharacterDescriptor; +import com.threerings.cast.ComponentClass; +import com.threerings.cast.ComponentRepository; +import com.threerings.cast.Log; + +/** + * Miscellaneous cast utility routines. + */ +public class CastUtil +{ + /** + * Returns a new character descriptor populated with a random set of + * components. + */ + public static CharacterDescriptor getRandomDescriptor ( + String gender, ComponentRepository crepo) + { + // get all available classes + ArrayList classes = new ArrayList(); + for (int i = 0; i < CLASSES.length; i++) { + String cname = gender + "/" + CLASSES[i]; + ComponentClass cclass = crepo.getComponentClass(cname); + + // make sure the component class exists + if (cclass == null) { + Log.warning("Missing definition for component class " + + "[class=" + cname + "]."); + continue; + } + + // make sure there are some components in this class + Iterator iter = crepo.enumerateComponentIds(cclass); + if (!iter.hasNext()) { + Log.info("Skipping class for which we have no components " + + "[class=" + cclass + "]."); + continue; + } + + classes.add(cclass); + } + + // select the components + int size = classes.size(); + int components[] = new int[size]; + for (int ii = 0; ii < size; ii++) { + ComponentClass cclass = (ComponentClass)classes.get(ii); + + // get the components available for this class + ArrayList choices = new ArrayList(); + Iterator iter = crepo.enumerateComponentIds(cclass); + CollectionUtil.addAll(choices, iter); + + // choose a random component + if (choices.size() > 0) { + int idx = RandomUtil.getInt(choices.size()); + components[ii] = ((Integer)choices.get(idx)).intValue(); + } else { + Log.info("Have no components in class [class=" + cclass + "]."); + } + } + + return new CharacterDescriptor(components, null); + } + + protected static final String[] CLASSES = { + "legs", "feet", "hand_left", "hand_right", "torso", + "head", "hair", "hat", "eyepatch" }; +} diff --git a/src/java/com/threerings/geom/GeomUtil.java b/src/java/com/threerings/geom/GeomUtil.java new file mode 100644 index 00000000..fc4391a5 --- /dev/null +++ b/src/java/com/threerings/geom/GeomUtil.java @@ -0,0 +1,251 @@ +// +// $Id: GeomUtil.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.geom; + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.geom.Point2D; + +/** + * General geometry utilites. + */ +public class GeomUtil +{ + /** + * Computes and returns the dot product of the two vectors. + * + * @param v1s the starting point of the first vector. + * @param v1e the ending point of the first vector. + * @param v2s the starting point of the second vector. + * @param v2e the ending point of the second vector. + */ + public static int dot (Point v1s, Point v1e, Point v2s, Point v2e) + { + return ((v1e.x - v1s.x) * (v2e.x - v2s.x) + + (v1e.y - v1s.y) * (v2e.y - v2s.y)); + } + + /** + * Computes and returns the dot product of the two vectors. See + * {@link #dot(Point,Point,Point,Point)} for an explanation of the + * arguments + */ + public static int dot (int v1sx, int v1sy, int v1ex, int v1ey, + int v2sx, int v2sy, int v2ex, int v2ey) + { + return ((v1ex - v1sx) * (v2ex - v2sx) + (v1ey - v1sy) * (v2ey - v2sy)); + } + + /** + * Computes and returns the dot product of the two vectors. The + * vectors are assumed to start with the same coordinate and end with + * different coordinates. + * + * @param vs the starting point of both vectors. + * @param v1e the ending point of the first vector. + * @param v2e the ending point of the second vector. + */ + public static int dot (Point vs, Point v1e, Point v2e) + { + return ((v1e.x - vs.x) * (v2e.x - vs.x) + + (v1e.y - vs.y) * (v2e.y - vs.y)); + } + + /** + * Computes and returns the dot product of the two vectors. + * See {@link #dot(Point,Point,Point)} for an explanation of the + * arguments + */ + public static int dot (int vsx, int vsy, int v1ex, int v1ey, + int v2ex, int v2ey) + { + return ((v1ex - vsx) * (v2ex - vsx) + (v1ey - vsy) * (v2ey - vsy)); + } + + /** + * Computes the point nearest to the specified point p3 + * on the line defined by the two points p1 and + * p2. The computed point is stored into n. + * Note: p1 and p2 must not be + * coincident. + * + * @param p1 one point on the line. + * @param p2 another point on the line (not equal to p1). + * @param p3 the point to which we wish to be most near. + * @param n the point on the line defined by p1 and + * p2 that is nearest to p. + * + * @return the point object supplied via n. + */ + public static Point nearestToLine (Point p1, Point p2, Point p3, Point n) + { + // see http://astronomy.swin.edu.au/~pbourke/geometry/pointline/ + // for a (not very good) explanation of the math + int Ax = p2.x - p1.x, Ay = p2.y - p1.y; + float u = (p3.x - p1.x) * Ax + (p3.y - p1.y) * Ay; + u /= (Ax * Ax + Ay * Ay); + n.x = p1.x + Math.round(Ax * u); + n.y = p1.y + Math.round(Ay * u); + return n; + } + + /** + * Calculate the intersection of two lines. Either line may be + * considered as a line segment, and the intersecting point + * is only considered valid if it lies upon the segment. + * Note that Point extends Point2D. + * + * @param p1 and p2 the coordinates of the first line. + * @param seg1 if the first line should be considered a segment. + * @param p3 and p4 the coordinates of the second line. + * @param seg2 if the second line should be considered a segment. + * @param result the point that will be filled in with the intersecting + * point. + * + * @return true if result was filled in, or false if the lines + * are parallel or the point of intersection lies outside of a + * segment. + */ + public static boolean lineIntersection ( + Point2D p1, Point2D p2, boolean seg1, + Point2D p3, Point2D p4, boolean seg2, Point2D result) + { + // see http://astronomy.swin.edu.au/~pbourke/geometry/lineline2d/ + double y43 = p4.getY() - p3.getY(); + double x21 = p2.getX() - p1.getX(); + double x43 = p4.getX() - p3.getX(); + double y21 = p2.getY() - p1.getY(); + double denom = y43 * x21 - x43 * y21; + if (denom == 0) { + return false; + } + + double y13 = p1.getY() - p3.getY(); + double x13 = p1.getX() - p3.getX(); + double ua = (x43 * y13 - y43 * x13) / denom; + if (seg1 && ((ua < 0) || (ua > 1))) { + return false; + } + + if (seg2) { + double ub = (x21 * y13 - y21 * x13) / denom; + if ((ub < 0) || (ub > 1)) { + return false; + } + } + + double x = p1.getX() + ua * x21; + double y = p1.getY() + ua * y21; + result.setLocation(x, y); + return true; + } + + /** + * Returns less than zero if p2 is on the left hand side + * of the line created by p1 and theta and + * greater than zero if it is on the right hand side. In theory, it + * will return zero if the point is on the line, but due to rounding + * errors it almost always decides that it's not exactly on the line. + * + * @param p1 the point on the line whose side we're checking. + * @param theta the (logical) angle defining the line. + * @param p2 the point that lies on one side or the other of the line. + */ + public static int whichSide (Point p1, double theta, Point p2) + { + // obtain the point defining the right hand normal (N) + theta += Math.PI/2; + int x = p1.x + (int)Math.round(1000*Math.cos(theta)), + y = p1.y + (int)Math.round(1000*Math.sin(theta)); + + // now dot the vector from p1->p2 with the vector from p1->N, if + // it's positive, we're on the right hand side, if it's negative + // we're on the left hand side and if it's zero, we're on the line + return dot(p1.x, p1.y, p2.x, p2.y, x, y); + } + + /** + * Shifts the position of the tainer rectangle to ensure + * that it contains the tained rectangle. The + * tainer rectangle must be larger than or equal to the + * size of the tained rectangle. + */ + public static void shiftToContain (Rectangle tainer, Rectangle tained) + { + if (tained.x < tainer.x) { + tainer.x = tained.x; + } + if (tained.y < tainer.y) { + tainer.y = tained.y; + } + if (tained.x + tained.width > tainer.x + tainer.width) { + tainer.x = tained.x - (tainer.width - tained.width); + } + if (tained.y + tained.height > tainer.y + tainer.height) { + tainer.y = tained.y - (tainer.height - tained.height); + } + } + + /** + * Adds the target rectangle to the bounds of the source rectangle. If + * the source rectangle is null, a new rectangle is created that is the + * size of the target rectangle. + * + * @return the source rectangle. + */ + public static Rectangle grow (Rectangle source, Rectangle target) + { + if (target == null) { + Log.warning("Can't grow with null rectangle [src=" + source + + ", tgt=" + target + "]."); + Thread.dumpStack(); + } else if (source == null) { + source = new Rectangle(target); + } else { + source.add(target); + } + return source; + } + + /** + * Returns the rectangle containing the specified tile in the supplied + * larger rectangle. Tiles go from left to right, top to bottom. + */ + public static Rectangle getTile ( + int width, int height, int tileWidth, int tileHeight, int tileIndex) + { + // figure out from whence to crop the tile + int tilesPerRow = width / tileWidth; + + // if we got a bogus region, return bogus tile bounds + if (tilesPerRow == 0) { + return new Rectangle(0, 0, width, height); + } + + int row = tileIndex / tilesPerRow; + int col = tileIndex % tilesPerRow; + + // crop the tile-sized image chunk from the full image + return new Rectangle( + tileWidth*col, tileHeight*row, tileWidth, tileHeight); + } +} diff --git a/src/java/com/threerings/geom/Log.java b/src/java/com/threerings/geom/Log.java new file mode 100644 index 00000000..96e630f6 --- /dev/null +++ b/src/java/com/threerings/geom/Log.java @@ -0,0 +1,63 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.geom; + +/** + * A placeholder class that contains a reference to the log object used by + * this package. + */ +public class Log +{ + public static final String PACKAGE = "geom"; + + public static com.samskivert.util.Log log = + new com.samskivert.util.Log(PACKAGE); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } + + public static int getLevel () + { + return com.samskivert.util.Log.getLevel(PACKAGE); + } +} diff --git a/src/java/com/threerings/jme/JmeApp.java b/src/java/com/threerings/jme/JmeApp.java new file mode 100644 index 00000000..d80cc5d2 --- /dev/null +++ b/src/java/com/threerings/jme/JmeApp.java @@ -0,0 +1,582 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme; + +import java.io.File; + +import com.samskivert.util.Queue; +import com.samskivert.util.RunQueue; +import com.samskivert.util.StringUtil; + +import com.jme.renderer.Camera; +import com.jme.renderer.ColorRGBA; +import com.jme.renderer.Renderer; + +import com.jme.scene.Node; +import com.jme.scene.state.LightState; +import com.jme.scene.state.ZBufferState; + +import com.jme.system.DisplaySystem; +import com.jme.system.JmeException; +import com.jme.system.PropertiesIO; +import com.jme.system.lwjgl.LWJGLPropertiesDialog; + +import com.jme.input.InputHandler; +import com.jme.input.KeyInput; +import com.jme.input.Mouse; +import com.jme.input.MouseInput; +import com.jmex.bui.BRootNode; +import com.jmex.bui.PolledRootNode; + +import com.jme.light.PointLight; +import com.jme.math.Vector3f; +import com.jme.util.Timer; + +import com.threerings.jme.camera.CameraHandler; + +/** + * Defines a basic application framework providing integration with the + * Presents networking system and + * targeting the framerate of the display. + */ +public class JmeApp + implements RunQueue +{ + /** + * Returns a context implementation that provides access to all the + * necessary bits. + */ + public JmeContext getContext () + { + return _ctx; + } + + /** + * Does the main initialization of the application. This method should + * be called first, and then the {@link #run} method should be called + * to begin the rendering/event loop. Derived classes can override + * this, being sure to call super before doing their own + * initalization. + * + * @return true if the application initialized successfully, false if + * initialization failed. (See {@link #reportInitFailure}.) + */ + public boolean init () + { + try { + // initialize the rendering system + initDisplay(); + if (!_display.isCreated()) { + throw new IllegalStateException( + "Failed to initialize display?"); + } + + // create an appropriate timer + _timer = Timer.getTimer(); + + // start with the target FPS equal to the refresh rate (but + // sometimes the refresh rate is reported as zero so don't let that + // freak us out) + setTargetFPS(Math.max(_display.getFrequency(), 60)); + + // initialize our main camera controls and user input handling + initInput(); + + // initialize the root node + initRoot(); + + // initialize the lighting + initLighting(); + + // initialize the UI support stuff + initInterface(); + + // update everything for the zeroth tick + _iface.updateRenderState(); + _geom.updateRenderState(); + _root.updateGeometricState(0f, true); + _root.updateRenderState(); + + return true; + + } catch (Throwable t) { + reportInitFailure(t); + return false; + } + } + + /** + * Configures whether or not we display FPS and other statistics atop the + * display. + */ + public void displayStatistics (boolean display) + { + if (display && (_stats == null)) { + _stats = new StatsDisplay(_display.getRenderer()); + _stats.updateGeometricState(0f, true); + _stats.updateRenderState(); + } else if (!display && (_stats != null)) { + _stats = null; + } + } + + /** + * Returns true if we are displaying statistics, false if not. + */ + public boolean showingStatistics () + { + return (_stats != null); + } + + /** + * Sets the target frames per second. + * + * @return the old target frames per second. + */ + public int setTargetFPS (int targetFPS) + { + int oldTargetFPS = _targetFPS; + _targetFPS = targetFPS; + _ticksPerFrame = _timer.getResolution() / _targetFPS; + return oldTargetFPS; + } + + /** + * Returns the frames per second averaged over the last 32 frames. + */ + public float getRecentFrameRate () + { + return _timer.getFrameRate(); + } + + /** + * Enables or disables the update and render part of the + * update/render/process events application loop. + */ + public void setEnabled (boolean update, boolean render) + { + _updateEnabled = update; + _renderEnabled = render; + } + + /** + * Starts up the main rendering and event processing loop. This method + * will not return until the application is terminated with a call to + * {@link #stop}. + */ + public void run () + { + synchronized (this) { + _dispatchThread = Thread.currentThread(); + } + + // enter the main rendering and event processing loop + while (!_finished && !_display.isClosing()) { + try { + // render the current frame + long frameStart = processFrame(); + + // and process events until it's time to render the next + PROCESS_EVENTS: + do { + Runnable r = (Runnable)_evqueue.getNonBlocking(); + if (r != null) { + r.run(); + } + if (_timer.getTime() - frameStart >= _ticksPerFrame) { + break PROCESS_EVENTS; + } else { + try { + Thread.sleep(1); + } catch (InterruptedException ie) { + } + } + } while (!_finished); + _failures = 0; + + } catch (Throwable t) { + Log.logStackTrace(t); + // stick a fork in things if we fail too many times in a row + if (++_failures > MAX_SUCCESSIVE_FAILURES) { + stop(); + } + } + } + + try { + cleanup(); + } catch (Throwable t) { + Log.logStackTrace(t); + } finally { + exit(); + } + } + + /** + * Returns the duration (in seconds) between the previous frame and the + * current frame. + */ + public float getFrameTime () + { + return _frameTime; + } + + /** + * Instructs the application to stop the main loop, cleanup and exit. + */ + public void stop () + { + _finished = true; + } + + // documentation inherited from interface RunQueue + public void postRunnable (Runnable r) + { + _evqueue.append(r); + } + + // documentation inherited from interface RunQueue + public boolean isDispatchThread () + { + return Thread.currentThread() == _dispatchThread; + } + + /** + * Determines the display configuration and creates the display. This must + * fill in {@link #_api} as a side-effect. + */ + protected DisplaySystem createDisplay () + { + PropertiesIO props = new PropertiesIO(getConfigPath("jme.cfg")); + if (!props.load()) { + LWJGLPropertiesDialog dialog = + new LWJGLPropertiesDialog(props, (String)null); + while (dialog.isVisible()) { + try { + Thread.sleep(5); + } catch (InterruptedException e) { + Log.warning("Error waiting for dialog system, " + + "using defaults."); + } + } + } + _api = props.getRenderer(); + DisplaySystem display = DisplaySystem.getDisplaySystem(_api); + display.createWindow(props.getWidth(), props.getHeight(), + props.getDepth(), props.getFreq(), + props.getFullscreen()); + return display; + } + + /** + * Initializes the underlying rendering system, creating a display of + * the proper resolution and depth. + */ + protected void initDisplay () + throws JmeException + { + // create the main display system + _display = createDisplay(); + + // create a camera + int width = _display.getWidth(), height = _display.getHeight(); + _camera = _display.getRenderer().createCamera(width, height); + + // start with a black background + _display.getRenderer().setBackgroundColor(ColorRGBA.black); + + // set up the camera + _camera.setFrustumPerspective(45.0f, width/(float)height, 1, 10000); + Vector3f loc = new Vector3f(0.0f, 0.0f, 25.0f); + Vector3f left = new Vector3f(-1.0f, 0.0f, 0.0f); + Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f); + Vector3f dir = new Vector3f(0.0f, 0f, -1.0f); + _camera.setFrame(loc, left, up, dir); + _camera.update(); + _display.getRenderer().setCamera(_camera); + + // tell the renderer to keep track of rendering information (total + // triangles drawn, etc.) + _display.getRenderer().enableStatistics(true); + } + + /** + * Sets up a main input controller to handle the camera and deal with + * global user input. + */ + protected void initInput () + { + _camhand = createCameraHandler(_camera); + _input = createInputHandler(_camhand); + } + + /** + * Creates the camera handler which provides various camera manipulation + * functionality. + */ + protected CameraHandler createCameraHandler (Camera camera) + { + return new CameraHandler(camera); + } + + /** + * Creates the input handler used to control our camera and manage non-UI + * keyboard input. + */ + protected InputHandler createInputHandler (CameraHandler hand) + { + return new InputHandler(); + } + + /** + * Creates our root node and sets up the basic rendering system. + */ + protected void initRoot () + { + _root = new Node("Root"); + + // set up a node for our geometry + _geom = new Node("Geometry"); + + // make everything opaque by default + _geom.setRenderQueueMode(Renderer.QUEUE_OPAQUE); + + // set up a zbuffer + ZBufferState zbuf = _display.getRenderer().createZBufferState(); + zbuf.setEnabled(true); + zbuf.setFunction(ZBufferState.CF_LEQUAL); + _geom.setRenderState(zbuf); + _root.attachChild(_geom); + } + + /** + * Sets up some default lighting. + */ + protected void initLighting () + { + PointLight light = new PointLight(); + light.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f)); + light.setAmbient(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f)); + light.setLocation(new Vector3f(100, 100, 100)); + light.setEnabled(true); + + _lights = _display.getRenderer().createLightState(); + _lights.setEnabled(true); + _lights.attach(light); + _geom.setRenderState(_lights); + } + + /** + * Initializes our user interface bits. + */ + protected void initInterface () + { + // set up a node for our interface + _iface = new Node("Interface"); + _root.attachChild(_iface); + + // create our root node + _rnode = createRootNode(); + _iface.attachChild(_rnode); + + // we don't hide the cursor + MouseInput.get().setCursorVisible(true); + } + + /** + * Allows a customized root node to be created. + */ + protected BRootNode createRootNode () + { + return new PolledRootNode(_timer, _input); + } + + /** + * Called when initialization fails to give the application a chance + * to report the failure to the user. + */ + protected void reportInitFailure (Throwable t) + { + Log.logStackTrace(t); + } + + /** + * Processes a single frame. + */ + protected final long processFrame () + { + // update our simulation and render a frame + long frameStart = _timer.getTime(); + if (_updateEnabled) { + update(frameStart); + } + if (_renderEnabled) { + render(frameStart); + _display.getRenderer().displayBackBuffer(); + } + return frameStart; + } + + /** + * Called every frame to update whatever sort of real time business we + * have that needs updating. + */ + protected void update (long frameTick) + { + // recalculate the frame rate + _timer.update(); + + // update the camera handler + _camhand.update(_frameTime); + + // run all of the controllers attached to nodes + _frameTime = (_lastTick == 0L) ? 0f : (float)(frameTick - _lastTick) / + _timer.getResolution(); + _lastTick = frameTick; + _root.updateGeometricState(_frameTime, true); + + // update our stats display if we have one + if (_stats != null) { + _stats.update(_timer, _display.getRenderer()); + } + } + + /** + * Called every frame to issue the rendering instructions for this frame. + */ + protected void render (float frameTime) + { + // clear out our previous information + _display.getRenderer().clearStatistics(); + _display.getRenderer().clearBuffers(); + + // draw the root node and all of its children + _display.getRenderer().draw(_root); + + // this would render bounding boxes + // _display.getRenderer().drawBounds(_root); + + // draw our stats atop everything + if (_stats != null) { + _display.getRenderer().draw(_stats); + } + } + + /** + * Called when the application is terminating cleanly after having + * successfully completed initialization and begun the main loop. + */ + protected void cleanup () + { + _display.reset(); + KeyInput.destroyIfInitalized(); + MouseInput.destroyIfInitalized(); + } + + /** + * Closes the display and exits the JVM process. + */ + protected void exit () + { + if (_display != null) { + _display.close(); + } + System.exit(0); + } + + /** + * Prepends the necessary bits onto the supplied path to properly + * locate it in our configuration directory. + */ + protected String getConfigPath (String file) + { + String cfgdir = ".narya"; + String home = System.getProperty("user.home"); + if (!StringUtil.isBlank(home)) { + cfgdir = home + File.separator + cfgdir; + } + // create the configuration directory if it does not already exist + File dir = new File(cfgdir); + if (!dir.exists()) { + dir.mkdir(); + } + return cfgdir + File.separator + file; + } + + /** Provides access to various needed bits. */ + protected JmeContext _ctx = new JmeContext() { + public DisplaySystem getDisplay () { + return _display; + } + + public Renderer getRenderer () { + return _display.getRenderer(); + } + + public CameraHandler getCameraHandler () { + return _camhand; + } + + public Node getGeometry () { + return _geom; + } + + public Node getInterface () { + return _iface; + } + + public InputHandler getInputHandler () { + return _input; + } + + public BRootNode getRootNode () { + return _rnode; + } + }; + + protected Timer _timer; + protected Thread _dispatchThread; + protected Queue _evqueue = new Queue(); + protected long _lastTick; + protected float _frameTime; + + protected String _api; + protected DisplaySystem _display; + protected Camera _camera; + protected CameraHandler _camhand; + + protected InputHandler _input; + protected BRootNode _rnode; + + protected long _ticksPerFrame; + protected int _targetFPS; + protected boolean _updateEnabled = true, _renderEnabled = true; + protected boolean _finished; + protected int _failures; + + protected Node _root, _geom, _iface; + protected LightState _lights; + protected StatsDisplay _stats; + + /** If we fail 100 frames in a row, stick a fork in ourselves. */ + protected static final int MAX_SUCCESSIVE_FAILURES = 100; +} diff --git a/src/java/com/threerings/jme/JmeCanvasApp.java b/src/java/com/threerings/jme/JmeCanvasApp.java new file mode 100644 index 00000000..3e7faf58 --- /dev/null +++ b/src/java/com/threerings/jme/JmeCanvasApp.java @@ -0,0 +1,184 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme; + +import java.awt.Canvas; +import java.awt.Graphics; +import java.awt.EventQueue; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; + +import org.lwjgl.LWJGLException; + +import com.jme.renderer.Renderer; +import com.jme.renderer.lwjgl.LWJGLRenderer; +import com.jme.scene.Node; +import com.jme.system.DisplaySystem; +import com.jmex.awt.JMECanvas; +import com.jmex.awt.JMECanvasImplementor; +import com.jmex.awt.lwjgl.LWJGLCanvas; + +import com.jmex.bui.CanvasRootNode; + +/** + * Extends the basic {@link JmeApp} with the necessary wiring to use the + * GL/AWT bridge to display our GL interface in an AWT component. + */ +public class JmeCanvasApp extends JmeApp +{ + public JmeCanvasApp (int width, int height) + { + _display = DisplaySystem.getDisplaySystem("LWJGL"); + + // create a throwaway canvas so that the display system knows it's + // created, then create our custom canvas + _display.createCanvas(width, height); + try { + _canvas = createCanvas(); + } catch (LWJGLException e) { + Log.warning("Failed to create LWJGL canvas [error=" + e + "]."); + } + ((JMECanvas)_canvas).setImplementor(_winimp); + _canvas.setBounds(0, 0, width, height); + _canvas.addComponentListener(new ComponentAdapter() { + public void componentResized (ComponentEvent ce) { + _winimp.resizeCanvas(_canvas.getWidth(), _canvas.getHeight()); + } + }); + } + + /** + * Returns the AWT canvas that contains our GL display. + */ + public Canvas getCanvas () + { + return _canvas; + } + + public void run () + { + Thread t = new Thread() { + public void run () { + while (!_finished) { + // queue up another repaint + _canvas.repaint(); + try { + Thread.sleep(10); + } catch (Exception e) { + } + } + } + }; + t.setDaemon(true); + t.start(); + } + + // documentation inherited from interface RunQueue + public void postRunnable (Runnable r) + { + EventQueue.invokeLater(r); + } + + // documentation inherited from interface RunQueue + public boolean isDispatchThread () + { + return EventQueue.isDispatchThread(); + } + + /** + * Creates and returns the LWJGL canvas instance. + */ + protected Canvas createCanvas () + throws LWJGLException + { + // LWJGL's canvas releases the context after painting. we make it + // current again, because we want it valid when we process events. + return new LWJGLCanvas() { + public void update (Graphics g) { + super.update(g); + try { + makeCurrent(); + } catch (LWJGLException e) { + Log.warning("Failed to make context current [error=" + + e + "]."); + } + } + }; + } + + // documentation inherited + protected DisplaySystem createDisplay () + { + // we've already created our display earlier so just return it + _api = "LWJGL"; + return _display; + } + + // documentation inherited + protected void initInterface () + { + _iface = new Node("Interface"); + _root.attachChild(_iface); + + _rnode = new CanvasRootNode(_canvas); + _iface.attachChild(_rnode); + } + + /** This is used if we embed our GL display in an AWT component. */ + protected JMECanvasImplementor _winimp = new JMECanvasImplementor() { + public void doSetup () { + super.doSetup(); + + LWJGLRenderer renderer = + new LWJGLRenderer(_canvas.getWidth(), _canvas.getHeight()); + renderer.setHeadless(true); + setRenderer(renderer); + _display.setRenderer(renderer); + DisplaySystem.updateStates(renderer); + + if (!init()) { + Log.warning("JmeCanvasApp init failed."); + } + } + + public void doUpdate () { + } + + public void doRender () { + // here we do our normal frame processing + try { + processFrame(); + // we don't process events as the AWT queue handles them + _failures = 0; + } catch (Throwable t) { + Log.logStackTrace(t); + // stick a fork in things if we fail too many + // times in a row + if (++_failures > MAX_SUCCESSIVE_FAILURES) { + JmeCanvasApp.this.stop(); + } + } + } + }; + + protected Canvas _canvas; +} diff --git a/src/java/com/threerings/jme/JmeContext.java b/src/java/com/threerings/jme/JmeContext.java new file mode 100644 index 00000000..6dcc4c32 --- /dev/null +++ b/src/java/com/threerings/jme/JmeContext.java @@ -0,0 +1,59 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme; + +import com.jme.input.InputHandler; +import com.jme.renderer.Renderer; +import com.jme.scene.Node; +import com.jme.system.DisplaySystem; + +import com.jmex.bui.BRootNode; + +import com.threerings.jme.camera.CameraHandler; + +/** + * Provides access to the various bits needed by things that operate in + * JME land. + */ +public interface JmeContext +{ + /** Returns the display to which we are rendering. */ + public DisplaySystem getDisplay (); + + /** Returns the renderer being used to draw everything. */ + public Renderer getRenderer (); + + /** Returns the handler for the camera being used to view the scene. */ + public CameraHandler getCameraHandler (); + + /** Returns the main geometry of our scene graph. */ + public Node getGeometry (); + + /** Returns the main interface node of our scene graph. */ + public Node getInterface (); + + /** Returns our main input handler. */ + public InputHandler getInputHandler (); + + /** Returns our UI root node. */ + public BRootNode getRootNode (); +} diff --git a/src/java/com/threerings/jme/Log.java b/src/java/com/threerings/jme/Log.java new file mode 100644 index 00000000..da1cee03 --- /dev/null +++ b/src/java/com/threerings/jme/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme; + +/** + * A placeholder class that contains a reference to the log object used by + * this package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("narya.jme"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/jme/StatsDisplay.java b/src/java/com/threerings/jme/StatsDisplay.java new file mode 100644 index 00000000..c03d3ff8 --- /dev/null +++ b/src/java/com/threerings/jme/StatsDisplay.java @@ -0,0 +1,83 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme; + +import com.jme.image.Texture; +import com.jme.renderer.Renderer; +import com.jme.scene.Node; +import com.jme.scene.Text; +import com.jme.scene.state.AlphaState; +import com.jme.scene.state.TextureState; +import com.jme.util.TextureManager; +import com.jme.util.Timer; + +/** + * Tracks and displays render statistics. + */ +public class StatsDisplay extends Node +{ + public StatsDisplay (Renderer renderer) + { + super("StatsNode"); + + // create an alpha state that will allow us to blend the text on + // top of whatever else is below + AlphaState astate = renderer.createAlphaState(); + astate.setBlendEnabled(true); + astate.setSrcFunction(AlphaState.SB_SRC_ALPHA); + astate.setDstFunction(AlphaState.DB_ONE); + astate.setTestEnabled(true); + astate.setTestFunction(AlphaState.TF_GREATER); + astate.setEnabled(true); + + // create a font texture + TextureState font = renderer.createTextureState(); + font.setTexture( + TextureManager.loadTexture( + getClass().getClassLoader().getResource(DEFAULT_JME_FONT), + Texture.MM_LINEAR, Texture.FM_LINEAR)); + font.setEnabled(true); + + _text = new Text("StatsLabel", ""); + _text.setCullMode(CULL_NEVER); + _text.setTextureCombineMode(TextureState.REPLACE); + + attachChild(_text); + setRenderState(font); + setRenderState(astate); + } + + public void update (Timer timer, Renderer renderer) + { + _stats.setLength(0); + _stats.append("FPS: ").append((int)timer.getFrameRate()); + _stats.append(" - ").append(renderer.getStatistics(_temp)); + _text.print(_stats); + } + + protected Text _text; + protected StringBuffer _stats = new StringBuffer(); + protected StringBuffer _temp = new StringBuffer(); + + protected static final String DEFAULT_JME_FONT = + "rsrc/media/jme/defaultfont.tga"; +} diff --git a/src/java/com/threerings/jme/camera/CameraHandler.java b/src/java/com/threerings/jme/camera/CameraHandler.java new file mode 100644 index 00000000..bdb362d8 --- /dev/null +++ b/src/java/com/threerings/jme/camera/CameraHandler.java @@ -0,0 +1,412 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.camera; + +import com.jme.math.FastMath; +import com.jme.math.Matrix3f; +import com.jme.math.Plane; +import com.jme.math.Vector3f; +import com.jme.renderer.Camera; + +import com.samskivert.util.ObserverList; + +/** + * Provides various useful mechanisms for manipulating the camera. + */ +public class CameraHandler +{ + /** + * Creates a new camera handler. The camera begins life at the origin, + * facing in the negative z direction (pointing at the ground). + */ + public CameraHandler (Camera camera) + { + _camera = camera; + resetAxes(); + } + + /** + * Resets the camera orientation to its initial state. + */ + public void resetAxes () + { + _camera.getDirection().set(0, 0, -1); + _camera.getLeft().set(-1, 0, 0); + _camera.getUp().set(0, 1, 0); + _camera.update(); + + _rxdir.set(1, 0, 0); + _rydir.set(0, 1, 0); + } + + /** + * Configures limits on the camera tilt. + */ + public void setTiltLimits (float minAngle, float maxAngle) + { + _minTilt = minAngle; + _maxTilt = maxAngle; + } + + /** + * Configures limits on the distance the camera can be panned. + * + * @param boundViaFrustum if true instead of bounding the camera's + * position, we will compute the intersections of the view frustum with the + * ground plan and bound that rectangle into the specified bounds. + * Note: the camera must generally be pointing down at the ground + * (up to perhaps 45 degrees or so) for this to work. At higher angles the + * back of the view frustum will intersect the ground plane at or near + * infinity. + */ + public void setPanLimits (float minX, float minY, float maxX, float maxY, + boolean boundViaFrustum) + { + _minX = minX; + _minY = minY; + _maxX = maxX; + _maxY = maxY; + _boundViaFrustum = boundViaFrustum; + } + + /** + * Configures the minimum and maximum zoom values allowed for the + * camera. + */ + public void setZoomLimits (float minZoom, float maxZoom) + { + _minZoom = minZoom; + _maxZoom = maxZoom; + } + + /** + * Enables or disables the current set of limits. + */ + public void setLimitsEnabled (boolean enabled) + { + _limitsEnabled = enabled; + } + + /** + * Sets the camera zoom level to a value between zero (zoomed in maximally) + * and 1 (zoomed out maximally). Zoom limits must have already been set up + * via a call to {@link #setZoomLimits}. + */ + public void setZoomLevel (float level) + { +// Log.info("Zoom " + level + " " + _camera.getLocation()); + level = Math.max(0f, Math.min(level, 1f)); + _camera.getLocation().z = _minZ + (_maxZ - _minZ) * level; + _camera.update(); + } + + /** + * Returns the current camera zoom level. + */ + public float getZoomLevel () + { + return (_camera.getLocation().z - _minZ) / (_maxZ - _minZ); + } + + /** + * Adds a camera path observer. + */ + public void addCameraObserver (CameraPath.Observer camobs) + { + _campathobs.add(camobs); + } + + /** + * Removes a camera path observer. + */ + public void removeCameraObserver (CameraPath.Observer camobs) + { + _campathobs.remove(camobs); + } + + /** + * Starts the camera moving along a path which will be updated every tick + * until it is complete. + */ + public void moveCamera (CameraPath path) + { + if (_campath != null) { + _campath.abort(); + _campathobs.apply(new CompletedOp(_campath)); + } + _campath = path; + } + + /** + * Returns true if the camera is currently animating along a path, false if + * it is not. + */ + public boolean cameraIsMoving () + { + return (_campath != null); + } + + /** + * Skips immediately to the end of the current camera path. + */ + public void skipPath () + { + // fake an update far enough into the future to trick the camera path + // into thinking it's done + update(100); + } + + /** + * This is called by the {@link JmeApp} on every frame to allow the handler + * to update the camera as necessary. + */ + public void update (float frameTime) + { + if (_campath != null) { + if (_campath.tick(frameTime)) { + CameraPath opath = _campath; + _campath = null; + _campathobs.apply(new CompletedOp(opath)); + } + } + } + + /** + * Returns the camera being manipulated by this handler. + */ + public Camera getCamera () + { + return _camera; + } + + /** + * Adjusts the camera's location. The specified location will be bounded + * within the current pan and zoom limits. + */ + public void setLocation (Vector3f location) + { + _camera.setLocation(bound(location)); + _camera.update(); + } + + /** + * Pans the camera the specified distance in the x and y directions. These + * distances will be multiplied by the current "view" x and y axes and + * added to the camera's position. + */ + public void panCamera (float x, float y) + { + Vector3f loc = _camera.getLocation(); + loc.addLocal(_rxdir.mult(x, _temp)); + loc.addLocal(_rydir.mult(y, _temp)); + setLocation(loc); + } + + /** + * Zooms the camera in (distance < 0) and out (distance > 0) by the + * specified amount. The distance is multiplied by the camera's current + * direction and added to its current position, which is then bounded into + * the pan and zoom volume. + */ + public void zoomCamera (float distance) + { + Vector3f loc = _camera.getLocation(); + float dist = getGroundPoint().distance(loc), + ndist = Math.min(Math.max(dist + distance, _minZoom), _maxZoom); + if ((distance = ndist - dist) == 0f) { + return; + } + loc.subtractLocal(_camera.getDirection().mult(distance, _temp)); + setLocation(loc); + } + + /** + * Locates the point on the ground at which the camera is "looking" and + * rotates the camera from that point around the specified vector by the + * specified angle. Additionally zooms the camera in (deltaZoom < 0) or out + * (deltaZoom > 0) along its direction of view by the specified amount. + */ + public void rotateCamera ( + Vector3f spot, Vector3f axis, float deltaAngle, float deltaZoom) + { + // get a vector from the camera's current position to the point around + // which we're going to orbit + Vector3f direction = _camera.getLocation().subtract(spot); + + // if we're rotating around the left vector, impose tilt limits + if (axis == _camera.getLeft()) { + float angle = FastMath.asin(_ground.normal.dot(direction) / + direction.length()); + float nangle = Math.min(Math.max(angle + deltaAngle, _minTilt), + _maxTilt); + if ((deltaAngle = nangle - angle) == 0f) { + return; + } + } + + // create a rotation matrix + _rotm.fromAxisAngle(axis, deltaAngle); + + // rotate the direction vector and the camera itself + _rotm.mult(direction, direction); + _rotm.mult(_camera.getUp(), _camera.getUp()); + _rotm.mult(_camera.getLeft(), _camera.getLeft()); + _rotm.mult(_camera.getDirection(), _camera.getDirection()); + + // if we're rotating around the ground normal, we need to update our + // notion of side-to-side and forward for panning + if (axis == _ground.normal) { + _rotm.mult(_rxdir, _rxdir); + _rotm.mult(_rydir, _rydir); + } + + // finally move the camera to its new location, zooming in or out in + // the process + float scale = 1 + (deltaZoom / direction.length()); + direction.scaleAdd(scale, spot); + setLocation(direction); + } + + /** + * Swings the camera perpendicular to the ground normal, around the point + * on the ground at which it is looking. + */ + public void orbitCamera (float deltaAngle) + { + rotateCamera(getGroundPoint(), _ground.normal, deltaAngle, 0); + } + + /** + * Swings the camera perpendicular to its left vector around the point on + * the ground at which it is looking. + */ + public void tiltCamera (float deltaAngle) + { + rotateCamera(getGroundPoint(), _camera.getLeft(), deltaAngle, 0); + } + + /** + * Returns the point on the ground (z = 0) at which the camera is looking. + */ + public Vector3f getGroundPoint () + { + float dist = -1f * _ground.normal.dot(_camera.getLocation()) / + _ground.normal.dot(_camera.getDirection()); + return _camera.getLocation().add(_camera.getDirection().mult(dist)); + } + + /** + * Returns the ground normal. Z is the default ground plane and the normal + * points in the positive z direction. Do not modify this value. + */ + public Vector3f getGroundNormal () + { + return _ground.normal; + } + + protected Vector3f bound (Vector3f loc) + { + if (!_limitsEnabled) { + return loc; + } + if (_boundViaFrustum) { + bound(_camera.getFrustumLeft(), _camera.getFrustumTop(), loc); + bound(_camera.getFrustumLeft(), _camera.getFrustumBottom(), loc); + bound(_camera.getFrustumRight(), _camera.getFrustumTop(), loc); + bound(_camera.getFrustumRight(), _camera.getFrustumBottom(), loc); + } else { + loc.x = Math.max(Math.min(loc.x, _maxX), _minX); + loc.y = Math.max(Math.min(loc.y, _maxY), _minY); + } + loc.z = Math.max(Math.min(loc.z, _maxZ), _minZ); + return loc; + } + + protected void bound (float left, float up, Vector3f loc) + { + // start with the location of the camera moved out into the near + // frustum plane + _temp.set(loc); + _temp.scaleAdd(_camera.getFrustumNear(), _camera.getDirection(), loc); + + // then slide it over to a corner of the near frustum rectangle + _temp.scaleAdd(left, _camera.getLeft(), _temp); + _temp.scaleAdd(up, _camera.getUp(), _temp); + + // turn this into a vector with origin at the camera's location + _temp.subtractLocal(loc); + _temp.normalizeLocal(); + + // determine the intersection of said vector with the ground plane + float dist = -1f * _ground.normal.dot(loc) / _ground.normal.dot(_temp); + _temp.scaleAdd(dist, _temp, loc); + + // we then assume that if the corner of the "viewable ground rectangle" + // is outside our bounds that we can simply adjust the camera location + // by the amount it is out of bounds + if (_temp.x > _maxX) { + loc.x -= (_temp.x - _maxX); + } else if (_temp.x < _minX) { + loc.x += (_minX - _temp.x); + } + if (_temp.y > _maxY) { + loc.y -= (_temp.y - _maxY); + } else if (_temp.y < _minY) { + loc.y += (_minY - _temp.y); + } + } + + /** Used to dispatch {@link CameraPath.Observer#pathCompleted}. */ + protected static class CompletedOp implements ObserverList.ObserverOp + { + public CompletedOp (CameraPath path) { + _path = path; + } + public boolean apply (Object observer) { + return ((CameraPath.Observer)observer).pathCompleted(_path); + } + protected CameraPath _path; + } + + protected Camera _camera; + protected CameraPath _campath; + protected ObserverList _campathobs = + new ObserverList(ObserverList.SAFE_IN_ORDER_NOTIFY); + + protected Matrix3f _rotm = new Matrix3f(); + protected Vector3f _temp = new Vector3f(); + + protected boolean _boundViaFrustum, _limitsEnabled = true; + protected float _minX = -Float.MAX_VALUE, _maxX = Float.MAX_VALUE; + protected float _minY = -Float.MAX_VALUE, _maxY = Float.MAX_VALUE; + protected float _minZ = -Float.MAX_VALUE, _maxZ = Float.MAX_VALUE; + protected float _minZoom = 0f, _maxZoom = Float.MAX_VALUE; + protected float _minTilt = -Float.MAX_VALUE, _maxTilt = Float.MAX_VALUE; + + protected Vector3f _rxdir = new Vector3f(1, 0, 0); + protected Vector3f _rydir = new Vector3f(0, 1, 0); + + protected static final Vector3f _xdir = new Vector3f(1, 0, 0); + protected static final Vector3f _ydir = new Vector3f(0, 1, 0); + + protected static final Plane _ground = new Plane(new Vector3f(0, 0, 1), 0); +} diff --git a/src/java/com/threerings/jme/camera/CameraPath.java b/src/java/com/threerings/jme/camera/CameraPath.java new file mode 100644 index 00000000..3837b603 --- /dev/null +++ b/src/java/com/threerings/jme/camera/CameraPath.java @@ -0,0 +1,69 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.camera; + +import com.jme.renderer.Camera; + +/** + * Used to move the camera along a particular path. + */ +public abstract class CameraPath +{ + /** Used to inform observers when a camera path is completed or aborted. */ + public interface Observer + { + /** + * Called when this path is finished (potentially early because another + * path was set before this path completed). + * + * @param path the path that was completed. + * + * @return true if the observer should remain in the list, false if it + * should be removed following this notification. + */ + public boolean pathCompleted (CameraPath path); + } + + /** + * This is called on every frame to allow the path to adjust the position + * of the camera. + * + * @return true if the path is completed and can be disposed, false if it + * is not yet completed. + */ + public abstract boolean tick (float secondsSince); + + /** + * Called if this path is aborted prior to completion due to being replaced + * by a new camera path. + */ + public void abort () + { + } + + protected CameraPath (CameraHandler camhand) + { + _camhand = camhand; + } + + protected CameraHandler _camhand; +} diff --git a/src/java/com/threerings/jme/camera/GodViewHandler.java b/src/java/com/threerings/jme/camera/GodViewHandler.java new file mode 100644 index 00000000..68c4ec2c --- /dev/null +++ b/src/java/com/threerings/jme/camera/GodViewHandler.java @@ -0,0 +1,191 @@ +// +// $Id$ + +package com.threerings.jme.camera; + +import com.jme.math.FastMath; +import com.jme.math.Matrix3f; +import com.jme.math.Vector3f; +import com.jme.renderer.Camera; + +import com.jme.input.InputHandler; +import com.jme.input.KeyBindingManager; +import com.jme.input.KeyInput; +import com.jme.input.RelativeMouse; +import com.jme.input.action.*; +import com.jme.input.action.InputActionEvent; + +import com.threerings.jme.JmeApp; +import com.threerings.jme.Log; + +/** + * Sets up camera controls for moving around from a top-down perspective, + * suitable for strategy games and their ilk. The "ground" is assumed to be the + * XY plane. + */ +public class GodViewHandler extends InputHandler +{ + /** + * Creates the handler. + * + * @param cam The camera to move with this handler. + */ + public GodViewHandler (CameraHandler camhand) + { + _camhand = camhand; + setKeyBindings(); + addActions(); + } + + protected void setKeyBindings () + { + KeyBindingManager keyboard = KeyBindingManager.getKeyBindingManager(); + + // the key bindings for the pan actions + keyboard.set("forward", KeyInput.KEY_W); + keyboard.set("arrow_forward", KeyInput.KEY_UP); + keyboard.set("backward", KeyInput.KEY_S); + keyboard.set("arrow_backward", KeyInput.KEY_DOWN); + keyboard.set("left", KeyInput.KEY_A); + keyboard.set("arrow_left", KeyInput.KEY_LEFT); + keyboard.set("right", KeyInput.KEY_D); + keyboard.set("arrow_right", KeyInput.KEY_RIGHT); + + // the key bindings for the zoom actions + keyboard.set("zoomIn", KeyInput.KEY_UP); + keyboard.set("zoomOut", KeyInput.KEY_DOWN); + + // the key bindings for the orbit actions + keyboard.set("turnRight", KeyInput.KEY_RIGHT); + keyboard.set("turnLeft", KeyInput.KEY_LEFT); + + // the key bindings for the tilt actions + keyboard.set("tiltForward", KeyInput.KEY_HOME); + keyboard.set("tiltBack", KeyInput.KEY_END); + + keyboard.set("screenshot", KeyInput.KEY_F12); + } + + protected void addActions () + { + addAction(new KeyScreenShotAction(), "screenshot", false); + addPanActions(); + addZoomActions(); + addOrbitActions(); + addTiltActions(); + } + + /** + * Adds actions for panning the camera around the scene. + */ + protected void addPanActions () + { + InputAction forward = new InputAction() { + public void performAction (InputActionEvent evt) { + _camhand.panCamera(0, speed * evt.getTime()); + } + }; + forward.setSpeed(0.5f); + addAction(forward, "forward", true); + addAction(forward, "arrow_forward", true); + + InputAction backward = new InputAction() { + public void performAction (InputActionEvent evt) { + _camhand.panCamera(0, -speed * evt.getTime()); + } + }; + backward.setSpeed(0.5f); + addAction(backward, "backward", true); + addAction(backward, "arrow_backward", true); + + InputAction left = new InputAction() { + public void performAction (InputActionEvent evt) { + _camhand.panCamera(-speed * evt.getTime(), 0); + } + }; + left.setSpeed(0.5f); + addAction(left, "left", true); + addAction(left, "arrow_left", true); + + InputAction right = new InputAction() { + public void performAction (InputActionEvent evt) { + _camhand.panCamera(speed * evt.getTime(), 0); + } + }; + right.setSpeed(0.5f); + addAction(right, "right", true); + addAction(right, "arrow_right", true); + } + + /** + * Adds actions for zooming the camaera in and out. + */ + protected void addZoomActions () + { + InputAction zoomIn = new InputAction() { + public void performAction (InputActionEvent evt) { + _camhand.zoomCamera(-speed * evt.getTime()); + } + }; + zoomIn.setSpeed(0.5f); + addAction(zoomIn, "zoomIn", true); + + InputAction zoomOut = new InputAction() { + public void performAction (InputActionEvent evt) { + _camhand.zoomCamera(speed * evt.getTime()); + } + }; + zoomOut.setSpeed(0.5f); + addAction(zoomOut, "zoomOut", true); + } + + /** + * Adds actions for orbiting the camera around the viewpoint. + */ + protected void addOrbitActions () + { + addAction(new OrbitAction(-FastMath.PI / 2), "turnRight", true); + addAction(new OrbitAction(FastMath.PI / 2), "turnLeft", true); + } + + /** + * Adds actions for tilting the camera (rotating around the yaw axis). + */ + protected void addTiltActions () + { + addAction(new TiltAction(-FastMath.PI / 2), "tiltForward", true); + addAction(new TiltAction(FastMath.PI / 2), "tiltBack", true); + } + + protected class OrbitAction extends InputAction + { + public OrbitAction (float radPerSec) + { + _radPerSec = radPerSec; + } + + public void performAction (InputActionEvent evt) + { + _camhand.orbitCamera(_radPerSec * evt.getTime()); + } + + protected float _radPerSec; + } + + protected class TiltAction extends InputAction + { + public TiltAction (float radPerSec) + { + _radPerSec = radPerSec; + } + + public void performAction (InputActionEvent evt) + { + _camhand.tiltCamera(_radPerSec * evt.getTime()); + } + + protected float _radPerSec; + } + + protected CameraHandler _camhand; +} diff --git a/src/java/com/threerings/jme/camera/PanPath.java b/src/java/com/threerings/jme/camera/PanPath.java new file mode 100644 index 00000000..ac3fd9df --- /dev/null +++ b/src/java/com/threerings/jme/camera/PanPath.java @@ -0,0 +1,87 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.camera; + +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; +import com.jme.renderer.Camera; + +/** + * Pans the camera to the specified location in the specified amount of time. + */ +public class PanPath extends CameraPath +{ + /** + * Creates a panning path for the specified camera that will retain the + * camera's current orientation. + * + * @param target the target position for the camera. + * @param duration the number of seconds in which to pan the camera. + */ + public PanPath (CameraHandler camhand, Vector3f target, float duration) + { + this(camhand, target, null, duration); + } + + /** + * Creates a panning path for the specified camera. + * + * @param target the target position for the camera. + * @param trot the target rotation for the camera (or null to + * keep its current orientation) + * @param duration the number of seconds in which to pan the camera. + */ + public PanPath (CameraHandler camhand, Vector3f target, Quaternion trot, + float duration) + { + super(camhand); + + Camera cam = camhand.getCamera(); + _start = new Vector3f(cam.getLocation()); + _velocity = target.subtract(_start); + _velocity.divideLocal(duration); + if (trot != null) { + _irot = new Quaternion(); + _irot.fromAxes(cam.getLeft(), cam.getUp(), cam.getDirection()); + _trot = trot; + } + + _duration = duration; + } + + // documentation inherited + public boolean tick (float secondsSince) + { + _elapsed = Math.min(_elapsed + secondsSince, _duration); + _camloc.scaleAdd(_elapsed, _velocity, _start); + _camhand.setLocation(_camloc); + if (_irot != null) { + _camhand.getCamera().setAxes(_axes.slerp(_irot, _trot, + _elapsed / _duration)); + } + return (_elapsed >= _duration); + } + + protected Vector3f _start, _velocity, _camloc = new Vector3f(); + protected Quaternion _irot, _trot, _axes = new Quaternion(); + protected float _elapsed, _duration; +} diff --git a/src/java/com/threerings/jme/camera/SplinePath.java b/src/java/com/threerings/jme/camera/SplinePath.java new file mode 100644 index 00000000..b29697cb --- /dev/null +++ b/src/java/com/threerings/jme/camera/SplinePath.java @@ -0,0 +1,113 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.camera; + +import com.jme.math.Vector3f; +import com.jme.renderer.Camera; + +import com.threerings.jme.Log; + +/** + * Moves the camera along a cubic Hermite spline path defined by the start and + * end locations and directions. Spline formulas obtained from + * Wikipedia. + */ +public class SplinePath extends CameraPath +{ + /** + * Creates a cubic spline path for the camera to follow. + * + * @param tloc the target location + * @param tdir the target direction + * @param axis the heading axis (typically {@link Vector3f#UNIT_Z}) + * @param duration the duration of the path + * @param tension the tension parameter, which can range from zero to one: + * higher tension values create a more direct path with a sharper turn, + * lower values create a rounder path with a smoother turn + */ + public SplinePath (CameraHandler camhand, Vector3f tloc, + Vector3f tdir, Vector3f axis, float duration, float tension) + { + super(camhand); + + // get the spline function coefficients + Camera cam = camhand.getCamera(); + _p0 = new Vector3f(cam.getLocation()); + _p1 = new Vector3f(tloc); + float tscale = (1f - tension) * _p0.distance(_p1); + _m0 = cam.getDirection().mult(tscale); + _m1 = tdir.mult(tscale); + + _axis = axis; + _duration = duration; + } + + // documentation inherited + public boolean tick (float secondsSince) + { + _elapsed = Math.min(_elapsed + secondsSince, _duration); + float t = _elapsed / _duration, t2 = t*t, t3 = t2*t, + h00 = 2*t3 - 3*t2 + 1, + h00p = 6*t2 - 6*t, + h10 = t3 - 2*t2 + t, + h10p = 3*t2 - 4*t + 1, + h01 = -2*t3 + 3*t2, + h01p = -6*t2 + 6*t, + h11 = t3 - t2, + h11p = 3*t2 - 2*t; + + // take the derivative to find the direction + _p0.mult(h00p, _dir); + _dir.scaleAdd(h10p, _m0, _dir); + _dir.scaleAdd(h01p, _p1, _dir); + _dir.scaleAdd(h11p, _m1, _dir); + _dir.normalizeLocal(); + + // evaluate the spline function to find the location + _p0.mult(h00, _loc); + _loc.scaleAdd(h10, _m0, _loc); + _loc.scaleAdd(h01, _p1, _loc); + _loc.scaleAdd(h11, _m1, _loc); + + // compute the left and up vectors using the direction and + // axis vectors + Camera cam = _camhand.getCamera(); + _axis.cross(_dir, _left); + _left.normalizeLocal(); + _dir.cross(_left, _up); + + // update the camera's location and orientation + cam.setFrame(_loc, _left, _up, _dir); + + return _elapsed >= _duration; + } + + /** The parameters of the spline. */ + Vector3f _p0, _m0, _p1, _m1, _axis; + + /** Working vectors. */ + Vector3f _loc = new Vector3f(), _left = new Vector3f(), + _up = new Vector3f(), _dir = new Vector3f(); + + /** The elapsed time and total duration. */ + float _elapsed, _duration; +} diff --git a/src/java/com/threerings/jme/camera/SwingPath.java b/src/java/com/threerings/jme/camera/SwingPath.java new file mode 100644 index 00000000..16836136 --- /dev/null +++ b/src/java/com/threerings/jme/camera/SwingPath.java @@ -0,0 +1,168 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.camera; + +import com.jme.math.FastMath; +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; +import com.jme.renderer.Camera; + +import com.threerings.jme.Log; + +/** + * Swings the camera around a point of interest (which should be somewhere + * along the camera's view vector). Also optionally pans the camera and/or + * zooms it in or out (moves it along its view vector) in the process. + * + *

+ */ +public class SwingPath extends CameraPath +{ + /** + * Creates a rotating, zooming path for the specified camera. + * + * @param spot the point of interest around which to swing the camera. + * @param axis the axis around which to rotate the camera. + * @param angle the angle through which to rotate the camera. + * @param angvel the (absolute value of the) velocity at which to rotate + * the camera (in radians per second). + * @param zoom the distance to zoom along the camera's view axis (negative + * = in, positive = out). + */ + public SwingPath (CameraHandler camhand, Vector3f spot, Vector3f axis, + float angle, float angvel, float zoom) + { + this(camhand, spot, axis, angle, angvel, null, 0f, null, zoom); + } + + /** + * Creates a rotating, panning, zooming path for the specified camera. + * + * @param spot the point of interest around which to swing the camera. + * @param paxis the primary axis around which to rotate the camera. + * @param pangle the angle through which to rotate the camera about the + * primary axis. + * @param angvel the (absolute value of the) velocity at which to rotate + * the camera (in radians per second) about the primary angle. + * @param saxis the secondary axis around which to rotate the camera, or + * null for none + * @param sangle the angle through which to rotate the camera about the + * secondary axis + * @param pan the amount to pan the camera, or null for none + * @param zoom the distance to zoom along the camera's view axis (negative + * = in, positive = out). + */ + public SwingPath (CameraHandler camhand, Vector3f spot, Vector3f paxis, + float pangle, float angvel, Vector3f saxis, float sangle, + Vector3f pan, float zoom) + { + super(camhand); + + if (pangle == 0) { + Log.warning("Requested to swing camera through zero degrees " + + "[spot=" + spot + ", paxis=" + paxis + + ", angvel=" + angvel + ", zoom=" + zoom + "]."); + pangle = 0.0001f; + } + if (angvel <= 0) { + Log.warning("Requested to swing camera with invalid velocity " + + "[spot=" + spot + ", paxis=" + paxis + ", pangle=" + + pangle + ", angvel=" + angvel + ", zoom=" + zoom + + "]."); + angvel = FastMath.PI; + } + + _spot = new Vector3f(spot); + _paxis = paxis; + _pangle = pangle; + _pangvel = (pangle > 0) ? angvel : -1 * angvel; + _saxis = (saxis == null) ? null : new Vector3f(saxis); + _sangle = sangle; + _sangvel = _sangle * _pangvel / _pangle; + _pan = pan; + if (_pan != null) { + _panvel = _pan.mult(_pangvel / _pangle); + _panned = new Vector3f(); + } + _zoom = zoom; + _zoomvel = _zoom * _pangvel / _pangle; + +// Log.info("Swinging camera [angle=" + _angle + ", angvel=" + _angvel + +// ", zoom=" + _zoom + ", zoomvel=" + _zoomvel + "]."); + } + + // documentation inherited + public boolean tick (float secondsSince) + { + float deltaPAngle = (secondsSince * _pangvel); + float deltaSAngle = (secondsSince * _sangvel); + float deltaZoom = (secondsSince * _zoomvel); + _protated += deltaPAngle; + _srotated += deltaSAngle; + _zoomed += deltaZoom; + if (_pan != null) { + _panvel.mult(secondsSince, _deltaPan); + _panned.addLocal(_deltaPan); + } + + // clamp our rotation at the target angle and determine whether or not + // we're done + boolean done = false; + if (_pangle > 0 && _protated > _pangle || + _pangle < 0 && _protated < _pangle) { + deltaPAngle -= (_protated - _pangle); + deltaSAngle -= (_srotated - _sangle); + deltaZoom -= (_zoomed - _zoom); + if (_pan != null) { + _deltaPan.subtractLocal(_panned).addLocal(_pan); + } + _protated = _pangle; + _srotated = _sangle; + _panned = _pan; + _zoomed = _zoom; + done = true; + } + + // have the camera handler do the necessary rotating, panning, zooming + if (_pan != null) { + _camhand.setLocation( + _camhand.getCamera().getLocation().addLocal(_deltaPan)); + _spot.addLocal(_deltaPan); + } + _camhand.rotateCamera(_spot, _paxis, deltaPAngle, deltaZoom); + if (_saxis != null) { + _rot.fromAngleAxis(deltaPAngle, _paxis).multLocal(_saxis); + _camhand.rotateCamera(_spot, _saxis, deltaSAngle, 0f); + } + + return done; + } + + protected Vector3f _spot, _paxis, _saxis; + protected float _pangle, _pangvel, _protated; + protected float _sangle, _sangvel, _srotated; + protected Vector3f _pan, _panvel, _panned; + protected float _zoom, _zoomvel, _zoomed; + + protected Quaternion _rot = new Quaternion(); + protected Vector3f _deltaPan = new Vector3f(); +} diff --git a/src/java/com/threerings/jme/camera/rotate_zoom.dia b/src/java/com/threerings/jme/camera/rotate_zoom.dia new file mode 100644 index 00000000..603bd5cc Binary files /dev/null and b/src/java/com/threerings/jme/camera/rotate_zoom.dia differ diff --git a/src/java/com/threerings/jme/camera/rotate_zoom.png b/src/java/com/threerings/jme/camera/rotate_zoom.png new file mode 100644 index 00000000..82ce4f88 Binary files /dev/null and b/src/java/com/threerings/jme/camera/rotate_zoom.png differ diff --git a/src/java/com/threerings/jme/chat/ChatView.java b/src/java/com/threerings/jme/chat/ChatView.java new file mode 100644 index 00000000..6a6a4be8 --- /dev/null +++ b/src/java/com/threerings/jme/chat/ChatView.java @@ -0,0 +1,181 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.chat; + +import java.util.StringTokenizer; + +import com.jmex.bui.BContainer; +import com.jmex.bui.BTextArea; +import com.jmex.bui.BTextField; +import com.jmex.bui.event.ActionEvent; +import com.jmex.bui.event.ActionListener; +import com.jmex.bui.layout.BorderLayout; + +import com.jme.renderer.ColorRGBA; + +import com.threerings.util.Name; + +import com.threerings.crowd.chat.client.ChatDirector; +import com.threerings.crowd.chat.client.ChatDisplay; +import com.threerings.crowd.chat.client.SpeakService; +import com.threerings.crowd.chat.data.ChatCodes; +import com.threerings.crowd.chat.data.ChatMessage; +import com.threerings.crowd.chat.data.SystemMessage; +import com.threerings.crowd.chat.data.UserMessage; +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.jme.JmeContext; +import com.threerings.jme.Log; + +/** + * Displays chat messages and allows for their input. + */ +public class ChatView extends BContainer + implements ChatDisplay +{ + public ChatView (JmeContext ctx, ChatDirector chatdtr) + { + _chatdtr = chatdtr; + setLayoutManager(new BorderLayout(2, 2)); + add(_text = new BTextArea(), BorderLayout.CENTER); + add(_input = new BTextField(), BorderLayout.SOUTH); + + _input.addListener(new ActionListener() { + public void actionPerformed (ActionEvent event) { + if (handleInput(_input.getText())) { + _input.setText(""); + } + } + }); + } + + public void willEnterPlace (PlaceObject plobj) + { + setSpeakService(plobj.speakService); + } + + public void didLeavePlace (PlaceObject plobj) + { + clearSpeakService(); + } + + public void setSpeakService (SpeakService spsvc) + { + if (spsvc != null) { + _spsvc = spsvc; + _chatdtr.addChatDisplay(this); + } + } + + public void clearSpeakService () + { + if (_spsvc != null) { + _chatdtr.removeChatDisplay(this); + _spsvc = null; + } + } + + /** + * Instructs our chat input field to request focus. + */ + public void requestFocus () + { + _input.requestFocus(); + } + + // documentation inherited from interface ChatDisplay + public void clear () + { + } + + // documentation inherited from interface ChatDisplay + public void displayMessage (ChatMessage msg) + { + // we may be restricted in the chat types we handle + if (!handlesType(msg.localtype)) { + return; + } + + if (msg instanceof UserMessage) { + UserMessage umsg = (UserMessage) msg; + if (umsg.localtype == ChatCodes.USER_CHAT_TYPE) { + append("[" + umsg.speaker + " whispers] ", ColorRGBA.green); + append(umsg.message + "\n"); + } else { + append("<" + umsg.speaker + "> ", ColorRGBA.blue); + append(umsg.message + "\n"); + } + + } else if (msg instanceof SystemMessage) { + append(msg.message + "\n", ColorRGBA.red); + + } else { + Log.warning("Received unknown message type: " + msg + "."); + } + } + + // documentation inherited + public void setEnabled (boolean enabled) + { + _input.setEnabled(enabled); + } + + protected void displayError (String message) + { + append(message + "\n", ColorRGBA.red); + } + + protected void append (String text, ColorRGBA color) + { + _text.appendText(text, color); + } + + protected void append (String text) + { + _text.appendText(text); + } + + protected boolean handlesType (String localType) + { + return true; + } + + protected boolean handleInput (String text) + { + if (text.length() == 0) { + // no empty banter + return false; + } + String msg = _chatdtr.requestChat(_spsvc, text, true); + if (msg.equals(ChatCodes.SUCCESS)) { + return true; + } else { + _chatdtr.displayFeedback(null, msg); + return false; + } + } + + protected ChatDirector _chatdtr; + protected SpeakService _spsvc; + protected BTextArea _text; + protected BTextField _input; +} diff --git a/src/java/com/threerings/jme/effect/FadeInOutEffect.java b/src/java/com/threerings/jme/effect/FadeInOutEffect.java new file mode 100644 index 00000000..9898dd65 --- /dev/null +++ b/src/java/com/threerings/jme/effect/FadeInOutEffect.java @@ -0,0 +1,148 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.effect; + +import com.jme.math.Vector3f; +import com.jme.renderer.ColorRGBA; +import com.jme.renderer.Renderer; +import com.jme.scene.Geometry; +import com.jme.scene.Node; +import com.jme.scene.shape.Quad; +import com.jme.scene.state.AlphaState; +import com.jme.system.DisplaySystem; + +import com.threerings.jme.util.LinearTimeFunction; +import com.threerings.jme.util.TimeFunction; + +/** + * Fades a supplied quad (or one that covers the screen) in from a solid color + * or out to a solid color. + */ +public class FadeInOutEffect extends Node +{ + public FadeInOutEffect (ColorRGBA color, float startAlpha, float endAlpha, + float duration, boolean overUI) + { + this(color, new LinearTimeFunction(startAlpha, endAlpha, duration), + overUI); + } + + public FadeInOutEffect (ColorRGBA color, TimeFunction alphaFunc, + boolean overUI) + { + this(null, color, alphaFunc, overUI); + setQuad(createCurtain()); + } + + public FadeInOutEffect (Quad quad, ColorRGBA color, TimeFunction alphaFunc, + boolean overUI) + { + super("FadeInOut"); + + _color = new ColorRGBA( + color.r, color.g, color.b, alphaFunc.getValue(0)); + _alphaFunc = alphaFunc; + + DisplaySystem ds = DisplaySystem.getDisplaySystem(); + _astate = ds.getRenderer().createAlphaState(); + _astate.setBlendEnabled(true); + _astate.setSrcFunction(AlphaState.SB_SRC_ALPHA); + _astate.setDstFunction(AlphaState.DB_ONE_MINUS_SRC_ALPHA); + _astate.setEnabled(true); + + if (quad != null) { + setQuad(quad); + } + + setRenderQueueMode(Renderer.QUEUE_ORTHO); + setZOrder(overUI ? -1 : 1); + } + + /** + * Configures the quad that will be faded in or out. + */ + public void setQuad (Quad quad) + { + attachChild(quad); + quad.setDefaultColor(_color); + quad.setRenderState(_astate); + quad.updateRenderState(); + } + + /** + * Allows the fade to be paused. + */ + public void setPaused (boolean paused) + { + _paused = paused; + } + + /** + * Indicates whether or not the fade is paused. + */ + public boolean isPaused () + { + return _paused; + } + + // documentation inherited + public void updateGeometricState (float time, boolean initiator) + { + super.updateGeometricState(time, initiator); + if (_paused) { + return; + } + + float alpha = _alphaFunc.getValue(time); + _color.a = Math.min(1f, Math.max(0f, alpha)); + if (_alphaFunc.isComplete()) { + fadeComplete(); + } + } + + /** + * Called (only once) when we have reached the end of our fade. + * Automatically detaches this effect from the hierarchy. + */ + protected void fadeComplete () + { + getParent().detachChild(this); + } + + /** + * Creates a quad that covers the entire screen for full-screen fades. + */ + protected Quad createCurtain () + { + // create a quad the size of the screen + DisplaySystem ds = DisplaySystem.getDisplaySystem(); + float width = ds.getWidth(), height = ds.getHeight(); + Quad curtain = new Quad("curtain" + hashCode(), width, height); + curtain.setLocalTranslation(new Vector3f(width/2, height/2, 0f)); + return curtain; + } + + protected ColorRGBA _color; + protected AlphaState _astate; + protected TimeFunction _alphaFunc; + protected boolean _paused; +} diff --git a/src/java/com/threerings/jme/effect/WindowSlider.java b/src/java/com/threerings/jme/effect/WindowSlider.java new file mode 100644 index 00000000..f8d6151f --- /dev/null +++ b/src/java/com/threerings/jme/effect/WindowSlider.java @@ -0,0 +1,127 @@ +// +// $Id$ + +package com.threerings.jme.effect; + +import com.jme.scene.Node; +import com.jme.system.DisplaySystem; + +import com.jmex.bui.BWindow; + +import com.threerings.jme.util.LinearTimeFunction; +import com.threerings.jme.util.TimeFunction; + +/** + * Slides a window onto the center of the screen from offscreen or offscreen + * from the center of the screen. + */ +public class WindowSlider extends Node +{ + public static final int FROM_TOP = 0; + public static final int FROM_RIGHT = 1; + public static final int TO_TOP = 2; + public static final int TO_RIGHT = 3; + + /** + * Creates a window slider with the specified mode and window that will + * slide the window either onto or off of the screen in the specified + * number of seconds. + * + * @param dx an offset applied to the starting or destination position + * along the x axis (starting for sliding off, destination for sliding on). + * @param dy an offset applied along the y axis. + */ + public WindowSlider (BWindow window, int mode, float duration, + int dx, int dy) + { + super("slider"); + + DisplaySystem ds = DisplaySystem.getDisplaySystem(); + int swidth = ds.getWidth(), sheight = ds.getHeight(); + int wwidth = window.getWidth(), wheight = window.getHeight(); + + int start = 0, end = 0; + switch (mode) { + case FROM_TOP: + start = sheight+wheight; + end = (sheight-wheight)/2 + dy; + window.setLocation((swidth-wwidth)/2 + dx, start); + break; + + case FROM_RIGHT: + start = swidth+wwidth; + end = (swidth-wwidth)/2 + dx; + window.setLocation(start, (sheight-wheight)/2 + dy); + break; + + case TO_TOP: + start = (sheight-wheight)/2 + dy; + end = sheight+wheight; + window.setLocation((swidth-wwidth)/2 + dx, start); + break; + + case TO_RIGHT: + start = (swidth-wwidth)/2 + dx; + end = swidth+wwidth; + window.setLocation(start, (sheight-wheight)/2 + dy); + break; + } + + _mode = mode; + _window = window; + _tfunc = new LinearTimeFunction(start, end, duration); + } + + /** + * Allows some number of ticks to be skipped to give the window that is + * being slid a chance to be layed out before we start keeping track of + * time. The layout may be expensive and cause the frame rate to drop for a + * frame or two, thus booching our smooth sliding onto the screen. + */ + public void setSkipTicks (int skipTicks) + { + _skipTicks = skipTicks; + } + + // documentation inherited + public void updateGeometricState (float time, boolean initiator) + { + super.updateGeometricState(time, initiator); + + // skip ticks as long as we need to + if (_skipTicks-- > 0) { + return; + } + + int winx, winy; + if (_mode % 2 == 1) { + winx = (int)_tfunc.getValue(time); + winy = _window.getY(); + } else { + winx = _window.getX(); + winy = (int)_tfunc.getValue(time); + } + _window.setLocation(winx, winy); + + if (_tfunc.isComplete()) { + slideComplete(); + } + } + + /** + * Called (only once) when we have reached the end of our slide. + * Automatically detaches this effect from the hierarchy. + */ + protected void slideComplete () + { + getParent().detachChild(this); + } + + protected int _mode; + protected BWindow _window; + protected TimeFunction _tfunc; + + // skip two frames by default as that generally handles the normal window + // layout process + protected int _skipTicks = 2; +} diff --git a/src/java/com/threerings/jme/model/EmissionController.java b/src/java/com/threerings/jme/model/EmissionController.java new file mode 100644 index 00000000..26907cdb --- /dev/null +++ b/src/java/com/threerings/jme/model/EmissionController.java @@ -0,0 +1,79 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.model; + +import java.util.Properties; + +import com.jme.math.Vector3f; +import com.jme.scene.Geometry; +import com.jme.scene.Spatial; +import com.jme.util.geom.BufferUtils; + +/** + * A model controller whose target represents an emitter. + */ +public abstract class EmissionController extends ModelController +{ + @Override // documentation inherited + public void configure (Properties props, Spatial target) + { + // substitute underlying mesh for geometry targets + if (target instanceof ModelNode) { + Spatial mesh = ((ModelNode)target).getChild("mesh"); + if (mesh != null) { + target = mesh; + } + } + super.configure(props, target); + } + + @Override // documentation inherited + public void init (Model model) + { + super.init(model); + _target.setCullMode(Spatial.CULL_ALWAYS); + } + + /** + * Determines the current location of the emitter in world coordinates. + */ + protected void getEmitterLocation (Vector3f result) + { + result.set(_target.getWorldTranslation()); + } + + /** + * Determines the current direction of the emitter in world coordinates. + */ + protected void getEmitterDirection (Vector3f result) + { + if (_target instanceof Geometry) { + BufferUtils.populateFromBuffer(result, + ((Geometry)_target).getNormalBuffer(0), 0); + } else { + result.set(0f, 0f, -1f); + } + _target.getWorldRotation().multLocal(result); + } + + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/jme/model/Model.java b/src/java/com/threerings/jme/model/Model.java new file mode 100644 index 00000000..d4bf1b90 --- /dev/null +++ b/src/java/com/threerings/jme/model/Model.java @@ -0,0 +1,1036 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.model; + +import java.io.Externalizable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Properties; + +import com.jme.bounding.BoundingVolume; +import com.jme.math.FastMath; +import com.jme.math.Matrix4f; +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; +import com.jme.renderer.Camera; +import com.jme.renderer.Renderer; +import com.jme.scene.Controller; +import com.jme.scene.Node; +import com.jme.scene.Spatial; + +import com.samskivert.util.ObserverList; +import com.samskivert.util.RandomUtil; + +import com.threerings.jme.Log; + +/** + * The base node for models. + */ +public class Model extends ModelNode +{ + /** The supported types of animation in decreasing order of complexity. */ + public enum AnimationMode { + SKIN, MORPH, FLIPBOOK + }; + + /** Lets listeners know when animations are completed (which only happens + * for non-repeating animations) or cancelled. */ + public interface AnimationObserver + { + /** + * Called when an animation has started. + * + * @return true to remain on the observer list, false to remove self + */ + public boolean animationStarted (Model model, String anim); + + /** + * Called when a non-repeating animation has finished. + * + * @return true to remain on the observer list, false to remove self + */ + public boolean animationCompleted (Model model, String anim); + + /** + * Called when an animation has been cancelled. + * + * @return true to remain on the observer list, false to remove self + */ + public boolean animationCancelled (Model model, String anim); + } + + /** An animation for the model. */ + public static class Animation + implements Serializable + { + /** The rate of the animation in frames per second. */ + public int frameRate; + + /** The animation repeat type ({@link Controller#RT_CLAMP}, + * {@link Controller#RT_CYCLE}, or {@link Controller#RT_WRAP}). */ + public int repeatType; + + /** The transformation targets of the animation. */ + public Spatial[] transformTargets; + + /** The animation transforms (one transform per target per frame). */ + public transient Transform[][] transforms; + + /** Uniquely identifies this animation within the model. */ + public transient int animId; + + /** For each frame, whether the frame has been stored in meshes. */ + public transient boolean[] stored; + + /** + * Returns this animation's duration in seconds. + */ + public float getDuration () + { + return (float)transforms.length / frameRate; + } + + /** + * Rebinds this animation for a prototype instance. + * + * @param pnodes a mapping from prototype nodes to instance nodes + */ + public Animation rebind (HashMap pnodes) + { + Animation anim = new Animation(); + anim.frameRate = frameRate; + anim.repeatType = repeatType; + anim.transforms = transforms; + anim.transformTargets = new Spatial[transformTargets.length]; + for (int ii = 0; ii < transformTargets.length; ii++) { + anim.transformTargets[ii] = + (Spatial)pnodes.get(transformTargets[ii]); + } + anim.animId = animId; + anim.stored = stored; + return anim; + } + + /** + * Applies the transforms for a frame of this animation. + */ + public void applyFrame (int fidx) + { + Transform[] xforms = transforms[fidx]; + for (int ii = 0; ii < transformTargets.length; ii++) { + xforms[ii].apply(transformTargets[ii]); + } + } + + /** + * Blends the transforms between two frames of this animation. + */ + public void blendFrames (int fidx, int nidx, float alpha) + { + Transform[] xforms = transforms[fidx], nxforms = transforms[nidx]; + for (int ii = 0; ii < transformTargets.length; ii++) { + xforms[ii].blend(nxforms[ii], alpha, transformTargets[ii]); + } + } + + private void writeObject (ObjectOutputStream out) + throws IOException + { + out.defaultWriteObject(); + out.writeInt(transforms.length); + for (int ii = 0; ii < transforms.length; ii++) { + for (int jj = 0; jj < transformTargets.length; jj++) { + transforms[ii][jj].writeExternal(out); + } + } + } + + private void readObject (ObjectInputStream in) + throws IOException, ClassNotFoundException + { + in.defaultReadObject(); + transforms = new Transform[in.readInt()][transformTargets.length]; + for (int ii = 0; ii < transforms.length; ii++) { + for (int jj = 0; jj < transformTargets.length; jj++) { + transforms[ii][jj] = new Transform(new Vector3f(), + new Quaternion(), new Vector3f()); + transforms[ii][jj].readExternal(in); + } + } + } + + private static final long serialVersionUID = 1; + } + + /** A frame element that manipulates the target's transform. */ + public static final class Transform + implements Externalizable + { + public Transform ( + Vector3f translation, Quaternion rotation, Vector3f scale) + { + _translation = translation; + _rotation = rotation; + _scale = scale; + } + + public void apply (Spatial target) + { + target.getLocalTranslation().set(_translation); + target.getLocalRotation().set(_rotation); + target.getLocalScale().set(_scale); + } + + /** + * Blends between this transform and the next, applying the result to + * the given target. + * + * @param alpha the blend factor: 0.0 for entirely this frame, 1.0 for + * entirely the next + */ + public void blend (Transform next, float alpha, Spatial target) + { + target.getLocalTranslation().interpolate(_translation, + next._translation, alpha); + target.getLocalRotation().slerp(_rotation, next._rotation, alpha); + target.getLocalScale().interpolate(_scale, next._scale, alpha); + } + + // documentation inherited from interface Externalizable + public void writeExternal (ObjectOutput out) + throws IOException + { + _translation.writeExternal(out); + _rotation.writeExternal(out); + _scale.writeExternal(out); + } + + // documentation inherited from interface Externalizable + public void readExternal (ObjectInput in) + throws IOException, ClassNotFoundException + { + _translation.readExternal(in); + _rotation.readExternal(in); + _scale.readExternal(in); + } + + /** The transform at this frame. */ + protected Vector3f _translation, _scale; + protected Quaternion _rotation; + + private static final long serialVersionUID = 1; + } + + /** Customized clone creator for models. */ + public static class CloneCreator + { + /** A shared seed used to select textures consistently. */ + public int random; + + /** Maps original objects to their copies. */ + public HashMap originalToCopy = new HashMap(); + + public CloneCreator (Model toCopy) + { + _toCopy = toCopy; + addProperty("vertices"); + addProperty("colors"); + addProperty("normals"); + addProperty("texcoords"); + addProperty("vboinfo"); + addProperty("indices"); + addProperty("obbtree"); + addProperty("displaylistid"); + addProperty("bound"); + } + + /** + * Sets the named property. + */ + public void addProperty (String name) + { + _properties.add(name); + } + + /** + * Clears the named property. + */ + public void removeProperty (String name) + { + _properties.remove(name); + } + + /** + * Checks whether the named property has been set. + */ + public boolean isSet (String name) + { + return _properties.contains(name); + } + + /** + * Creates a new copy of the target model. + */ + public Model createCopy () + { + random = RandomUtil.getInt(Integer.MAX_VALUE); + Model copy = (Model)_toCopy.putClone(null, this); + originalToCopy.clear(); // make sure no references remain + return copy; + } + + /** The model to copy. */ + protected Model _toCopy; + + /** The set of added properties. */ + protected HashSet _properties = new HashSet(); + } + + /** + * Attempts to read a model from the specified file. + * + * @param map if true, map buffers into memory directly from the + * file + */ + public static Model readFromFile (File file, boolean map) + throws IOException + { + // read the serialized model and its children + FileInputStream fis = new FileInputStream(file); + ObjectInputStream ois = new ObjectInputStream(fis); + Model model; + try { + model = (Model)ois.readObject(); + } catch (ClassNotFoundException e) { + Log.warning("Encountered unknown class [error=" + e + "]."); + return null; + } + + // then either read or map the buffers + FileChannel fc = fis.getChannel(); + if (map) { + long pos = fc.position(); + model.sliceBuffers(fc.map(FileChannel.MapMode.READ_ONLY, + pos, fc.size() - pos)); + } else { + model.readBuffers(fc); + } + ois.close(); + + // initialize the model as a prototype + model.initPrototype(); + + return model; + } + + /** + * No-arg constructor for deserialization. + */ + public Model () + { + } + + /** + * Standard constructor. + */ + public Model (String name, Properties props) + { + super(name); + _props = props; + } + + /** + * Returns a reference to the properties of the model. + */ + public Properties getProperties () + { + return _props; + } + + /** + * Initializes this model as prototype. Only necessary when the prototype + * was not loaded through {@link #readFromFile}. + */ + public void initPrototype () + { + // initialize shared transient animation state + if (_anims != null) { + int nextId = 1; + for (Animation anim : _anims.values()) { + anim.animId = nextId++; + anim.stored = new boolean[anim.transforms.length]; + } + } + setReferenceTransforms(); + cullInvisibleNodes(); + initInstance(); + } + + /** + * Adds an animation to the model's library. This should only be called by + * the model compiler. + */ + public void addAnimation (String name, Animation anim) + { + if (_anims == null) { + _anims = new HashMap(); + } + _anims.put(name, anim); + + // store the original transforms + Transform[] oxforms = new Transform[anim.transformTargets.length]; + for (int ii = 0; ii < anim.transformTargets.length; ii++) { + Spatial target = anim.transformTargets[ii]; + oxforms[ii] = new Transform( + new Vector3f(target.getLocalTranslation()), + new Quaternion(target.getLocalRotation()), + new Vector3f(target.getLocalScale())); + } + + // run through every frame of the animation, expanding the bounding + // volumes of any deformable meshes + for (int ii = 0; ii < anim.transforms.length; ii++) { + for (int jj = 0; jj < anim.transforms[ii].length; jj++) { + anim.transforms[ii][jj].apply(anim.transformTargets[jj]); + } + updateWorldData(0f); + expandModelBounds(); + } + + // restore the original transforms + for (int ii = 0; ii < anim.transformTargets.length; ii++) { + oxforms[ii].apply(anim.transformTargets[ii]); + } + updateWorldData(0f); + } + + /** + * Sets the animation mode to use for this model. This should be set + * on the prototype before any animations are started or any instances + * are created. + */ + public void setAnimationMode (AnimationMode mode) + { + _animMode = mode; + } + + /** + * Returns the animation mode configured for this model. + */ + public AnimationMode getAnimationMode () + { + return _animMode; + } + + /** + * Returns the names of the model's animations. + */ + public String[] getAnimationNames () + { + if (_prototype != null) { + return _prototype.getAnimationNames(); + } + return (_anims == null) ? new String[0] : + _anims.keySet().toArray(new String[_anims.size()]); + } + + /** + * Checks whether the unit has an animation with the given name. + */ + public boolean hasAnimation (String name) + { + if (_prototype != null) { + return _prototype.hasAnimation(name); + } + return (_anims == null) ? false : _anims.containsKey(name); + } + + /** + * Starts the named animation. + * + * @return the duration of the started animation (for looping animations, + * the duration of one cycle), or -1 if the animation was not found + */ + public float startAnimation (String name) + { + Animation anim = getAnimation(name); + if (anim == null) { + return -1f; + } + if (_anim != null) { + _animObservers.apply(new AnimCancelledOp(_animName)); + } + _anim = anim; + _animName = name; + _fidx = 0; + _nidx = 1; + _fdir = +1; + _elapsed = 0f; + _animObservers.apply(new AnimStartedOp(_animName)); + return anim.getDuration() / _animSpeed; + } + + /** + * Fast-forwards the current animation by the given number of seconds. + */ + public void fastForwardAnimation (float time) + { + updateAnimation(time); + } + + /** + * Gets a reference to the animation with the given name. + */ + public Animation getAnimation (String name) + { + if (_anims == null) { + return null; + } + Animation anim = _anims.get(name); + if (anim != null) { + return anim; + } + if (_prototype != null) { + Animation panim = _prototype._anims.get(name); + if (panim != null) { + _anims.put(name, anim = panim.rebind(_pnodes)); + return anim; + } + } + Log.warning("Requested unknown animation [name=" + name + "]."); + return null; + } + + /** + * Returns a reference to the currently running animation, or + * null if no animation is running. + */ + public Animation getAnimation () + { + return _anim; + } + + /** + * Stops the currently running animation. + */ + public void stopAnimation () + { + if (_anim == null) { + return; + } + _anim = null; + _animObservers.apply(new AnimCancelledOp(_animName)); + } + + /** + * Sets the animation speed, which acts as a multiplier for the frame rate + * of each animation. + */ + public void setAnimationSpeed (float speed) + { + _animSpeed = speed; + } + + /** + * Returns the currently configured animation speed. + */ + public float getAnimationSpeed () + { + return _animSpeed; + } + + /** + * Adds an animation observer. + */ + public void addAnimationObserver (AnimationObserver obs) + { + _animObservers.add(obs); + } + + /** + * Removes an animation observer. + */ + public void removeAnimationObserver (AnimationObserver obs) + { + _animObservers.remove(obs); + } + + /** + * Returns a reference to the node that contains this model's emissions + * (in world space, so the emissions do not move with the model). This + * node is created and added when this method is first called. + */ + public Node getEmissionNode () + { + if (_emissionNode == null) { + attachChild(_emissionNode = new Node("emissions") { + public void updateWorldVectors () { + worldTranslation.set(localTranslation); + worldRotation.set(localRotation); + worldScale.set(localScale); + } + }); + } + return _emissionNode; + } + + /** + * Writes this model out to a file. + */ + public void writeToFile (File file) + throws IOException + { + // start out by writing this node and its children + FileOutputStream fos = new FileOutputStream(file); + ObjectOutputStream oos = new ObjectOutputStream(fos); + oos.writeObject(this); + + // now traverse the scene graph appending buffers to the + // end of the file + writeBuffers(fos.getChannel()); + oos.close(); + } + + @Override // documentation inherited + public void writeExternal (ObjectOutput out) + throws IOException + { + // don't serialize the emission node; it contains transient geometry + // created by controllers + if (_emissionNode != null) { + detachChild(_emissionNode); + } + super.writeExternal(out); + if (_emissionNode != null) { + attachChild(_emissionNode); + } + out.writeObject(_props); + out.writeObject(_anims); + out.writeObject(getControllers()); + } + + @Override // documentation inherited + public void readExternal (ObjectInput in) + throws IOException, ClassNotFoundException + { + super.readExternal(in); + _props = (Properties)in.readObject(); + _anims = (HashMap)in.readObject(); + ArrayList controllers = (ArrayList)in.readObject(); + for (Object ctrl : controllers) { + addController((Controller)ctrl); + } + } + + @Override // documentation inherited + public void resolveTextures (TextureProvider tprov) + { + super.resolveTextures(tprov); + for (Object ctrl : getControllers()) { + if (ctrl instanceof ModelController) { + ((ModelController)ctrl).resolveTextures(tprov); + } + } + } + + /** + * Creates and returns a new instance of this model. + */ + public Model createInstance () + { + if (_prototype != null) { + return _prototype.createInstance(); + } + if (_ccreator == null) { + _ccreator = new CloneCreator(this); + } + Model instance = _ccreator.createCopy(); + instance.initInstance(); + return instance; + } + + /** + * Locks the transforms and bounds of this model in the expectation that it + * will never be moved from its current position. + */ + public void lockInstance () + { + // collect the controller targets and lock recursively + HashSet targets = new HashSet(); + for (Object ctrl : getControllers()) { + if (ctrl instanceof ModelController) { + targets.add(((ModelController)ctrl).getTarget()); + } + } + lockInstance(targets); + } + + @Override // documentation inherited + public Spatial putClone (Spatial store, CloneCreator properties) + { + Model mstore = (Model)properties.originalToCopy.get(this); + if (mstore != null) { + return mstore; + } else if (store == null) { + mstore = new Model(getName(), _props); + } else { + mstore = (Model)store; + } + // don't clone the emission node, as it contains transient geometry + if (_emissionNode != null) { + detachChild(_emissionNode); + } + super.putClone(mstore, properties); + if (_emissionNode != null) { + attachChild(_emissionNode); + } + mstore._prototype = this; + if (_anims != null) { + mstore._anims = new HashMap(); + } + mstore._pnodes = (HashMap)properties.originalToCopy.clone(); + mstore._animMode = _animMode; + return mstore; + } + + @Override // documentation inherited + public void updateGeometricState (float time, boolean initiator) + { + // if we were not visible the last time we were rendered, don't do a + // full update; just update the world bound and wait until we come + // into view + boolean wasOutside = _outside; + _outside = isOutsideFrustum() && worldBound != null; + + // slow evvvverything down by the animation speed + time *= _animSpeed; + if (_anim != null) { + updateAnimation(time); + } + + // update controllers and children with accumulated time + _accum += time; + if (_outside) { + if (!wasOutside) { + updateModelBound(); + } + updateWorldVectors(); + worldBound = _modelBound.transform(getWorldRotation(), + getWorldTranslation(), getWorldScale(), worldBound); + + } else { + super.updateGeometricState(_accum, initiator); + _accum = 0f; + } + } + + @Override // documentation inherited + public void onDraw (Renderer r) + { + // if we switch from invisible to visible, we have to do a last-minute + // full update (which only works if our meshes are enqueued) + super.onDraw(r); + if (_outside && !isOutsideFrustum()) { + updateWorldData(0f); + } + } + + @Override // documentation inherited + public void updateModelBound () + { + if (worldBound == null) { + return; + } + setTransform(getWorldTranslation(), getWorldRotation(), + getWorldScale(), _xform); + _xform.invertLocal(); + _xform.toTranslationVector(_trans); + extractScale(_xform, _scale); + _xform.toRotationQuat(_rot); + _modelBound = worldBound.transform(_rot, _trans, _scale, _modelBound); + } + + /** + * Determines whether this node was determined to be entirely outside the + * view frustum. + */ + protected boolean isOutsideFrustum () + { + for (Node node = this; node != null; node = node.getParent()) { + if (node.getLastFrustumIntersection() == Camera.OUTSIDE_FRUSTUM) { + return true; + } + } + return false; + } + + /** + * Initializes the per-instance state of this model. + */ + protected void initInstance () + { + // initialize the controllers + for (Object ctrl : getControllers()) { + if (ctrl instanceof ModelController) { + ((ModelController)ctrl).init(this); + } + } + } + + /** + * Updates the model's state according to the current animation. + */ + protected void updateAnimation (float time) + { + // no need to update between frames for flipbook animation + if (_animMode == AnimationMode.FLIPBOOK && _elapsed > 0f && + _elapsed < 1f) { + _elapsed += (time * _anim.frameRate); + return; + } + + // advance the frame counter if necessary + while (_elapsed > 1f) { + advanceFrameCounter(); + _elapsed -= 1f; + } + + // update the target transforms and animation frame if not outside the + // view frustum + if (!_outside) { + if (_animMode == AnimationMode.FLIPBOOK) { + int frameId = (_anim.animId << 16) | _fidx; + _anim.applyFrame(_fidx); + if (!_anim.stored[_fidx]) { + storeMeshFrame(frameId, false); + updateWorldData(0f); + _anim.stored[_fidx] = true; + } + setMeshFrame(frameId); + + } else if (_animMode == AnimationMode.MORPH) { + int frameId1 = (_anim.animId << 16) | _fidx, + frameId2 = (_anim.animId << 16) | _nidx; + if (!_anim.stored[_fidx]) { + storeMeshFrame(frameId1, true); + _anim.applyFrame(_fidx); + updateWorldData(0f); + _anim.stored[_fidx] = true; + } + if (!_anim.stored[_nidx]) { + storeMeshFrame(frameId2, true); + _anim.applyFrame(_nidx); + updateWorldData(0f); + _anim.stored[_nidx] = true; + } + _anim.blendFrames(_fidx, _nidx, _elapsed); + blendMeshFrames(frameId1, frameId2, _elapsed); + + } else { // _animMode == AnimationMode.SKIN + _anim.blendFrames(_fidx, _nidx, _elapsed); + } + } + + // if the next index is the same as this one, we are finished + if (_fidx == _nidx) { + _anim = null; + _animObservers.apply(new AnimCompletedOp(_animName)); + return; + } + + _elapsed += (time * _anim.frameRate); + } + + /** + * Advances the frame counter by one frame. + */ + protected void advanceFrameCounter () + { + _fidx = _nidx; + int nframes = _anim.transforms.length; + if (_anim.repeatType == Controller.RT_CLAMP) { + _nidx = Math.min(_nidx + 1, nframes - 1); + + } else if (_anim.repeatType == Controller.RT_WRAP) { + _nidx = (_nidx + 1) % nframes; + + } else { // _anim.repeatType == Controller.RT_CYCLE + if ((_nidx + _fdir) < 0 || (_nidx + _fdir) >= nframes) { + _fdir *= -1; // reverse direction + } + _nidx += _fdir; + } + } + + /** + * Extracts the scale factor from the given transform and normalizes it. + */ + protected static void extractScale (Matrix4f m, Vector3f scale) + { + scale.x = FastMath.sqrt(m.m00*m.m00 + m.m01*m.m01 + m.m02*m.m02); + m.m00 /= scale.x; + m.m01 /= scale.x; + m.m02 /= scale.x; + scale.y = FastMath.sqrt(m.m10*m.m10 + m.m11*m.m11 + m.m12*m.m12); + m.m10 /= scale.y; + m.m11 /= scale.y; + m.m12 /= scale.y; + scale.z = FastMath.sqrt(m.m20*m.m20 + m.m21*m.m21 + m.m22*m.m22); + m.m20 /= scale.z; + m.m21 /= scale.z; + m.m22 /= scale.z; + } + + /** A reference to the prototype, or null if this is a + * prototype. */ + protected Model _prototype; + + /** For prototype models, a customized clone creator used to generate + * instances. */ + protected CloneCreator _ccreator; + + /** The animation mode to use for this model. */ + protected AnimationMode _animMode; + + /** For instances, maps prototype nodes to their corresponding instance + * nodes. */ + protected HashMap _pnodes; + + /** The model properties. */ + protected Properties _props; + + /** The model animations. */ + protected HashMap _anims; + + /** The currently running animation, or null for none. */ + protected Animation _anim; + + /** The name of the currently running animation, if any. */ + protected String _animName; + + /** The current animation speed multiplier. */ + protected float _animSpeed = 1f; + + /** The index of the current and next frames. */ + protected int _fidx, _nidx; + + /** The direction for wrapping animations (+1 forward, -1 backward). */ + protected int _fdir; + + /** The frame portion elapsed since the start of the current frame. */ + protected float _elapsed; + + /** The amount of update time accumulated while outside of view frustum. */ + protected float _accum; + + /** The child node that contains the model's emissions in world space. */ + protected Node _emissionNode; + + /** The model space bounding volume. */ + protected BoundingVolume _modelBound; + + /** Whether or not we were outside the frustum at the last update. */ + protected boolean _outside; + + /** Temporary transform variables. */ + protected Matrix4f _xform = new Matrix4f(); + protected Vector3f _trans = new Vector3f(), _scale = new Vector3f(); + protected Quaternion _rot = new Quaternion(); + + /** Animation completion listeners. */ + protected ObserverList _animObservers = + new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY); + + /** Used to notify observers of animation initiation. */ + protected class AnimStartedOp + implements ObserverList.ObserverOp + { + public AnimStartedOp (String name) + { + _name = name; + } + + // documentation inherited from interface ObserverOp + public boolean apply (AnimationObserver obs) + { + return obs.animationStarted(Model.this, _name); + } + + /** The name of the animation started. */ + protected String _name; + } + + /** Used to notify observers of animation completion. */ + protected class AnimCompletedOp + implements ObserverList.ObserverOp + { + public AnimCompletedOp (String name) + { + _name = name; + } + + // documentation inherited from interface ObserverOp + public boolean apply (AnimationObserver obs) + { + return obs.animationCompleted(Model.this, _name); + } + + /** The name of the animation completed. */ + protected String _name; + } + + /** Used to notify observers of animation cancellation. */ + protected class AnimCancelledOp + implements ObserverList.ObserverOp + { + public AnimCancelledOp (String name) + { + _name = name; + } + + // documentation inherited from interface ObserverOp + public boolean apply (AnimationObserver obs) + { + return obs.animationCancelled(Model.this, _name); + } + + /** The name of the animation cancelled. */ + protected String _name; + } + + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/jme/model/ModelController.java b/src/java/com/threerings/jme/model/ModelController.java new file mode 100644 index 00000000..f129c7f7 --- /dev/null +++ b/src/java/com/threerings/jme/model/ModelController.java @@ -0,0 +1,167 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.model; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Properties; + +import com.jme.scene.Controller; +import com.jme.scene.Node; +import com.jme.scene.Spatial; + +import com.samskivert.util.StringUtil; + +/** + * The superclass of procedural animation controllers for models. + */ +public abstract class ModelController extends Controller + implements Externalizable +{ + /** + * Configures this controller based on the supplied (sub-)properties and + * controller target. + */ + public void configure (Properties props, Spatial target) + { + _target = target; + String[] anims = StringUtil.parseStringArray( + props.getProperty("animations", "")); + if (anims.length == 0) { + return; + } + _animations = new HashSet(); + Collections.addAll(_animations, anims); + } + + /** + * Returns a reference to the controller's target. + */ + public Spatial getTarget () + { + return _target; + } + + /** + * Resolves any textures required by the controller. + */ + public void resolveTextures (TextureProvider tprov) + { + } + + /** + * Initializes this controller. + */ + public void init (Model model) + { + model.addAnimationObserver(_animobs); + if (_animations != null) { + setActive(false); + } + } + + /** + * Creates or populates and returns a clone of this object using the given + * clone properties. + * + * @param store an instance of this class to populate, or null + * to create a new instance + */ + public Controller putClone ( + Controller store, Model.CloneCreator properties) + { + if (store == null) { + return null; + } + ModelController mstore = (ModelController)store; + mstore._target = ((ModelSpatial)_target).putClone(null, properties); + mstore._animations = _animations; + return mstore; + } + + // documentation inherited from interface Externalizable + public void writeExternal (ObjectOutput out) + throws IOException + { + out.writeObject(_target); + out.writeObject(_animations); + } + + // documentation inherited from interface Externalizable + public void readExternal (ObjectInput in) + throws IOException, ClassNotFoundException + { + _target = (Spatial)in.readObject(); + _animations = (HashSet)in.readObject(); + } + + /** + * Called when an animation is started on the model. + */ + protected void animationStarted (String anim) + { + if (_animations != null && _animations.contains(anim)) { + setActive(true); + } + } + + /** + * Called when an animation is stopped on the model. + */ + protected void animationStopped (String anim) + { + if (_animations != null) { + setActive(false); + } + } + + /** The target to control. */ + protected Spatial _target; + + /** The animations for which this controller should be active, or + * null for all of them. */ + protected HashSet _animations; + + /** Listens to the model's animation state. */ + protected Model.AnimationObserver _animobs = + new Model.AnimationObserver() { + public boolean animationStarted (Model model, String anim) { + ModelController.this.animationStarted(anim); + return true; + } + public boolean animationCompleted (Model model, String anim) { + animationStopped(anim); + return true; + } + public boolean animationCancelled (Model model, String anim) { + animationStopped(anim); + return true; + } + }; + + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/jme/model/ModelMesh.java b/src/java/com/threerings/jme/model/ModelMesh.java new file mode 100644 index 00000000..fdb936a6 --- /dev/null +++ b/src/java/com/threerings/jme/model/ModelMesh.java @@ -0,0 +1,557 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.model; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +import java.util.Properties; + +import com.jme.bounding.BoundingVolume; +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; +import com.jme.renderer.Renderer; +import com.jme.scene.Controller; +import com.jme.scene.SharedMesh; +import com.jme.scene.Spatial; +import com.jme.scene.TriMesh; +import com.jme.scene.VBOInfo; +import com.jme.scene.batch.TriangleBatch; +import com.jme.scene.state.AlphaState; +import com.jme.scene.state.CullState; +import com.jme.scene.state.RenderState; +import com.jme.scene.state.TextureState; +import com.jme.scene.state.ZBufferState; +import com.jme.system.DisplaySystem; +import com.jme.util.geom.BufferUtils; + +import com.samskivert.util.StringUtil; + +import com.threerings.jme.Log; + +/** + * A {@link TriMesh} with a serialization mechanism tailored to stored models. + */ +public class ModelMesh extends TriMesh + implements Externalizable, ModelSpatial +{ + /** + * No-arg constructor for deserialization. + */ + public ModelMesh () + { + super("mesh"); + } + + /** + * Creates a mesh with no vertex data. + */ + public ModelMesh (String name) + { + super(name); + } + + /** + * Configures this mesh based on the given parameters and (sub-)properties. + * + * @param texture the texture specified in the model export, if any (can be + * overridden by textures specified in the properties) + * @param solid whether or not the mesh allows back face culling + * @param transparent whether or not the mesh is (partially) transparent + */ + public void configure ( + boolean solid, String texture, boolean transparent, Properties props) + { + _textures = (texture == null) ? null : StringUtil.parseStringArray( + props.getProperty(texture, texture)); + _solid = solid; + _transparent = transparent; + } + + /** + * Sets the buffers as {@link ByteBuffer}s, because we can't create byte + * views of non-byte buffers. This method is where the model is + * initialized after loading. + */ + public void reconstruct ( + ByteBuffer vertices, ByteBuffer normals, ByteBuffer colors, + ByteBuffer textures, ByteBuffer indices) + { + reconstruct( + vertices == null ? null : vertices.asFloatBuffer(), + normals == null ? null : normals.asFloatBuffer(), + colors == null ? null : colors.asFloatBuffer(), + textures == null ? null : textures.asFloatBuffer(), + indices == null ? null : indices.asIntBuffer()); + _vertexByteBuffer = vertices; + _normalByteBuffer = normals; + _colorByteBuffer = colors; + _textureByteBuffer = textures; + _indexByteBuffer = indices; + + // initialize the model if we're displaying + if (DisplaySystem.getDisplaySystem() == null) { + return; + } + if (_backCull == null) { + initSharedStates(); + } + if (_solid) { + setRenderState(_backCull); + } + if (_transparent) { + setRenderQueueMode(Renderer.QUEUE_TRANSPARENT); + setRenderState(_blendAlpha); + setRenderState(_overlayZBuffer); + } + } + + /** + * Adjusts the vertices and the transform of the mesh so that the mesh's + * position lies at the center of its bounding volume. + */ + public void centerVertices () + { + Vector3f offset = getBatch(0).getModelBound().getCenter().negate(); + if (!offset.equals(Vector3f.ZERO)) { + getLocalTranslation().subtractLocal(offset); + getBatch(0).getModelBound().getCenter().set(Vector3f.ZERO); + getBatch(0).translatePoints(offset); + } + } + + @Override // documentation inherited + public void reconstruct ( + FloatBuffer vertices, FloatBuffer normals, FloatBuffer colors, + FloatBuffer textures, IntBuffer indices) + { + super.reconstruct(vertices, normals, colors, textures, indices); + + _vertexBufferSize = (vertices == null) ? 0 : vertices.capacity(); + _normalBufferSize = (normals == null) ? 0 : normals.capacity(); + _colorBufferSize = (colors == null) ? 0 : colors.capacity(); + _textureBufferSize = (textures == null) ? 0 : textures.capacity(); + _indexBufferSize = (indices == null) ? 0 : indices.capacity(); + } + + @Override // documentation inherited + public void reconstruct ( + FloatBuffer vertices, FloatBuffer normals, FloatBuffer colors, + FloatBuffer textures) + { + super.reconstruct(vertices, normals, colors, textures); + + _vertexBufferSize = (vertices == null) ? 0 : vertices.capacity(); + _normalBufferSize = (normals == null) ? 0 : normals.capacity(); + _colorBufferSize = (colors == null) ? 0 : colors.capacity(); + _textureBufferSize = (textures == null) ? 0 : textures.capacity(); + } + + // documentation inherited from interface ModelSpatial + public Spatial putClone (Spatial store, Model.CloneCreator properties) + { + ModelMesh mstore = (ModelMesh)properties.originalToCopy.get(this); + if (mstore != null) { + return mstore; + } else if (store == null) { + mstore = new ModelMesh(getName()); + } else { + mstore = (ModelMesh)store; + } + properties.originalToCopy.put(this, mstore); + mstore.normalsMode = normalsMode; + mstore.cullMode = cullMode; + for (int ii = 0; ii < RenderState.RS_MAX_STATE; ii++) { + RenderState rstate = getRenderState(ii); + if (rstate != null) { + mstore.setRenderState(rstate); + } + } + mstore.renderQueueMode = renderQueueMode; + mstore.lockedMode = lockedMode; + mstore.lightCombineMode = lightCombineMode; + mstore.textureCombineMode = textureCombineMode; + mstore.name = name; + mstore.isCollidable = isCollidable; + mstore.localRotation.set(localRotation); + mstore.localTranslation.set(localTranslation); + mstore.localScale.set(localScale); + for (Object controller : getControllers()) { + if (controller instanceof ModelController) { + mstore.addController( + ((ModelController)controller).putClone(null, properties)); + } + } + TriangleBatch batch = (TriangleBatch)getBatch(0), + mbatch = (TriangleBatch)mstore.getBatch(0); + mbatch.setVertexBuffer(properties.isSet("vertices") ? + batch.getVertexBuffer() : + BufferUtils.clone(batch.getVertexBuffer())); + mbatch.setColorBuffer(properties.isSet("colors") ? + batch.getColorBuffer() : + BufferUtils.clone(batch.getColorBuffer())); + mbatch.setNormalBuffer(properties.isSet("normals") ? + batch.getNormalBuffer() : + BufferUtils.clone(batch.getNormalBuffer())); + FloatBuffer texcoords; + for (int ii = 0; (texcoords = batch.getTextureBuffer(ii)) != null; + ii++) { + mbatch.setTextureBuffer(properties.isSet("texcoords") ? + texcoords : BufferUtils.clone(texcoords), ii); + } + mbatch.setIndexBuffer(properties.isSet("indices") ? + batch.getIndexBuffer() : + BufferUtils.clone(batch.getIndexBuffer())); + if (properties.isSet("vboinfo")) { + mbatch.setVBOInfo(batch.getVBOInfo()); + } + if (properties.isSet("obbtree")) { + mbatch.setCollisionTree(batch.getCollisionTree()); + } + if (properties.isSet("displaylistid")) { + mbatch.setDisplayListID(batch.getDisplayListID()); + } + if (batch.getModelBound() != null) { + mbatch.setModelBound(properties.isSet("bound") ? + batch.getModelBound() : batch.getModelBound().clone(null)); + } + if (_textures != null && _textures.length > 1) { + int tidx = properties.random % _textures.length; + mstore._textures = new String[] { _textures[tidx] }; + mstore._tstates = new TextureState[] { _tstates[tidx] }; + mstore.setRenderState(_tstates[tidx]); + } else { + mstore._textures = _textures; + mstore._tstates = _tstates; + } + return mstore; + } + + @Override // documentation inherited + public void updateWorldVectors () + { + if (!_transformLocked) { + super.updateWorldVectors(); + } + } + + // documentation inherited from interface Externalizable + public void writeExternal (ObjectOutput out) + throws IOException + { + out.writeUTF(getName()); + out.writeObject(getLocalTranslation()); + out.writeObject(getLocalRotation()); + out.writeObject(getLocalScale()); + out.writeObject(getBatch(0).getModelBound()); + out.writeInt(_vertexBufferSize); + out.writeInt(_normalBufferSize); + out.writeInt(_colorBufferSize); + out.writeInt(_textureBufferSize); + out.writeInt(_indexBufferSize); + out.writeObject(_textures); + out.writeBoolean(_solid); + out.writeBoolean(_transparent); + } + + // documentation inherited from interface Externalizable + public void readExternal (ObjectInput in) + throws IOException, ClassNotFoundException + { + setName(in.readUTF()); + setLocalTranslation((Vector3f)in.readObject()); + setLocalRotation((Quaternion)in.readObject()); + setLocalScale((Vector3f)in.readObject()); + setModelBound((BoundingVolume)in.readObject()); + _vertexBufferSize = in.readInt(); + _normalBufferSize = in.readInt(); + _colorBufferSize = in.readInt(); + _textureBufferSize = in.readInt(); + _indexBufferSize = in.readInt(); + _textures = (String[])in.readObject(); + _solid = in.readBoolean(); + _transparent = in.readBoolean(); + } + + // documentation inherited from interface ModelSpatial + public void expandModelBounds () + { + // no-op + } + + // documentation inherited from interface ModelSpatial + public void setReferenceTransforms () + { + // no-op + } + + // documentation inherited from interface ModelSpatial + public void lockStaticMeshes ( + Renderer renderer, boolean useVBOs, boolean useDisplayLists) + { + if (useVBOs && renderer.supportsVBO()) { + VBOInfo vboinfo = new VBOInfo(true); + vboinfo.setVBOIndexEnabled(true); + setVBOInfo(vboinfo); + + } else if (useDisplayLists) { + lockMeshes(renderer); + } + } + + // documentation inherited from interface ModelSpatial + public void resolveTextures (TextureProvider tprov) + { + if (_textures == null) { + return; + } + _tstates = new TextureState[_textures.length]; + for (int ii = 0; ii < _textures.length; ii++) { + _tstates[ii] = tprov.getTexture(_textures[ii]); + } + if (_tstates[0] != null) { + setRenderState(_tstates[0]); + } + } + + // documentation inherited from interface ModelSpatial + public void storeMeshFrame (int frameId, boolean blend) + { + // no-op + } + + // documentation inherited from interface ModelSpatial + public void setMeshFrame (int frameId) + { + // no-op + } + + // documentation inherited from interface ModelSpatial + public void blendMeshFrames (int frameId1, int frameId2, float alpha) + { + // no-op + } + + // documentation inherited from interface ModelSpatial + public void writeBuffers (FileChannel out) + throws IOException + { + if (_vertexBufferSize > 0) { + _vertexByteBuffer.rewind(); + convertOrder(_vertexByteBuffer, ByteOrder.LITTLE_ENDIAN); + out.write(_vertexByteBuffer); + convertOrder(_vertexByteBuffer, ByteOrder.nativeOrder()); + } + if (_normalBufferSize > 0) { + _normalByteBuffer.rewind(); + convertOrder(_normalByteBuffer, ByteOrder.LITTLE_ENDIAN); + out.write(_normalByteBuffer); + convertOrder(_normalByteBuffer, ByteOrder.nativeOrder()); + } + if (_colorBufferSize > 0) { + _colorByteBuffer.rewind(); + convertOrder(_colorByteBuffer, ByteOrder.LITTLE_ENDIAN); + out.write(_colorByteBuffer); + convertOrder(_colorByteBuffer, ByteOrder.nativeOrder()); + } + if (_textureBufferSize > 0) { + _textureByteBuffer.rewind(); + convertOrder(_textureByteBuffer, ByteOrder.LITTLE_ENDIAN); + out.write(_textureByteBuffer); + convertOrder(_textureByteBuffer, ByteOrder.nativeOrder()); + } + if (_indexBufferSize > 0) { + _indexByteBuffer.rewind(); + convertOrder(_indexByteBuffer, ByteOrder.LITTLE_ENDIAN); + out.write(_indexByteBuffer); + convertOrder(_indexByteBuffer, ByteOrder.nativeOrder()); + } + } + + // documentation inherited from interface ModelSpatial + public void readBuffers (FileChannel in) + throws IOException + { + ByteBuffer vbbuf = null, nbbuf = null, cbbuf = null, tbbuf = null, + ibbuf = null; + ByteOrder le = ByteOrder.LITTLE_ENDIAN; + if (_vertexBufferSize > 0) { + vbbuf = ByteBuffer.allocateDirect(_vertexBufferSize*4).order(le); + in.read(vbbuf); + vbbuf.rewind(); + convertOrder(vbbuf, ByteOrder.nativeOrder()); + } + if (_normalBufferSize > 0) { + nbbuf = ByteBuffer.allocateDirect(_normalBufferSize*4).order(le); + in.read(nbbuf); + nbbuf.rewind(); + convertOrder(nbbuf, ByteOrder.nativeOrder()); + } + if (_colorBufferSize > 0) { + cbbuf = ByteBuffer.allocateDirect(_colorBufferSize*4).order(le); + in.read(cbbuf); + cbbuf.rewind(); + convertOrder(cbbuf, ByteOrder.nativeOrder()); + } + if (_textureBufferSize > 0) { + tbbuf = ByteBuffer.allocateDirect(_textureBufferSize*4).order(le); + in.read(tbbuf); + tbbuf.rewind(); + convertOrder(tbbuf, ByteOrder.nativeOrder()); + } + if (_indexBufferSize > 0) { + ibbuf = ByteBuffer.allocateDirect(_indexBufferSize*4).order(le); + in.read(ibbuf); + ibbuf.rewind(); + convertOrder(ibbuf, ByteOrder.nativeOrder()); + } + reconstruct(vbbuf, nbbuf, cbbuf, tbbuf, ibbuf); + } + + // documentation inherited from interface ModelSpatial + public void sliceBuffers (MappedByteBuffer map) + { + ByteBuffer vbbuf = null, nbbuf = null, cbbuf = null, tbbuf = null, + ibbuf = null; + int total = 0; + if (_vertexBufferSize > 0) { + int npos = map.position() + _vertexBufferSize*4; + map.limit(npos); + vbbuf = map.slice().order(ByteOrder.LITTLE_ENDIAN); + map.position(npos); + } + if (_normalBufferSize > 0) { + int npos = map.position() + _normalBufferSize*4; + map.limit(npos); + nbbuf = map.slice().order(ByteOrder.LITTLE_ENDIAN); + map.position(npos); + } + if (_colorBufferSize > 0) { + int npos = map.position() + _colorBufferSize*4; + map.limit(npos); + cbbuf = map.slice().order(ByteOrder.LITTLE_ENDIAN); + map.position(npos); + } + if (_textureBufferSize > 0) { + int npos = map.position() + _textureBufferSize*4; + map.limit(npos); + tbbuf = map.slice().order(ByteOrder.LITTLE_ENDIAN); + map.position(npos); + } + if (_indexBufferSize > 0) { + int npos = map.position() + _indexBufferSize*4; + map.limit(npos); + ibbuf = map.slice().order(ByteOrder.LITTLE_ENDIAN); + map.position(npos); + } + reconstruct(vbbuf, nbbuf, cbbuf, tbbuf, ibbuf); + } + + /** + * Locks the transform and bounds of this mesh on the assumption that its + * position will not change. + */ + protected void lockInstance () + { + lockBounds(); + _transformLocked = true; + } + + /** + * Imposes the specified order on the given buffer of 32 bit values. + */ + protected static void convertOrder (ByteBuffer buf, ByteOrder order) + { + if (buf.order() == order) { + return; + } + IntBuffer obuf = buf.asIntBuffer(), + nbuf = buf.order(order).asIntBuffer(); + while (obuf.hasRemaining()) { + nbuf.put(obuf.get()); + } + } + + /** + * Initializes the states shared between all models. Requires an active + * display. + */ + protected static void initSharedStates () + { + Renderer renderer = DisplaySystem.getDisplaySystem().getRenderer(); + _backCull = renderer.createCullState(); + _backCull.setCullMode(CullState.CS_BACK); + _blendAlpha = renderer.createAlphaState(); + _blendAlpha.setBlendEnabled(true); + _overlayZBuffer = renderer.createZBufferState(); + _overlayZBuffer.setFunction(ZBufferState.CF_LEQUAL); + _overlayZBuffer.setWritable(false); + } + + /** The sizes of the various buffers (zero for null). */ + protected int _vertexBufferSize, _normalBufferSize, _colorBufferSize, + _textureBufferSize, _indexBufferSize; + + /** The backing byte buffers for the various buffers. */ + protected ByteBuffer _vertexByteBuffer, _normalByteBuffer, + _colorByteBuffer, _textureByteBuffer, _indexByteBuffer; + + /** The type of bounding volume that this mesh should use. */ + protected int _boundingType; + + /** The name of this model's textures, or null for none. */ + protected String[] _textures; + + /** Whether or not this mesh can enable back-face culling. */ + protected boolean _solid; + + /** Whether or not this mesh must be rendered as transparent. */ + protected boolean _transparent; + + /** For prototype meshes, the resolved texture states. */ + protected TextureState[] _tstates; + + /** Whether or not the transform has been locked. This operates in a + * slightly different way than JME's locking, in that it allows applying + * transformations to display lists. */ + protected boolean _transformLocked; + + /** The shared state for back face culling. */ + protected static CullState _backCull; + + /** The shared state for alpha blending. */ + protected static AlphaState _blendAlpha; + + /** The shared state for checking, but not writing to, the z buffer. */ + protected static ZBufferState _overlayZBuffer; + + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/jme/model/ModelNode.java b/src/java/com/threerings/jme/model/ModelNode.java new file mode 100644 index 00000000..9af6b79e --- /dev/null +++ b/src/java/com/threerings/jme/model/ModelNode.java @@ -0,0 +1,407 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.model; + +import java.io.DataOutput; +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +import java.util.ArrayList; +import java.util.HashSet; + +import com.jme.math.Matrix4f; +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; +import com.jme.renderer.Renderer; +import com.jme.scene.Controller; +import com.jme.scene.Node; +import com.jme.scene.Spatial; +import com.jme.scene.state.RenderState; + +import com.threerings.jme.Log; + +/** + * A {@link Node} with a serialization mechanism tailored to stored models. + */ +public class ModelNode extends Node + implements Externalizable, ModelSpatial +{ + /** + * No-arg constructor for deserialization. + */ + public ModelNode () + { + super("node"); + } + + /** + * Standard constructor. + */ + public ModelNode (String name) + { + super(name); + } + + /** + * Recursively searches the scene graph rooted at this node for a + * node with the provided name. + */ + public Spatial getDescendant (String name) + { + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child.getName().equals(name)) { + return child; + } else if (child instanceof ModelNode) { + child = ((ModelNode)child).getDescendant(name); + if (child != null) { + return child; + } + } + } + return null; + } + + /** + * Returns a reference to the model space transform of the node. + */ + public Matrix4f getModelTransform () + { + return _modelTransform; + } + + @Override // documentation inherited + public void updateWorldData (float time) + { + // we use locked bounds as an indication that we can skip the update + // altogether + if ((lockedMode & LOCKED_BOUNDS) == 0) { + super.updateWorldData(time); + } + } + + @Override // documentation inherited + public void updateWorldBound () + { + // if the node is culled, there are no mesh descendants and thus no + // bounds + if (cullMode != CULL_ALWAYS) { + super.updateWorldBound(); + } + } + + @Override // documentation inherited + public void updateWorldVectors () + { + super.updateWorldVectors(); + if (parent instanceof ModelNode) { + setTransform(getLocalTranslation(), getLocalRotation(), + getLocalScale(), _localTransform); + ((ModelNode)parent).getModelTransform().mult(_localTransform, + _modelTransform); + + } else { + _modelTransform.loadIdentity(); + } + } + + // documentation inherited from interface ModelSpatial + public Spatial putClone (Spatial store, Model.CloneCreator properties) + { + ModelNode mstore = (ModelNode)properties.originalToCopy.get(this); + if (mstore != null) { + return mstore; + } else if (store == null) { + mstore = new ModelNode(getName()); + } else { + mstore = (ModelNode)store; + } + properties.originalToCopy.put(this, mstore); + mstore.normalsMode = normalsMode; + mstore.cullMode = cullMode; + for (int ii = 0; ii < RenderState.RS_MAX_STATE; ii++) { + RenderState rstate = getRenderState(ii); + if (rstate != null) { + mstore.setRenderState(rstate); + } + } + mstore.renderQueueMode = renderQueueMode; + mstore.lockedMode = lockedMode; + mstore.lightCombineMode = lightCombineMode; + mstore.textureCombineMode = textureCombineMode; + mstore.name = name; + mstore.isCollidable = isCollidable; + mstore.localRotation.set(localRotation); + mstore.localTranslation.set(localTranslation); + mstore.localScale.set(localScale); + for (Object controller : getControllers()) { + if (controller instanceof ModelController) { + mstore.addController( + ((ModelController)controller).putClone(null, properties)); + } + } + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + mstore.attachChild( + ((ModelSpatial)child).putClone(null, properties)); + } + } + return mstore; + } + + // documentation inherited from interface Externalizable + public void writeExternal (ObjectOutput out) + throws IOException + { + out.writeUTF(getName()); + out.writeObject(getLocalTranslation()); + out.writeObject(getLocalRotation()); + out.writeObject(getLocalScale()); + out.writeObject(getChildren()); + } + + // documentation inherited from interface Externalizable + public void readExternal (ObjectInput in) + throws IOException, ClassNotFoundException + { + setName(in.readUTF()); + setLocalTranslation((Vector3f)in.readObject()); + setLocalRotation((Quaternion)in.readObject()); + setLocalScale((Vector3f)in.readObject()); + ArrayList children = (ArrayList)in.readObject(); + if (children != null) { + for (Spatial child : children) { + attachChild(child); + } + } + } + + // documentation inherited from interface ModelSpatial + public void expandModelBounds () + { + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).expandModelBounds(); + } + } + } + + // documentation inherited from interface ModelSpatial + public void setReferenceTransforms () + { + updateWorldVectors(); + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).setReferenceTransforms(); + } + } + } + + // documentation inherited from interface ModelSpatial + public void lockStaticMeshes ( + Renderer renderer, boolean useVBOs, boolean useDisplayLists) + { + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).lockStaticMeshes(renderer, useVBOs, + useDisplayLists); + } + } + } + + // documentation inherited from interface ModelSpatial + public void resolveTextures (TextureProvider tprov) + { + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).resolveTextures(tprov); + } + } + } + + // documentation inherited from interface ModelSpatial + public void storeMeshFrame (int frameId, boolean blend) + { + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).storeMeshFrame(frameId, blend); + } + } + } + + // documentation inherited from interface ModelSpatial + public void setMeshFrame (int frameId) + { + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).setMeshFrame(frameId); + } + } + } + + // documentation inherited from interface ModelSpatial + public void blendMeshFrames (int frameId1, int frameId2, float alpha) + { + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).blendMeshFrames( + frameId1, frameId2, alpha); + } + } + } + + // documentation inherited from interface ModelSpatial + public void writeBuffers (FileChannel out) + throws IOException + { + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).writeBuffers(out); + } + } + } + + // documentation inherited from interface ModelSpatial + public void readBuffers (FileChannel in) + throws IOException + { + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).readBuffers(in); + } + } + } + + // documentation inherited from interface ModelSpatial + public void sliceBuffers (MappedByteBuffer map) + { + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).sliceBuffers(map); + } + } + } + + /** + * Sets the cull state of any nodes that do not contain geometric + * descendants to {@link CULL_ALWAYS} so that they don't waste + * rendering time. + * + * @return true if this node should be drawn, false if it contains + * no mesh descendants + */ + protected boolean cullInvisibleNodes () + { + boolean hasVisibleDescendants = false; + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (!(child instanceof ModelNode) || + ((ModelNode)child).cullInvisibleNodes()) { + hasVisibleDescendants = true; + } + } + setCullMode(hasVisibleDescendants ? CULL_INHERIT : CULL_ALWAYS); + return hasVisibleDescendants; + } + + /** + * Locks the transforms and bounds of this instance with the assumption + * that the position will never change. + * + * @param targets the targets of the model's controllers, which determine + * the subset of nodes that can be locked + * @return true if this node is a target or contains any targets, otherwise + * false + */ + protected boolean lockInstance (HashSet targets) + { + updateWorldVectors(); + lockedMode |= LOCKED_TRANSFORMS; + + boolean containsTargets = false; + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (targets.contains(child) || (child instanceof ModelNode && + ((ModelNode)child).lockInstance(targets))) { + containsTargets = true; + + } else if (child instanceof ModelMesh) { + ((ModelMesh)child).lockInstance(); + } + } + if (containsTargets) { + return true; + } else { + updateWorldBound(); + lockedMode |= LOCKED_BOUNDS; + return false; + } + } + + /** + * Sets a matrix to the transform defined by the given translation, + * rotation, and scale values. + */ + protected static Matrix4f setTransform ( + Vector3f translation, Quaternion rotation, Vector3f scale, + Matrix4f result) + { + result.set(rotation); + result.setTranslation(translation); + + result.m00 *= scale.x; + result.m01 *= scale.y; + result.m02 *= scale.z; + + result.m10 *= scale.x; + result.m11 *= scale.y; + result.m12 *= scale.z; + + result.m20 *= scale.x; + result.m21 *= scale.y; + result.m22 *= scale.z; + + return result; + } + + /** The node's transform in local and model space. */ + protected Matrix4f _localTransform = new Matrix4f(), + _modelTransform = new Matrix4f(); + + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/jme/model/ModelSpatial.java b/src/java/com/threerings/jme/model/ModelSpatial.java new file mode 100644 index 00000000..bb85ce6d --- /dev/null +++ b/src/java/com/threerings/jme/model/ModelSpatial.java @@ -0,0 +1,115 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.model; + +import java.io.Externalizable; +import java.io.IOException; + +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +import com.jme.math.Matrix4f; +import com.jme.renderer.Renderer; +import com.jme.scene.Spatial; + +/** + * Contains method common to both {@link ModelNode}s and {@link ModelMesh}es. + */ +public interface ModelSpatial +{ + /** + * Recursively expands the model bounds of any deformable meshes so that + * they include the current vertex positions. + */ + public void expandModelBounds (); + + /** + * Recursively sets the reference transforms for any bones in the model. + */ + public void setReferenceTransforms (); + + /** + * Recursively compiles any static meshes to vertex buffer objects (VBOs) + * or display lists. + * + * @param useVBOs if true, use VBOs if the graphics card supports them + * @param useDisplayLists if true and not using VBOs, compile static + * objects to display lists + */ + public void lockStaticMeshes ( + Renderer renderer, boolean useVBOs, boolean useDisplayLists); + + /** + * Recursively resolves texture references using the given provider. + */ + public void resolveTextures (TextureProvider tprov); + + /** + * Recursively requests that the current state of all skinned meshes be + * stored as an animation frame on the next update. + * + * @param frameId the frame id, which uniquely identifies one frame of + * one animation + * @param blend whether or not the stored frames will be retrieved by + * calls to {@link #blendAnimationFrames} as opposed to + * {@link #setAnimationFrames} + */ + public void storeMeshFrame (int frameId, boolean blend); + + /** + * Recursively switches all skinned meshes to a stored animation frame for + * the next update. + */ + public void setMeshFrame (int frameId); + + /** + * Recursively blends all skinned meshes between two stored animation + * frames for the next update. + */ + public void blendMeshFrames (int frameId1, int frameId2, float alpha); + + /** + * Creates or populates and returns a clone of this object using the given + * clone properties. + * + * @param store an instance of this class to populate, or null + * to create a new instance + */ + public Spatial putClone (Spatial store, Model.CloneCreator properties); + + /** + * Recursively writes any data buffers to the output channel. + */ + public void writeBuffers (FileChannel out) + throws IOException; + + /** + * Recursively reads any data buffers from the input channel. + */ + public void readBuffers (FileChannel in) + throws IOException; + + /** + * Recursively slices any data buffers from the buffer map. + */ + public void sliceBuffers (MappedByteBuffer map); +} diff --git a/src/java/com/threerings/jme/model/Rotator.java b/src/java/com/threerings/jme/model/Rotator.java new file mode 100644 index 00000000..e99da213 --- /dev/null +++ b/src/java/com/threerings/jme/model/Rotator.java @@ -0,0 +1,128 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.model; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import java.util.Properties; + +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; +import com.jme.scene.Controller; +import com.jme.scene.Spatial; + +import com.samskivert.util.StringUtil; + +import com.threerings.jme.Log; + +/** + * A procedural animation that rotates a node around at a constant angular + * velocity. + */ +public class Rotator extends ModelController +{ + @Override // documentation inherited + public void configure (Properties props, Spatial target) + { + super.configure(props, target); + String axisstr = props.getProperty("axis", "x"), + rpsstr = props.getProperty("radpersec", "3.14"); + if (axisstr.equalsIgnoreCase("x")) { + _axis = Vector3f.UNIT_X; + } else if (axisstr.equalsIgnoreCase("y")) { + _axis = Vector3f.UNIT_Y; + } else if (axisstr.equalsIgnoreCase("z")) { + _axis = Vector3f.UNIT_Z; + } else { + float[] axis = StringUtil.parseFloatArray(axisstr); + if (axis != null && axis.length == 3) { + _axis = new Vector3f(axis[0], axis[1], + axis[2]).normalizeLocal(); + + } else { + Log.warning("Invalid rotation axis [axis=" + axisstr + "]."); + } + } + try { + _radpersec = Float.parseFloat(rpsstr); + } catch (NumberFormatException e) { + Log.warning("Invalid rotation rate [radpersec=" + rpsstr + "]."); + } + } + + // documentation inherited + public void update (float time) + { + if (!isActive()) { + return; + } + _rot.fromAngleNormalAxis(time * _radpersec, _axis); + _target.getLocalRotation().multLocal(_rot); + } + + @Override // documentation inherited + public Controller putClone ( + Controller store, Model.CloneCreator properties) + { + Rotator rstore; + if (store == null) { + rstore = new Rotator(); + } else { + rstore = (Rotator)store; + } + super.putClone(rstore, properties); + rstore._axis = _axis; + rstore._radpersec = _radpersec; + return rstore; + } + + // documentation inherited from interface Externalizable + public void writeExternal (ObjectOutput out) + throws IOException + { + super.writeExternal(out); + out.writeObject(_axis); + out.writeFloat(_radpersec); + } + + // documentation inherited from interface Externalizable + public void readExternal (ObjectInput in) + throws IOException, ClassNotFoundException + { + super.readExternal(in); + _axis = (Vector3f)in.readObject(); + _radpersec = in.readFloat(); + } + + /** The axis about which to rotate. */ + protected Vector3f _axis; + + /** The velocity at which to rotate in radians per second. */ + protected float _radpersec; + + /** A temporary quaternion. */ + protected Quaternion _rot = new Quaternion(); + + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/jme/model/SkinMesh.java b/src/java/com/threerings/jme/model/SkinMesh.java new file mode 100644 index 00000000..e9a21e83 --- /dev/null +++ b/src/java/com/threerings/jme/model/SkinMesh.java @@ -0,0 +1,495 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.model; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.Serializable; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; + +import com.jme.bounding.BoundingVolume; +import com.jme.math.Matrix4f; +import com.jme.math.Vector3f; +import com.jme.renderer.Renderer; +import com.jme.scene.Spatial; +import com.jme.scene.TriMesh; +import com.jme.scene.VBOInfo; +import com.jme.scene.batch.SharedBatch; +import com.jme.scene.batch.TriangleBatch; +import com.jme.system.DisplaySystem; +import com.jme.util.geom.BufferUtils; + +import com.samskivert.util.HashIntMap; + +import com.threerings.jme.Log; + +/** + * A triangle mesh that deforms according to a bone hierarchy. + */ +public class SkinMesh extends ModelMesh +{ + /** Represents the vertex weights of a group of vertices influenced by the + * same set of bones. */ + public static class WeightGroup + implements Serializable + { + /** The number of vertices in this weight group. */ + public int vertexCount; + + /** The bones influencing this group. */ + public Bone[] bones; + + /** The array of interleaved weights (of length vertexCount * + * boneIndices.length): weights for first vertex, weights for + * second, etc. */ + public float[] weights; + + /** + * Rebinds this weight group for a prototype instance. + * + * @param bmap the mapping from prototype to instance bones + */ + public WeightGroup rebind (HashMap bmap) + { + WeightGroup wgroup = new WeightGroup(); + wgroup.vertexCount = vertexCount; + wgroup.bones = new Bone[bones.length]; + for (int ii = 0; ii < bones.length; ii++) { + wgroup.bones[ii] = bmap.get(bones[ii]); + } + wgroup.weights = weights; + return wgroup; + } + + private static final long serialVersionUID = 1; + } + + /** Represents a bone that influences the mesh. */ + public static class Bone + implements Serializable + { + /** The node that defines the bone's position. */ + public ModelNode node; + + /** The inverse of the bone's model space reference transform. */ + public transient Matrix4f invRefTransform; + + /** The bone's current transform in model space. */ + public transient Matrix4f transform; + + public Bone (ModelNode node) + { + this.node = node; + transform = new Matrix4f(); + } + + /** + * Rebinds this bone for a prototype instance. + * + * @param pnodes a mapping from prototype nodes to instance nodes + */ + public Bone rebind (HashMap pnodes) + { + Bone bone = new Bone((ModelNode)pnodes.get(node)); + bone.invRefTransform = invRefTransform; + bone.transform = new Matrix4f(); + return bone; + } + + /** + * Initializes the bone's transient state. + */ + private void readObject (ObjectInputStream in) + throws IOException, ClassNotFoundException + { + in.defaultReadObject(); + transform = new Matrix4f(); + } + + private static final long serialVersionUID = 1; + } + + /** + * No-arg constructor for deserialization. + */ + public SkinMesh () + { + } + + /** + * Creates an empty mesh. + */ + public SkinMesh (String name) + { + super(name); + } + + /** + * Sets the array of weight groups that determine how bones affect + * each vertex. + */ + public void setWeightGroups (WeightGroup[] weightGroups) + { + _weightGroups = weightGroups; + + // compile a list of all referenced bones + HashSet bones = new HashSet(); + for (WeightGroup group : weightGroups) { + Collections.addAll(bones, group.bones); + } + _bones = bones.toArray(new Bone[bones.size()]); + } + + @Override // documentation inherited + public void reconstruct ( + ByteBuffer vertices, ByteBuffer normals, ByteBuffer colors, + ByteBuffer textures, ByteBuffer indices) + { + super.reconstruct(vertices, normals, colors, textures, indices); + + // store the current buffers as the originals + storeOriginalBuffers(); + + // initialize the quantized frame table + _frames = new HashIntMap(); + } + + @Override // documentation inherited + public void centerVertices () + { + super.centerVertices(); + storeOriginalBuffers(); + } + + @Override // documentation inherited + public Spatial putClone (Spatial store, Model.CloneCreator properties) + { + SkinMesh mstore = (SkinMesh)properties.originalToCopy.get(this); + if (mstore != null) { + return mstore; + } else if (store == null) { + mstore = new SkinMesh(getName()); + } else { + mstore = (SkinMesh)store; + } + properties.removeProperty("vertices"); + properties.removeProperty("normals"); + properties.removeProperty("displaylistid"); + super.putClone(mstore, properties); + properties.addProperty("vertices"); + properties.addProperty("normals"); + properties.addProperty("displaylistid"); + mstore._frames = _frames; + mstore._useDisplayLists = _useDisplayLists; + mstore._invRefTransform = _invRefTransform; + mstore._bones = new Bone[_bones.length]; + HashMap bmap = new HashMap(); + for (int ii = 0; ii < _bones.length; ii++) { + bmap.put(_bones[ii], mstore._bones[ii] = + _bones[ii].rebind(properties.originalToCopy)); + } + mstore._weightGroups = new WeightGroup[_weightGroups.length]; + for (int ii = 0; ii < _weightGroups.length; ii++) { + mstore._weightGroups[ii] = _weightGroups[ii].rebind(bmap); + } + mstore._ovbuf = _ovbuf; + mstore._onbuf = _onbuf; + mstore._vbuf = new float[_vbuf.length]; + mstore._nbuf = new float[_nbuf.length]; + return mstore; + } + + @Override // documentation inherited + public void writeExternal (ObjectOutput out) + throws IOException + { + super.writeExternal(out); + out.writeObject(_weightGroups); + } + + @Override // documentation inherited + public void readExternal (ObjectInput in) + throws IOException, ClassNotFoundException + { + super.readExternal(in); + setWeightGroups((WeightGroup[])in.readObject()); + } + + @Override // documentation inherited + public void expandModelBounds () + { + BoundingVolume obound = + (BoundingVolume)getBatch(0).getModelBound().clone(null); + updateModelBound(); + getBatch(0).getModelBound().mergeLocal(obound); + } + + @Override // documentation inherited + public void setReferenceTransforms () + { + _invRefTransform = new Matrix4f(); + if (parent instanceof ModelNode) { + Matrix4f transform = new Matrix4f(); + ModelNode.setTransform(getLocalTranslation(), getLocalRotation(), + getLocalScale(), transform); + ((ModelNode)parent).getModelTransform().mult(transform, + _invRefTransform); + _invRefTransform.invertLocal(); + } + for (Bone bone : _bones) { + bone.invRefTransform = + _invRefTransform.mult(bone.node.getModelTransform()).invert(); + } + } + + @Override // documentation inherited + public void lockStaticMeshes ( + Renderer renderer, boolean useVBOs, boolean useDisplayLists) + { + // we can use VBOs for color, texture, and indices + if (useVBOs && renderer.supportsVBO()) { + VBOInfo vboinfo = new VBOInfo(false); + vboinfo.setVBOColorEnabled(true); + vboinfo.setVBOTextureEnabled(true); + vboinfo.setVBOIndexEnabled(true); + setVBOInfo(vboinfo); + } + _useDisplayLists = useDisplayLists; + } + + @Override // documentation inherited + public void storeMeshFrame (int frameId, boolean blend) + { + _storeFrameId = frameId; + _storeBlend = blend; + } + + @Override // documentation inherited + public void setMeshFrame (int frameId) + { + TriangleBatch batch = getBatch(0), + tbatch = (TriangleBatch)_frames.get(frameId); + if (batch instanceof SharedBatch) { + ((SharedBatch)batch).setTarget(tbatch); + } else { + clearBatches(); + addBatch(new SharedBatch(tbatch)); + getBatch(0).updateRenderState(); + } + } + + @Override // documentation inherited + public void blendMeshFrames (int frameId1, int frameId2, float alpha) + { + BlendFrame frame1 = (BlendFrame)_frames.get(frameId1), + frame2 = (BlendFrame)_frames.get(frameId2); + frame1.blend(frame2, alpha, _vbuf, _nbuf); + FloatBuffer vbuf = getVertexBuffer(0), nbuf = getNormalBuffer(0); + vbuf.rewind(); + vbuf.put(_vbuf); + nbuf.rewind(); + nbuf.put(_nbuf); + } + + @Override // documentation inherited + public void updateWorldData (float time) + { + super.updateWorldData(time); + if (_weightGroups == null || _storeFrameId == -1) { + return; + } + // update the bone transforms + for (Bone bone : _bones) { + _invRefTransform.mult(bone.node.getModelTransform(), + bone.transform); + bone.transform.multLocal(bone.invRefTransform); + } + + // deform the mesh according to the positions of the bones (this code + // is ugly as sin because it's optimized at a low level) + Bone[] bones; + int vertexCount, jj, kk, ww; + float[] weights; + Matrix4f m; + float weight, ovx, ovy, ovz, onx, ony, onz, vx, vy, vz, nx, ny, nz; + for (int ii = 0, bidx = 0; ii < _weightGroups.length; ii++) { + vertexCount = _weightGroups[ii].vertexCount; + bones = _weightGroups[ii].bones; + weights = _weightGroups[ii].weights; + for (jj = 0, ww = 0; jj < vertexCount; jj++) { + ovx = _ovbuf[bidx]; + ovy = _ovbuf[bidx + 1]; + ovz = _ovbuf[bidx + 2]; + onx = _onbuf[bidx]; + ony = _onbuf[bidx + 1]; + onz = _onbuf[bidx + 2]; + vx = vy = vz = 0f; + nx = ny = nz = 0f; + for (kk = 0; kk < bones.length; kk++) { + m = bones[kk].transform; + weight = weights[ww++]; + + vx += (ovx*m.m00 + ovy*m.m01 + ovz*m.m02 + m.m03) * weight; + vy += (ovx*m.m10 + ovy*m.m11 + ovz*m.m12 + m.m13) * weight; + vz += (ovx*m.m20 + ovy*m.m21 + ovz*m.m22 + m.m23) * weight; + + nx += (onx*m.m00 + ony*m.m01 + onz*m.m02) * weight; + ny += (onx*m.m10 + ony*m.m11 + onz*m.m12) * weight; + nz += (onx*m.m20 + ony*m.m21 + onz*m.m22) * weight; + } + _vbuf[bidx] = vx; + _vbuf[bidx + 1] = vy; + _vbuf[bidx + 2] = vz; + _nbuf[bidx++] = nx; + _nbuf[bidx++] = ny; + _nbuf[bidx++] = nz; + } + } + + // if skinning in real time, copy the data from arrays to buffers; + // otherwise, store the mesh as an animation frame + if (_storeFrameId == 0) { + FloatBuffer vbuf = getVertexBuffer(0), nbuf = getNormalBuffer(0); + vbuf.rewind(); + vbuf.put(_vbuf); + nbuf.rewind(); + nbuf.put(_nbuf); + } else { + storeFrame(); + _storeFrameId = -1; + } + } + + /** + * Stores the current frame data for later use. + */ + protected void storeFrame () + { + if (_storeBlend) { + _frames.put(_storeFrameId, new BlendFrame( + (float[])_vbuf.clone(), (float[])_nbuf.clone())); + } else { + TriangleBatch batch = getBatch(0), tbatch = new TriangleBatch(); + tbatch.setParentGeom(DUMMY_MESH); + tbatch.setColorBuffer(batch.getColorBuffer()); + tbatch.setTextureBuffer(batch.getTextureBuffer(0), 0); + tbatch.setIndexBuffer(batch.getIndexBuffer()); + tbatch.setVertexBuffer(BufferUtils.createFloatBuffer(_vbuf)); + tbatch.setNormalBuffer(BufferUtils.createFloatBuffer(_nbuf)); + VBOInfo ovboinfo = batch.getVBOInfo(); + if (ovboinfo != null) { + VBOInfo vboinfo = new VBOInfo(true); + vboinfo.setVBOIndexEnabled(true); + vboinfo.setVBOColorID(ovboinfo.getVBOColorID()); + vboinfo.setVBOTextureID(0, ovboinfo.getVBOTextureID(0)); + vboinfo.setVBOIndexID(ovboinfo.getVBOIndexID()); + tbatch.setVBOInfo(vboinfo); + } else if (_useDisplayLists) { + tbatch.lockMeshes( + DisplaySystem.getDisplaySystem().getRenderer()); + } + _frames.put(_storeFrameId, tbatch); + } + } + + /** + * Stores the current vertex and normal buffers for later deformation. + */ + protected void storeOriginalBuffers () + { + FloatBuffer vbuf = getVertexBuffer(0), nbuf = getNormalBuffer(0); + vbuf.rewind(); + nbuf.rewind(); + FloatBuffer.wrap(_ovbuf = new float[vbuf.capacity()]).put(vbuf); + FloatBuffer.wrap(_onbuf = new float[nbuf.capacity()]).put(nbuf); + _vbuf = new float[_ovbuf.length]; + _nbuf = new float[_onbuf.length]; + } + + /** A stored frame used for linear blending. */ + protected static class BlendFrame + { + /** The skinned vertex and normal values. */ + public float[] vbuf, nbuf; + + public BlendFrame (float[] vbuf, float[] nbuf) + { + this.vbuf = vbuf; + this.nbuf = nbuf; + } + + public void blend ( + BlendFrame next, float alpha, float[] rvbuf, float[] rnbuf) + { + float[] nvbuf = next.vbuf, nnbuf = next.nbuf; + float ialpha = 1f - alpha; + for (int ii = 0, nn = vbuf.length; ii < nn; ii++) { + rvbuf[ii] = vbuf[ii] * ialpha + nvbuf[ii] * alpha; + rnbuf[ii] = nbuf[ii] * ialpha + nnbuf[ii] * alpha; + } + } + } + + /** Pre-skinned {@link TriangleBatch}es or {@link BlendFrame}s shared + * between all instances corresponding to frame ids from + * {@link #storeAnimationFrame}. */ + protected HashIntMap _frames; + + /** Whether or to use display lists if VBOs are unavailable for quantized + * meshes. */ + protected boolean _useDisplayLists; + + /** The inverse of the model space reference transform. */ + protected Matrix4f _invRefTransform; + + /** The groups of vertices influenced by different sets of bones. */ + protected WeightGroup[] _weightGroups; + + /** The bones referenced by the weight groups. */ + protected Bone[] _bones; + + /** The original (undeformed) vertex and normal buffers and the deformed + * versions. */ + protected float[] _ovbuf, _onbuf, _vbuf, _nbuf; + + /** The frame id to store on the next update. If 0, don't store any frame + * and skin the mesh as normal. If -1, a frame has been stored and thus + * skinning should only take place when further frames are requested. */ + protected int _storeFrameId; + + /** Whether or not the stored frame id will be used for blending. */ + protected boolean _storeBlend; + + /** A dummy mesh that simply hold transformation values. */ + protected static final TriMesh DUMMY_MESH = new TriMesh(); + + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/jme/model/TextureProvider.java b/src/java/com/threerings/jme/model/TextureProvider.java new file mode 100644 index 00000000..9734f7ad --- /dev/null +++ b/src/java/com/threerings/jme/model/TextureProvider.java @@ -0,0 +1,39 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.model; + +import com.jme.scene.state.TextureState; + +/** + * Provides a means for models to resolve their texture references. + */ +public interface TextureProvider +{ + /** + * Returns a texture state containing the named texture. + * + * @param name the name of the texture, which will be interpreted as an + * absolute resource path if it starts with a forward slash; otherwise, + * as a relative resource path + */ + public TextureState getTexture (String name); +} diff --git a/src/java/com/threerings/jme/package.html b/src/java/com/threerings/jme/package.html new file mode 100644 index 00000000..e6df5fa9 --- /dev/null +++ b/src/java/com/threerings/jme/package.html @@ -0,0 +1,15 @@ + + + + + + + + + Defines extensions to jMonkeyEngine + an OpenGL-based 3D rendering engine. An application framework is + provided that integrates jME with the Presents distributed object + system and other useful bits and bobs are also included. + + + diff --git a/src/java/com/threerings/jme/sprite/BallisticPath.java b/src/java/com/threerings/jme/sprite/BallisticPath.java new file mode 100644 index 00000000..51ad7b83 --- /dev/null +++ b/src/java/com/threerings/jme/sprite/BallisticPath.java @@ -0,0 +1,92 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.sprite; + +import com.jme.math.FastMath; +import com.jme.math.Vector3f; + +/** + * Moves a sprite ballistically. + */ +public class BallisticPath extends Path +{ + /** Gravity: it's the law. */ + public static final float G = -9.8f; + + /** + * Moves the supplied sprite from the starting coordinate (which will + * be modified) using the starting velocity, under the specified + * acceleration. + */ + public BallisticPath (Sprite sprite, Vector3f start, Vector3f velocity, + Vector3f accel, float duration) + { + super(sprite); + _curpos = start; + _velocity = velocity; + _accel = accel; + _duration = duration; + } + + // documentation inherited + public void update (float time) + { + // adjust the position + _velocity.mult(time, _temp); + _curpos.addLocal(_temp); + _sprite.setLocalTranslation(_curpos); + + // check to see if we're done + _accum += time; + if (_accum >= _duration) { + _sprite.pathCompleted(); + } else { + // adjust our velocity due to acceleration + _accel.mult(time, _temp); + _velocity.addLocal(_temp); + } + } + + /** + * Computes and returns the angle of elevation needed to launch a + * ballistic projectile at the specified velocity and have it travel + * the specified range (when it will once again reach the launch + * elevation). + */ + public static float computeElevation (float range, float vel, float accel) + { + return FastMath.asin(accel * range / (vel * vel)) / 2; + } + + /** + * Computes and returns the flight time of a projectile launched at an + * angle previously computed with {@link #computeElevation}.. + */ + public static float computeFlightTime (float range, float vel, float angle) + { + return range / (vel * FastMath.cos(angle)); + } + + protected Vector3f _curpos, _velocity, _accel; + protected float _duration, _accum; + protected Vector3f _temp = new Vector3f(0, 0, 0); +} diff --git a/src/java/com/threerings/jme/sprite/LinePath.java b/src/java/com/threerings/jme/sprite/LinePath.java new file mode 100644 index 00000000..dc973cd4 --- /dev/null +++ b/src/java/com/threerings/jme/sprite/LinePath.java @@ -0,0 +1,60 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.sprite; + +import com.jme.math.Vector3f; + +/** + * Moves a sprite along a straight line. + */ +public class LinePath extends Path +{ + /** + * Moves the supplied sprite from the starting coordinate to the + * specified destination coordinate in the specified time. + */ + public LinePath (Sprite sprite, Vector3f start, Vector3f finish, + float duration) + { + super(sprite); + _start = start; + _finish = finish; + _duration = duration; + } + + // documentation inherited + public void update (float time) + { + _accum += time; + if (_accum >= _duration) { + _sprite.setLocalTranslation(_finish); + _sprite.pathCompleted(); + } else { + _temp.interpolate(_start, _finish, _accum / _duration); + _sprite.setLocalTranslation(_temp); + } + } + + protected Vector3f _start, _finish; + protected float _duration, _accum; + protected Vector3f _temp = new Vector3f(0, 0, 0); +} diff --git a/src/java/com/threerings/jme/sprite/LineSegmentPath.java b/src/java/com/threerings/jme/sprite/LineSegmentPath.java new file mode 100644 index 00000000..9d62f4f9 --- /dev/null +++ b/src/java/com/threerings/jme/sprite/LineSegmentPath.java @@ -0,0 +1,103 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.sprite; + +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; + +/** + * Moves a sprite along a series of straight lines. + */ +public class LineSegmentPath extends Path +{ + /** + * Creates a path for the supplied sprite traversing the supplied + * series of points with the specified duration between points. + * + * @param points a list of points between which the sprite will be + * moved linearly (the sprite will be moved immediately to the first + * point). + * @param durations defines the elapsed time between each successive + * traversal. This will as a result be shorter by one element than the + * points array. + */ + public LineSegmentPath (Sprite sprite, Vector3f up, Vector3f orient, + Vector3f[] points, float[] durations) + { + super(sprite); + _up = up; + _orient = orient; + _points = points; + _durations = durations; + updateRotation(); + } + + // documentation inherited + public void update (float time) + { + // note the accumulated time + _accum += time; + + // if we have surpassed the time for this segment, subtract the + // segment time and move on to the next segment + while (_current < _durations.length && _accum > _durations[_current]) { + _accum -= _durations[_current]; + advance(); + } + + // if we have completed our path, move the sprite to the final + // position and wrap everything up + if (_current >= _durations.length) { + _sprite.setLocalTranslation(_points[_points.length-1]); + _sprite.pathCompleted(); + return; + } + + // move the sprite to the appropriate position between points + _temp.interpolate(_points[_current], _points[_current+1], + _accum / _durations[_current]); + _sprite.setLocalTranslation(_temp); + } + + protected void advance () + { + _current++; + if (_current < _points.length-1) { + updateRotation(); + } + } + + protected void updateRotation () + { + _points[_current+1].subtract(_points[_current], _temp); + PathUtil.computeRotation(_up, _orient, _temp, _rotate); + _sprite.getLocalRotation().set(_rotate); + } + + protected Vector3f _up, _orient; + protected Vector3f[] _points; + protected float[] _durations; + protected float _accum; + protected int _current; + protected Vector3f _temp = new Vector3f(0, 0, 0); + protected Quaternion _rotate = new Quaternion(); +} diff --git a/src/java/com/threerings/jme/sprite/OrientingBallisticPath.java b/src/java/com/threerings/jme/sprite/OrientingBallisticPath.java new file mode 100644 index 00000000..7f85b38d --- /dev/null +++ b/src/java/com/threerings/jme/sprite/OrientingBallisticPath.java @@ -0,0 +1,70 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.sprite; + +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; + +/** + * A ballistic path that orients the sprite toward the velocity vector as + * it traverses the path. + */ +public class OrientingBallisticPath extends BallisticPath +{ + /** + * Creates a {@link BallisticPath} that will rotate the sprite so that + * the supplied orient is aligned with the velocity + * vector at all points along the path. If the provided orientation is + * not initially in line with the starting velocity, the sprite will + * be rotated immediately and then adjusted as it follows the path. + */ + public OrientingBallisticPath ( + Sprite sprite, Vector3f orient, Vector3f start, Vector3f velocity, + Vector3f accel, float duration) + { + super(sprite, start, velocity, accel, duration); + + // TODO: handle orient that is not (1, 0, 0) + _orient = orient; + + // compute the up vector (opposite of acceleration) + _up = accel.negate(); + _up.normalizeLocal(); + + _sprite.setLocalRotation( + PathUtil.computeAxisRotation(_up, velocity, _rotate)); + } + + // documentation inherited + public void update (float time) + { + // do the normal update + super.update(time); + + _sprite.setLocalRotation( + PathUtil.computeAxisRotation(_up, _velocity, _rotate)); + } + + protected Vector3f _orient; + protected Vector3f _up; + protected Quaternion _rotate = new Quaternion(); +} diff --git a/src/java/com/threerings/jme/sprite/Path.java b/src/java/com/threerings/jme/sprite/Path.java new file mode 100644 index 00000000..3693d0f5 --- /dev/null +++ b/src/java/com/threerings/jme/sprite/Path.java @@ -0,0 +1,51 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.sprite; + +import com.jme.scene.Controller; + +/** + * Defines a framework for moving sprites around and notifying interested + * parties when the sprite has completed its path or if the path has been + * cancelled. + */ +public abstract class Path extends Controller +{ + /** + * Creates and initializes this path with the sprite it will be + * manipulating. + */ + protected Path (Sprite sprite) + { + _sprite = sprite; + } + + /** + * Called when this path is removed from its sprite (either due to + * completion or cancellation). + */ + public void wasRemoved () + { + } + + protected Sprite _sprite; +} diff --git a/src/java/com/threerings/jme/sprite/PathObserver.java b/src/java/com/threerings/jme/sprite/PathObserver.java new file mode 100644 index 00000000..597bb22e --- /dev/null +++ b/src/java/com/threerings/jme/sprite/PathObserver.java @@ -0,0 +1,44 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.sprite; + +/** + * Implement this interface to find out when a sprite completes or cancels + * its path. + */ +public interface PathObserver extends SpriteObserver +{ + /** + * Called when a sprite's path is cancelled either because a new path + * was started or the path was explicitly cancelled with {@link + * Sprite#cancelMove}. + */ + public void pathCancelled (Sprite sprite, Path path); + + /** + * Called when a sprite completes its traversal of a path. + * + * @param sprite the sprite that completed its path. + * @param path the path that was completed. + */ + public void pathCompleted (Sprite sprite, Path path); +} diff --git a/src/java/com/threerings/jme/sprite/PathUtil.java b/src/java/com/threerings/jme/sprite/PathUtil.java new file mode 100644 index 00000000..7512377d --- /dev/null +++ b/src/java/com/threerings/jme/sprite/PathUtil.java @@ -0,0 +1,95 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.sprite; + +import com.jme.math.FastMath; +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; + +/** + * Path related utility functions. + */ +public class PathUtil +{ + /** + * Computes a rotation to align the X axis with the specified + * orientation and stores it in the provided target. The provided up + * vector is crossed with the new orientation to find the left vector + * and the left vector is recrossed with the orientation to find the + * correct up vector. + * + * @return the supplied target quaternion. + */ + public static Quaternion computeAxisRotation ( + Vector3f up, Vector3f orient, Quaternion target) + { + _axes[0].set(orient); + _axes[0].normalizeLocal(); + up.cross(_axes[0], _axes[1]); + _axes[1].normalizeLocal(); + _axes[0].cross(_axes[1], _axes[2]); + target.fromAxes(_axes); + return target; + } + + /** + * Computes a rotation from the one vector to another. + * + * @param axis will be used as the axis of rotation if the two vectors + * are parallel. + */ + public static Quaternion computeRotation ( + Vector3f axis, Vector3f from, Vector3f to, Quaternion target) + { + float angle = computeAngle(from, to); + if (angle == FastMath.PI) { // opposite + target.fromAngleAxis(angle, axis); + } else if (angle == 0) { // coincident + target.x = target.y = target.z = 0; + target.w = 1; + } else { + from.cross(to, _axis); + target.fromAngleAxis(angle, _axis); + } + return target; + } + + /** + * Computes the angle between two arbitrary vectors. + */ + public static float computeAngle (Vector3f one, Vector3f two) + { + return FastMath.acos(one.dot(two) / (one.length() * two.length())); + } + + /** + * Computes the angle between two normalized vectors. + */ + public static float computeAngleNormal (Vector3f one, Vector3f two) + { + return FastMath.acos(one.dot(two)); + } + + protected static Vector3f[] _axes = { + new Vector3f(), new Vector3f(), new Vector3f() }; + protected static Vector3f _axis = new Vector3f(); +} diff --git a/src/java/com/threerings/jme/sprite/Sprite.java b/src/java/com/threerings/jme/sprite/Sprite.java new file mode 100644 index 00000000..8261cf2f --- /dev/null +++ b/src/java/com/threerings/jme/sprite/Sprite.java @@ -0,0 +1,244 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.sprite; + +import com.jme.scene.Node; +import com.jme.scene.Spatial; + +import com.samskivert.util.ObserverList; + +import com.threerings.jme.Log; + +/** + * Represents a visual entity that one controls as a single unit. Sprites + * can be made to follow paths which is one of their primary reasons for + * existence. + */ +public class Sprite extends Node +{ + /** + * Walks down the hierarchy provided setting the animation speed on + * any controllers found along the way. + */ + public static void setAnimationSpeed (Spatial spatial, float speed) + { + for (int ii = 0; ii < spatial.getControllers().size(); ii++) { + spatial.getController(ii).setSpeed(speed); + } + if (spatial instanceof Node) { + Node node = (Node)spatial; + for (int ii = 0; ii < node.getQuantity(); ii++) { + setAnimationSpeed(node.getChild(ii), speed); + } + } + } + + /** + * Walks down the hierarchy provided turning on or off any controllers + * found along the way. + */ + public static void setAnimationActive (Spatial spatial, boolean active) + { + for (int ii = 0; ii < spatial.getControllers().size(); ii++) { + spatial.getController(ii).setActive(active); + } + if (spatial instanceof Node) { + Node node = (Node)spatial; + for (int ii = 0; ii < node.getQuantity(); ii++) { + setAnimationActive(node.getChild(ii), active); + } + } + } + + /** + * Walks down the hierarchy provided setting the animation repeat type on + * any controllers found along the way. + */ + public static void setAnimationRepeatType (Spatial spatial, int repeatType) + { + for (int ii = 0; ii < spatial.getControllers().size(); ii++) { + spatial.getController(ii).setRepeatType(repeatType); + } + if (spatial instanceof Node) { + Node node = (Node)spatial; + for (int ii = 0; ii < node.getQuantity(); ii++) { + setAnimationRepeatType(node.getChild(ii), repeatType); + } + } + } + + public Sprite () + { + super(""); + setName("sprite:" + hashCode()); + } + + /** + * Adds an observer to this sprite. Observers are notified when path + * related events take place. + */ + public void addObserver (SpriteObserver obs) + { + if (_observers == null) { + _observers = new ObserverList( + ObserverList.SAFE_IN_ORDER_NOTIFY); + } + _observers.add(obs); + } + + /** + * Removes the specified observer from this sprite. + */ + public void removeObserver (SpriteObserver obs) + { + if (_observers != null) { + _observers.remove(obs); + } + } + + /** + * Returns true if this sprite is moving along a path, false if not. + */ + public boolean isMoving () + { + return _path != null; + } + + /** + * Instructs this sprite to move along the specified path. Any + * currently executing path will be cancelled. + */ + public void move (Path path) + { + // if there's a previous path, let it know that it's going away + cancelMove(); + + // save off this path + _path = path; + addController(_path); + } + + /** + * Cancels any currently executing path. Any registered observers will + * be notified of the cancellation. + */ + public void cancelMove () + { + if (_path != null) { + Path oldpath = _path; + _path = null; + oldpath.wasRemoved(); + if (_observers != null) { + _observers.apply(new CancelledOp(this, oldpath)); + } + } + } + + /** + * Called by the active path when it has completed. Note: + * don't call this method unless you are implementing a {@link Path}. + */ + public void pathCompleted () + { + if (_path == null) { + Log.warning("pathCompleted() called on pathless sprite " + + "(re-completed?) [sprite=" + this + "]."); + Thread.dumpStack(); + return; + } + + Path oldpath = _path; + _path = null; + removeController(oldpath); + oldpath.wasRemoved(); + if (_observers != null) { + _observers.apply(new CompletedOp(this, oldpath)); + } + } + + /** + * Configures the speed of all controllers in our model hierarchy. + */ + public void setAnimationSpeed (float speed) + { + setAnimationSpeed(this, speed); + } + + /** + * Configures the active state of all controllers in our model hierarchy. + */ + public void setAnimationActive (boolean active) + { + setAnimationActive(this, active); + } + + /** + * Configures the repeat type of all controllers in our model hierarchy. + */ + public void setAnimationRepeatType (int repeatType) + { + setAnimationRepeatType(this, repeatType); + } + + /** Used to dispatch {@link PathObserver#pathCancelled}. */ + protected static class CancelledOp + implements ObserverList.ObserverOp + { + public CancelledOp (Sprite sprite, Path path) { + _sprite = sprite; + _path = path; + } + + public boolean apply (SpriteObserver observer) { + if (observer instanceof PathObserver) { + ((PathObserver)observer).pathCancelled(_sprite, _path); + } + return true; + } + + protected Sprite _sprite; + protected Path _path; + } + + /** Used to dispatch {@link PathObserver#pathCompleted}. */ + protected static class CompletedOp + implements ObserverList.ObserverOp + { + public CompletedOp (Sprite sprite, Path path) { + _sprite = sprite; + _path = path; + } + + public boolean apply (SpriteObserver observer) { + if (observer instanceof PathObserver) { + ((PathObserver)observer).pathCompleted(_sprite, _path); + } + return true; + } + + protected Sprite _sprite; + protected Path _path; + } + + protected ObserverList _observers; + protected Path _path; +} diff --git a/src/java/com/threerings/jme/sprite/SpriteObserver.java b/src/java/com/threerings/jme/sprite/SpriteObserver.java new file mode 100644 index 00000000..4b1249e7 --- /dev/null +++ b/src/java/com/threerings/jme/sprite/SpriteObserver.java @@ -0,0 +1,29 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.sprite; + +/** + * The super-interface for all sprite observers. + */ +public interface SpriteObserver +{ +} diff --git a/src/java/com/threerings/jme/tile/FringeConfiguration.java b/src/java/com/threerings/jme/tile/FringeConfiguration.java new file mode 100644 index 00000000..ba7ca2ee --- /dev/null +++ b/src/java/com/threerings/jme/tile/FringeConfiguration.java @@ -0,0 +1,160 @@ +// +// $Id: FringeConfiguration.java 3403 2005-03-14 23:58:02Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tile; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; + +import com.samskivert.util.StringUtil; + +import com.threerings.jme.Log; + +/** + * Used to manage data about which tiles fringe on which others and how + * they fringe. + */ +public class FringeConfiguration implements Serializable +{ + /** Contains information on a type of tile and all of the fringe + * records associated with it. */ + public static class TileRecord implements Serializable + { + /** The type of tile to which this applies. */ + public String type; + + /** The fringe priority of this type. */ + public int priority; + + /** A list of the fringe records that can be used for fringing. */ + public ArrayList fringes = new ArrayList(); + + /** Used when parsing from an XML definition. */ + public void addFringe (FringeRecord record) + { + if (record.isValid()) { + fringes.add(record); + } else { + Log.warning("Not adding invalid fringe record [tile=" + this + + ", fringe=" + record + "]."); + } + } + + /** Did everything parse well? */ + public boolean isValid () + { + return ((type != null) && (priority > 0)); + } + + /** Generates a string representation of this instance. */ + public String toString () + { + return "[type=" + type + ", priority=" + priority + + ", fringes=" + StringUtil.toString(fringes) + "]"; + } + + /** Increase this value when object's serialized state is impacted + * by a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; + } + + /** Used to parse the type fringe definitions. */ + public static class FringeRecord implements Serializable + { + /** The name of the fringe tileset image. */ + public String name; + + /** Is this a mask? */ + public boolean mask; + + /** Did everything parse well? */ + public boolean isValid () + { + return (name != null); + } + + /** Generates a string representation of this instance. */ + public String toString () + { + return "[name=" + name + ", mask=" + mask + "]"; + } + + /** Increase this value when object's serialized state is impacted + * by a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; + } + + /** + * Adds a parsed fringe record to this instance. This is used when + * parsing the fringe records from xml. + */ + public void addTileRecord (TileRecord record) + { + if (record.isValid()) { + _trecs.put(record.type, record); + } else { + Log.warning("Refusing to add invalid tile record " + + "[tile=" + record + "]."); + } + } + + /** + * If the first type fringes upon the second, return the fringe + * priority of the first type, otherwise return -1. + */ + public int fringesOn (String fringer, String fringed) + { + TileRecord f1 = (TileRecord)_trecs.get(fringer); + + // we better have a fringe record for the fringer + if (null != f1) { + // it had better have some types defined + if (f1.fringes.size() > 0) { + TileRecord f2 = (TileRecord)_trecs.get(fringed); + // and we only fringe if fringed doesn't have a fringe + // record or has a lower priority + if ((null == f2) || (f1.priority > f2.priority)) { + return f1.priority; + } + } + } + + return -1; + } + + /** + * Get a random FringeRecord from amongst the ones listed for the + * specified type. + */ + public FringeRecord getFringe (String type, int hashValue) + { + TileRecord t = (TileRecord)_trecs.get(type); + return (FringeRecord)t.fringes.get(hashValue % t.fringes.size()); + } + + /** The mapping from tile type to tile record. */ + protected HashMap _trecs = new HashMap(); + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/jme/tile/TileFringer.java b/src/java/com/threerings/jme/tile/TileFringer.java new file mode 100644 index 00000000..6ddb7521 --- /dev/null +++ b/src/java/com/threerings/jme/tile/TileFringer.java @@ -0,0 +1,404 @@ +// +// $Id: TileFringer.java 3177 2004-10-28 17:49:02Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tile; + +import java.awt.Graphics2D; +import java.awt.Transparency; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.HashMap; + +import com.samskivert.util.QuickSort; + +import com.threerings.media.image.ImageUtil; +import com.threerings.media.tile.TileUtil; + +import com.threerings.jme.Log; + +/** + * Computes fringe tile images according to the rules in an associated + * fringe configuration. + */ +public class TileFringer +{ + public static interface TileSource + { + /** Returns the type of tile at the specified coordinates or -1 if + * there is no tile at this coordinate. */ + public String getTileType (int x, int y); + + /** Returns the tile type to use when a coordinate has no tile. */ + public String getDefaultType (); + } + + public static interface ImageSource extends ImageUtil.ImageCreator + { + /** Creates a blank image into which various fringe images will be + * composited. */ + public BufferedImage createImage ( + int width, int height, int transparency); + + /** Returns the source image for a tile of the specified type. + * This can be randomly selected and change from call to call. */ + public BufferedImage getTileSource (String type); + + /** Returns the named fringe source image (one long strip). */ + public BufferedImage getFringeSource (String name); + } + + /** + * Creates a fringer that will fringe according to the rules in the + * supplied configuration. + */ + public TileFringer (FringeConfiguration config, ImageSource isrc) + { + _config = config; + _isrc = isrc; + } + + /** + * Computes, creates and returns the base tile with the appropriate + * fringe imagery applied to it for the specified location. + * + * @param masks used to cache intermediate images of tiles cut out + * using a fringe mask. + */ + public BufferedImage getFringeTile ( + TileSource tiles, int col, int row, HashMap masks) + { + // get the type of the tile we are considering + String baseType = tiles.getTileType(col, row); + if (baseType == null) { + baseType = tiles.getDefaultType(); + } + + // start with an empty fringer list + FringerRec fringers = null; + + // walk through our influence tiles + for (int y = row - 1, maxy = row + 2; y < maxy; y++) { + for (int x = col - 1, maxx = col + 2; x < maxx; x++) { + // we sensibly do not consider ourselves + if ((x == col) && (y == row)) { + continue; + } + + // determine the type of our fringing neighbor + String fringerType = tiles.getTileType(x, y); + if (fringerType == null) { + fringerType = tiles.getDefaultType(); + } + + // determine if it fringes on our tile + int pri = _config.fringesOn(fringerType, baseType); + if (pri == -1) { + continue; + } + + FringerRec fringer = (fringers == null) ? + null : fringers.find(fringerType); + if (fringer == null) { + fringer = fringers = + new FringerRec(fringerType, pri, fringers); + } + + // now turn on the appropriate fringebits + int dy = y - row, dx = x - col; + fringer.bits |= FLAGMATRIX[dy*3+dx+4]; + } + } + + // if nothing fringed, we're done + if (fringers == null) { + return null; + } + + // otherwise compose a fringe tile from the specified fringes + return composeFringeTile( + baseType, fringers.toArray(), masks, TileUtil.getTileHash(col, row)); + } + + /** + * Compose a fringe tile out of the various fringe images needed. + */ + protected BufferedImage composeFringeTile ( + String baseType, FringerRec[] fringers, HashMap masks, int hashValue) + { + // sort the array so that higher priority fringers get drawn first + QuickSort.sort(fringers); + + BufferedImage source = _isrc.getTileSource(baseType); + if (source == null) { + Log.warning("Missing source tile [type=" + baseType + "]."); + return null; + } + + BufferedImage ftimg = _isrc.createImage( + source.getWidth(), source.getHeight(), Transparency.OPAQUE); + Graphics2D gfx = (Graphics2D)ftimg.getGraphics(); + try { + // start with the base tile image + gfx.drawImage(source, 0, 0, null); + + // and stamp the fringers on top of it + for (int ii = 0; ii < fringers.length; ii++) { + int[] indexes = getFringeIndexes(fringers[ii].bits); + for (int jj = 0; jj < indexes.length; jj++) { + stampTileImage(gfx, fringers[ii].fringerType, + indexes[jj], masks, hashValue); + } + } + + } finally { + gfx.dispose(); + } + return ftimg; + } + + /** + * Looks up or creates the appropriate fringe mask and draws it into + * the supplied graphics context. + */ + protected void stampTileImage ( + Graphics2D gfx, String fringerType, int index, + HashMap masks, int hashValue) + { + FringeConfiguration.FringeRecord frec = + _config.getFringe(fringerType, hashValue); + BufferedImage fsimg = (frec == null) ? null : + _isrc.getFringeSource(frec.name); + if (fsimg == null) { + Log.warning("Missing fringe source image [type=" + fringerType + + ", hash=" + hashValue + ", frec=" + frec + "]."); + return; + } + + if (frec.mask) { + // it's a mask; look for it in the cache + String maskkey = fringerType + ":" + frec.name + ":" + index; + BufferedImage mimg = (BufferedImage)masks.get(maskkey); + if (mimg == null) { + BufferedImage fsrc = getSubimage(fsimg, index); + BufferedImage bsrc = _isrc.getTileSource(fringerType); + mimg = ImageUtil.composeMaskedImage(_isrc, fsrc, bsrc); + masks.put(maskkey, mimg); + } + gfx.drawImage(mimg, 0, 0, null); + + } else { + // this is a non-mask image so just use the data from the + // fringe source image directly + gfx.drawImage(getSubimage(fsimg, index), 0, 0, null); + } + } + + /** + * Returns the indexth tile image from the supplied + * source image. The source image is assumed to be a single strip of + * tile images, each with equal width and height. + */ + protected BufferedImage getSubimage (BufferedImage source, int index) + { + int size = source.getHeight(), x = size * index; + return source.getSubimage(x, 0, size, size); + } + + /** + * Get the fringe index specified by the fringebits. If no index + * is available, try breaking down the bits into contiguous regions of + * bits and look for indexes for those. + */ + protected int[] getFringeIndexes (int bits) + { + int index = BITS_TO_INDEX[bits]; + if (index != -1) { + int[] ret = new int[1]; + ret[0] = index; + return ret; + } + + // otherwise, split the bits into contiguous components + + // look for a zero and start our first split + int start = 0; + while ((((1 << start) & bits) != 0) && (start < NUM_FRINGEBITS)) { + start++; + } + + if (start == NUM_FRINGEBITS) { + // we never found an empty fringebit, and since index (above) + // was already -1, we have no fringe tile for these bits.. sad. + return new int[0]; + } + + ArrayList indexes = new ArrayList(); + int weebits = 0; + for (int ii=(start + 1) % NUM_FRINGEBITS; ii != start; + ii = (ii + 1) % NUM_FRINGEBITS) { + + if (((1 << ii) & bits) != 0) { + weebits |= (1 << ii); + } else if (weebits != 0) { + index = BITS_TO_INDEX[weebits]; + if (index != -1) { + indexes.add(Integer.valueOf(index)); + } + weebits = 0; + } + } + if (weebits != 0) { + index = BITS_TO_INDEX[weebits]; + if (index != -1) { + indexes.add(Integer.valueOf(index)); + } + } + + int[] ret = new int[indexes.size()]; + for (int ii=0; ii < ret.length; ii++) { + ret[ii] = ((Integer) indexes.get(ii)).intValue(); + } + return ret; + } + + /** A record for holding information about a particular fringe as + * we're computing what it will look like. */ + protected static class FringerRec implements Comparable + { + public String fringerType; + public int priority; + public int bits; + public FringerRec next; + + public FringerRec (String type, int pri, FringerRec next) { + fringerType = type; + priority = pri; + this.next = next; + } + + public FringerRec find (String type) + { + if (fringerType.equals(type)) { + return this; + } else if (next != null) { + return next.find(type); + } else { + return null; + } + } + + public FringerRec[] toArray () + { + return toArray(0); + } + + public int compareTo (Object o) { + return priority - ((FringerRec) o).priority; + } + + public String toString () { + return "[type=" + fringerType + ", pri=" + priority + + ", bits=" + Integer.toString(bits, 16) + "]"; + } + + protected FringerRec[] toArray (int index) + { + FringerRec[] array; + if (next == null) { + array = new FringerRec[index+1]; + } else { + array = next.toArray(index+1); + } + array[index] = this; + return array; + } + } + + protected static final int NORTH = 1 << 0; + protected static final int NORTHEAST = 1 << 1; + protected static final int EAST = 1 << 2; + protected static final int SOUTHEAST = 1 << 3; + protected static final int SOUTH = 1 << 4; + protected static final int SOUTHWEST = 1 << 5; + protected static final int WEST = 1 << 6; + protected static final int NORTHWEST = 1 << 7; + + protected static final int NUM_FRINGEBITS = 8; + + /** A matrix mapping adjacent tiles to which fringe bits they affect. + * (x and y are offset by +1, since we can't have -1 as an array index). + * These are "upside down" thanks to OpenGL. */ + protected static final int[] FLAGMATRIX = { + SOUTHWEST, (SOUTHEAST | SOUTH | SOUTHWEST), SOUTHEAST, + (NORTHWEST | WEST | SOUTHWEST), 0, (NORTHEAST | EAST | SOUTHEAST), + NORTHWEST, (NORTHWEST | NORTH | NORTHEAST), NORTHEAST, + }; + + /** The fringe tiles we use. These are the 17 possible tiles made up + * of continuous fringebits sections. */ + protected static final int[] FRINGETILES = { + SOUTHEAST, + SOUTHWEST | SOUTH | SOUTHEAST, + SOUTHWEST, + NORTHEAST | EAST | SOUTHEAST, + NORTHWEST | WEST | SOUTHWEST, + NORTHEAST, + NORTHWEST | NORTH | NORTHEAST, + NORTHWEST, + + SOUTHWEST | WEST | NORTHWEST | NORTH | NORTHEAST, + NORTHWEST | NORTH | NORTHEAST | EAST | SOUTHEAST, + NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST, + SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST, + + NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST, + SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST, + SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST, + NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST, + + // all the directions! + NORTH | NORTHEAST | EAST | SOUTHEAST | SOUTH | SOUTHWEST | + WEST | NORTHWEST + }; + + /** A reverse map of the {@link #FRINGETILES} array, for quickly + * looking up which tile we want. */ + protected static final int[] BITS_TO_INDEX; + + // Construct the BITS_TO_INDEX array. + static { + int num = (1 << NUM_FRINGEBITS); + BITS_TO_INDEX = new int[num]; + + // first clear everything to -1 (meaning there is no tile defined) + for (int ii=0; ii < num; ii++) { + BITS_TO_INDEX[ii] = -1; + } + + // then fill in with the defined tiles. + for (int ii=0; ii < FRINGETILES.length; ii++) { + BITS_TO_INDEX[FRINGETILES[ii]] = ii; + } + } + + protected ImageSource _isrc; + protected FringeConfiguration _config; +} diff --git a/src/java/com/threerings/jme/tile/tools/CompileFringeConfigurationTask.java b/src/java/com/threerings/jme/tile/tools/CompileFringeConfigurationTask.java new file mode 100644 index 00000000..6e220862 --- /dev/null +++ b/src/java/com/threerings/jme/tile/tools/CompileFringeConfigurationTask.java @@ -0,0 +1,76 @@ +// +// $Id: CompileFringeConfigurationTask.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tile.tools; + +import java.io.File; +import java.io.Serializable; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Task; + +import com.samskivert.io.PersistenceException; +import com.threerings.util.CompiledConfig; + +import com.threerings.jme.tile.tools.xml.FringeConfigurationParser; + +/** + * Compiles a fringe configuration from XML format into binary format. + */ +public class CompileFringeConfigurationTask extends Task +{ + public void setConfig (File config) + { + _config = config; + } + + public void setTarget (File target) + { + _target = target; + } + + public void execute () throws BuildException + { + // make sure the config file exists + if (!_config.exists()) { + throw new BuildException( + "Fringe configuration file not found [path=" + _config + "]."); + } + + FringeConfigurationParser parser = new FringeConfigurationParser(); + Serializable config; + try { + config = parser.parseConfig(_config); + } catch (Exception e) { + throw new BuildException("Failure parsing fringe config", e); + } + + try { + // and write it on out + CompiledConfig.saveConfig(_target, config); + } catch (Exception e) { + throw new BuildException("Failure writing serialized config", e); + } + } + + protected File _config; + protected File _target; +} diff --git a/src/java/com/threerings/jme/tile/tools/xml/FringeConfigurationParser.java b/src/java/com/threerings/jme/tile/tools/xml/FringeConfigurationParser.java new file mode 100644 index 00000000..a769ef5c --- /dev/null +++ b/src/java/com/threerings/jme/tile/tools/xml/FringeConfigurationParser.java @@ -0,0 +1,84 @@ +// +// $Id: FringeConfigurationParser.java 3254 2004-11-30 20:03:47Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tile.tools.xml; + +import java.io.Serializable; + +import org.apache.commons.digester.Digester; + +import com.samskivert.util.StringUtil; +import com.samskivert.xml.SetPropertyFieldsRule; + +import com.threerings.tools.xml.CompiledConfigParser; + +import com.threerings.jme.Log; +import com.threerings.jme.tile.FringeConfiguration.TileRecord; +import com.threerings.jme.tile.FringeConfiguration.FringeRecord; +import com.threerings.jme.tile.FringeConfiguration; + +/** + * Parses fringe config definitions, which look like so (with angle + * brackets instead of square): + *
+ * [fringe]
+ *   [tile type="water" priority="100"]
+ *      [fringe name="water_fringe_1"/]
+ *      [fringe name="water_fringe_2"/]
+ *      [fringe name="water_fringe_3"/]
+ *   [/tile]
+ *   [tile type="dirt" priority="10"]
+ *      [fringe name="dirt_fringe_1" mask="true"/]
+ *   [/tile]
+ *   [tile type="cobble" priority="100"]
+ *      [fringe name="cobble_fringe_1" mask="false"/]
+ *   [/tile]
+ * [/fringe]
+ * 
+ */ +public class FringeConfigurationParser extends CompiledConfigParser +{ + // documentation inherited + protected Serializable createConfigObject () + { + return new FringeConfiguration(); + } + + // documentation inherited + protected void addRules (Digester digest) + { + // configure top-level constraints + String prefix = "fringe"; + digest.addRule(prefix, new SetPropertyFieldsRule()); + + // create and configure fringe config instances + prefix += "/tile"; + digest.addObjectCreate(prefix, TileRecord.class.getName()); + digest.addRule(prefix, new SetPropertyFieldsRule()); + digest.addSetNext(prefix, "addTileRecord"); + + // create the fringe type records in each tile record + prefix += "/fringe"; + digest.addObjectCreate(prefix, FringeRecord.class.getName()); + digest.addRule(prefix, new SetPropertyFieldsRule()); + digest.addSetNext(prefix, "addFringe"); + } +} diff --git a/src/java/com/threerings/jme/tools/AnimationDef.java b/src/java/com/threerings/jme/tools/AnimationDef.java new file mode 100644 index 00000000..709e9117 --- /dev/null +++ b/src/java/com/threerings/jme/tools/AnimationDef.java @@ -0,0 +1,165 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tools; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Properties; + +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; +import com.jme.scene.Controller; +import com.jme.scene.Spatial; + +import com.threerings.jme.Log; +import com.threerings.jme.model.Model; + +/** + * A basic representation for keyframe animations. + */ +public class AnimationDef +{ + /** The rate of the animation in frames per second. */ + public int frameRate; + + /** A single frame of the animation. */ + public static class FrameDef + { + /** Transform for affected nodes. */ + public ArrayList transforms = + new ArrayList(); + + public void addTransform (TransformDef transform) + { + transforms.add(transform); + } + + /** Adds all transform targets in this frame to the supplied set. */ + public void addTransformTargets ( + HashMap nodes, HashSet targets) + { + for (int ii = 0, nn = transforms.size(); ii < nn; ii++) { + String name = transforms.get(ii).name; + Spatial target = nodes.get(name); + if (target != null) { + targets.add(target); + } else { + Log.debug("Missing animation target [name=" + name + + "]."); + } + } + } + + /** Returns the array of transforms for this frame. */ + public Model.Transform[] getTransforms (Spatial[] targets) + { + Model.Transform[] mtransforms = + new Model.Transform[targets.length]; + for (int ii = 0; ii < targets.length; ii++) { + mtransforms[ii] = getTransform(targets[ii]); + } + return mtransforms; + } + + /** Returns the transform for the supplied target. */ + protected Model.Transform getTransform (Spatial target) + { + String name = target.getName(); + for (int ii = 0, nn = transforms.size(); ii < nn; ii++) { + TransformDef transform = transforms.get(ii); + if (name.equals(transform.name)) { + return transform.getTransform(); + } + } + return null; + } + } + + /** A transform for a single node. */ + public static class TransformDef + { + /** The name of the affected node. */ + public String name; + + /** The transformation parameters. */ + public float[] translation; + public float[] rotation; + public float[] scale; + + /** Returns the live transform object. */ + public Model.Transform getTransform () + { + return new Model.Transform( + new Vector3f(translation[0], translation[1], translation[2]), + new Quaternion(rotation[0], rotation[1], rotation[2], + rotation[3]), + new Vector3f(scale[0], scale[1], scale[2])); + } + } + + /** The individual frames of the animation. */ + public ArrayList frames = new ArrayList(); + + public void addFrame (FrameDef frame) + { + frames.add(frame); + } + + /** + * Creates the "live" animation object that will be serialized with the + * object. + * + * @param props the animation properties + * @param nodes the nodes in the model, mapped by name + */ + public Model.Animation createAnimation ( + Properties props, HashMap nodes) + { + // find all affected nodes + HashSet targets = new HashSet(); + for (int ii = 0, nn = frames.size(); ii < nn; ii++) { + frames.get(ii).addTransformTargets(nodes, targets); + } + + // create and configure the animation + Model.Animation anim = new Model.Animation(); + anim.frameRate = frameRate; + String rtype = props.getProperty("repeat_type", "clamp"); + if (rtype.equals("cycle")) { + anim.repeatType = Controller.RT_CYCLE; + } else if (rtype.equals("wrap")) { + anim.repeatType = Controller.RT_WRAP; + } else { + anim.repeatType = Controller.RT_CLAMP; + } + + // collect all transforms + anim.transformTargets = targets.toArray(new Spatial[targets.size()]); + anim.transforms = new Model.Transform[frames.size()][targets.size()]; + for (int ii = 0; ii < anim.transforms.length; ii++) { + anim.transforms[ii] = + frames.get(ii).getTransforms(anim.transformTargets); + } + return anim; + } +} diff --git a/src/java/com/threerings/jme/tools/BuildSphereMap.java b/src/java/com/threerings/jme/tools/BuildSphereMap.java new file mode 100644 index 00000000..a147893c --- /dev/null +++ b/src/java/com/threerings/jme/tools/BuildSphereMap.java @@ -0,0 +1,167 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tools; + +import java.awt.image.BufferedImage; + +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import com.jme.image.Texture; +import com.jme.math.FastMath; +import com.jme.math.Vector3f; + +/** + * A tool for converting cube maps (sky boxes) to sphere maps. + */ +public class BuildSphereMap +{ + public static void main (String[] args) + { + if (args.length < 7) { + System.err.println("Usage: BuildSphereMap front.ext back.ext " + + "left.ext right.ext up.ext dest.ext size"); + System.exit(-1); + } + + try { + execute(new File(args[0]), new File(args[1]), new File(args[2]), + new File(args[3]), new File(args[4]), new File(args[5]), + Integer.parseInt(args[6])); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Builds the sphere map. + * + * @param front the file containing the front side of the cube map + * @param back the file containing the back side of the cube map + * @param left the file containing the left side of the cube map + * @param right the file containing the right side of the cube map + * @param up the file containing the up side of the cube map + * @param target the file to contain the sphere map + * @param size the size of the sphere map to generate + */ + public static void execute (File front, File back, File left, File right, + File up, File target, int size) + throws IOException + { + // load up the sides of the cube map + BufferedImage[] sides = new BufferedImage[5]; + sides[FRONT] = ImageIO.read(front); + sides[BACK] = ImageIO.read(back); + sides[LEFT] = ImageIO.read(left); + sides[RIGHT] = ImageIO.read(right); + sides[UP] = ImageIO.read(up); + + // compute the pixels + int[] rgb = new int[size * size]; + Vector3f vec = new Vector3f(); + for (int y = 0, idx = 0; y < size; y++) { + for (int x = 0; x < size; x++, idx++) { + float vx = x / (size*0.5f) - 1f, vy = y / (size*0.5f) - 1f, + d2 = vx*vx + vy*vy; + int p = 0; + if (d2 <= 1f) { + vec.set(vx, vy, FastMath.sqrt(1f - d2)); + rgb[idx] = getCubeMapPixel(vec, sides); + } + } + } + + // create and write the image + BufferedImage image = new BufferedImage(size, size, + BufferedImage.TYPE_INT_RGB); + image.setRGB(0, 0, size, size, rgb, 0, size); + String dest = target.toString(), + ext = dest.substring(dest.lastIndexOf('.')+1); + ImageIO.write(image, ext, target); + } + + /** + * Returns the pixel from the cube map to which the given vector points. + */ + protected static int getCubeMapPixel (Vector3f vec, BufferedImage[] sides) + { + int side = getCubeMapSide(vec); + + float s, t; + switch (side) { + case FRONT: + s = -vec.x / vec.y; + t = vec.z / vec.y; + break; + case BACK: + s = -vec.x / vec.y; + t = -vec.z / vec.y; + break; + case LEFT: + s = vec.y / vec.x; + t = -vec.z / vec.x; + break; + case RIGHT: + s = vec.y / vec.x; + t = vec.z / vec.x; + break; + default: + case UP: + s = vec.x / vec.z; + t = -vec.y / vec.z; + break; + } + int width = sides[side].getWidth(), height = sides[side].getHeight(); + return sides[side].getRGB((int)((width-1) * (s+1f)/2f), + (int)((height-1) * (1f-t)/2f)); + } + + /** + * Returns the side index identifying the face of the cube map to which + * the given vector points. + */ + protected static int getCubeMapSide (Vector3f vec) + { + if (vec.x > vec.z && vec.x > vec.y && vec.x > -vec.y) { + return RIGHT; + + } else if (vec.x < -vec.z && vec.x < -vec.y && vec.x < vec.y) { + return LEFT; + + } else if (vec.y > vec.z) { + return FRONT; + + } else if (vec.y < -vec.z) { + return BACK; + + } else { + return UP; + } + } + + /** The sides of the cube map. */ + protected static final int FRONT = 0, BACK = 1, LEFT = 2, RIGHT = 3, + UP = 4; +} diff --git a/src/java/com/threerings/jme/tools/BuildSphereMapTask.java b/src/java/com/threerings/jme/tools/BuildSphereMapTask.java new file mode 100644 index 00000000..27ad058a --- /dev/null +++ b/src/java/com/threerings/jme/tools/BuildSphereMapTask.java @@ -0,0 +1,86 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tools; + +import java.io.File; +import java.io.IOException; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Task; + +/** + * An ant task for converting cube maps (sky boxes) to sphere maps. + */ +public class BuildSphereMapTask extends Task +{ + public void setFront (File front) + { + _front = front; + } + + public void setBack (File back) + { + _back = back; + } + + public void setLeft (File left) + { + _left = left; + } + + public void setRight (File right) + { + _right = right; + } + + public void setUp (File up) + { + _up = up; + } + + public void setTarget (File target) + { + _target = target; + } + + public void setSize (int size) + { + _size = size; + } + + public void execute () throws BuildException + { + try { + BuildSphereMap.execute(_front, _back, _left, _right, _up, _target, + _size); + + } catch (IOException e) { + throw new BuildException("Failure building sphere map", e); + } + } + + /** The files representing the sides of the cube map and the target. */ + protected File _front, _back, _left, _right, _up, _target; + + /** The size of the target image. */ + protected int _size; +} diff --git a/src/java/com/threerings/jme/tools/CompileModel.java b/src/java/com/threerings/jme/tools/CompileModel.java new file mode 100644 index 00000000..8e5536ea --- /dev/null +++ b/src/java/com/threerings/jme/tools/CompileModel.java @@ -0,0 +1,139 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tools; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Properties; +import java.util.logging.Level; + +import com.jme.scene.Spatial; +import com.jme.util.LoggingSystem; +import com.jmex.model.XMLparser.Converters.DummyDisplaySystem; + +import com.samskivert.util.PropertiesUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.jme.model.Model; +import com.threerings.jme.model.ModelMesh; +import com.threerings.jme.model.ModelNode; +import com.threerings.jme.model.SkinMesh; +import com.threerings.jme.tools.xml.AnimationParser; +import com.threerings.jme.tools.xml.ModelParser; + +/** + * An application for compiling 3D models defined in XML to fast-loading binary + * files. + */ +public class CompileModel +{ + /** + * Loads the model described by the given properties file and compiles it + * to a .dat file in the same directory. + * + * @return the loaded model, or null if the compiled version + * is up-to-date + */ + public static Model compile (File source) + throws Exception + { + String spath = source.toString(); + int didx = spath.lastIndexOf('.'); + String root = (didx == -1) ? spath : spath.substring(0, didx); + File content = new File(root + ".mxml"), + target = new File(root + ".dat"); + boolean needsUpdate = false; + if (source.lastModified() >= target.lastModified() || + content.lastModified() >= target.lastModified()) { + needsUpdate = true; + } + + // load the model properties + Properties props = new Properties(); + FileInputStream in = new FileInputStream(source); + props.load(in); + in.close(); + + // locate the animations, if any + String[] anims = + StringUtil.parseStringArray(props.getProperty("animations", "")); + File[] afiles = new File[anims.length]; + File dir = source.getParentFile(); + for (int ii = 0; ii < anims.length; ii++) { + afiles[ii] = new File(dir, anims[ii] + ".mxml"); + if (afiles[ii].lastModified() >= target.lastModified()) { + needsUpdate = true; + } + } + if (!needsUpdate) { + return null; + } + System.out.println("Compiling " + source.getParent() + "..."); + + // load the model content + ModelDef mdef = _mparser.parseModel(content.toString()); + HashMap nodes = new HashMap(); + Model model = mdef.createModel(props, nodes); + model.initPrototype(); + + // load the animations, if any + for (int ii = 0; ii < anims.length; ii++) { + System.out.println(" Adding " + afiles[ii] + "..."); + AnimationDef adef = _aparser.parseAnimation(afiles[ii].toString()); + model.addAnimation(anims[ii], adef.createAnimation( + PropertiesUtil.getSubProperties(props, anims[ii]), nodes)); + } + + // write and return the model + model.writeToFile(target); + return model; + } + + public static void main (String[] args) + { + if (args.length < 1) { + System.err.println("Usage: CompileModel source.properties"); + System.exit(-1); + } + // create a dummy display system + new DummyDisplaySystem(); + LoggingSystem.getLogger().setLevel(Level.WARNING); + + try { + compile(new File(args[0])); + } catch (Exception e) { + System.err.println("Error compiling model: " + e); + } + } + + /** A parser for the model definitions. */ + protected static ModelParser _mparser = new ModelParser(); + + /** A parser for the animation definitions. */ + protected static AnimationParser _aparser = new AnimationParser(); +} diff --git a/src/java/com/threerings/jme/tools/CompileModelTask.java b/src/java/com/threerings/jme/tools/CompileModelTask.java new file mode 100644 index 00000000..2e16bc5b --- /dev/null +++ b/src/java/com/threerings/jme/tools/CompileModelTask.java @@ -0,0 +1,77 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tools; + +import java.io.File; + +import java.util.ArrayList; +import java.util.logging.Level; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.FileSet; + +import com.jme.util.LoggingSystem; +import com.jmex.model.XMLparser.Converters.DummyDisplaySystem; + +/** + * An ant task for compiling 3D models defined in XML to fast-loading binary + * files. + */ +public class CompileModelTask extends Task +{ + public void addFileset (FileSet set) + { + _filesets.add(set); + } + + public void init () throws BuildException + { + // create a dummy display system + new DummyDisplaySystem(); + LoggingSystem.getLogger().setLevel(Level.WARNING); + } + + public void execute () + throws BuildException + { + for (int ii = 0, nn = _filesets.size(); ii < nn; ii++) { + FileSet fs = _filesets.get(ii); + DirectoryScanner ds = fs.getDirectoryScanner(getProject()); + File fromDir = fs.getDir(getProject()); + String[] srcFiles = ds.getIncludedFiles(); + + for (int f = 0; f < srcFiles.length; f++) { + File source = new File(fromDir, srcFiles[f]); + try { + CompileModel.compile(source); + } catch (Exception e) { + System.err.println("Error compiling " + source + ": " + e); + } + } + } + } + + /** A list of filesets that contain XML models. */ + protected ArrayList _filesets = new ArrayList(); +} diff --git a/src/java/com/threerings/jme/tools/ConvertModel.java b/src/java/com/threerings/jme/tools/ConvertModel.java new file mode 100644 index 00000000..20e9895f --- /dev/null +++ b/src/java/com/threerings/jme/tools/ConvertModel.java @@ -0,0 +1,101 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tools; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; + +import com.jmex.model.XMLparser.Converters.AseToJme; +import com.jmex.model.XMLparser.Converters.DummyDisplaySystem; +import com.jmex.model.XMLparser.Converters.FormatConverter; +import com.jmex.model.XMLparser.Converters.MaxToJme; +import com.jmex.model.XMLparser.Converters.Md2ToJme; +import com.jmex.model.XMLparser.Converters.Md3ToJme; +import com.jmex.model.XMLparser.Converters.ObjToJme; + +/** + * A tool for converting various 3D model formats into JME's internal + * format. + */ +public class ConvertModel +{ + public static void main (String[] args) + { + if (args.length < 2) { + System.err.println("Usage: ConvertModel source.ext dest.jme"); + System.exit(-1); + } + + // create a dummy display system which the converters need + new DummyDisplaySystem(); + + ConvertModel app = new ConvertModel(); + File source = new File(args[0]); + File target = new File(args[1]); + + String path = source.getPath().toLowerCase(); + String type = path.substring(path.lastIndexOf(".") + 1); + + // set up our converter + FormatConverter convert = null; + if (type.equals("obj")) { + convert = new ObjToJme(); + try { + convert.setProperty("mtllib", new URL("file:" + source)); + } catch (Exception e) { + System.err.println("Failed to create material URL: " + e); + System.exit(-1); + } + } else if (type.equals("3ds")) { + convert = new MaxToJme(); + } else if (type.equals("md2")) { + convert = new Md2ToJme(); + } else if (type.equals("md3")) { + convert = new Md3ToJme(); + } else if (type.equals("ase")) { + convert = new AseToJme(); + } else { + System.err.println("Unknown model type '" + type + "'."); + System.exit(-1); + } + + // and do the deed + try { + BufferedOutputStream bout = new BufferedOutputStream( + new FileOutputStream(target)); + BufferedInputStream bin = new BufferedInputStream( + new FileInputStream(source)); + convert.convert(bin, bout); + bout.close(); + } catch (IOException ioe) { + System.err.println("Error converting '" + source + + "' to '" + target + "'."); + ioe.printStackTrace(System.err); + System.exit(-1); + } + } +} diff --git a/src/java/com/threerings/jme/tools/ConvertModelTask.java b/src/java/com/threerings/jme/tools/ConvertModelTask.java new file mode 100644 index 00000000..f59bc9ad --- /dev/null +++ b/src/java/com/threerings/jme/tools/ConvertModelTask.java @@ -0,0 +1,135 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tools; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.logging.Level; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.FileSet; + +import com.jme.util.LoggingSystem; +import com.jmex.model.XMLparser.Converters.AseToJme; +import com.jmex.model.XMLparser.Converters.DummyDisplaySystem; +import com.jmex.model.XMLparser.Converters.FormatConverter; +import com.jmex.model.XMLparser.Converters.MaxToJme; +import com.jmex.model.XMLparser.Converters.Md2ToJme; +import com.jmex.model.XMLparser.Converters.Md3ToJme; +import com.jmex.model.XMLparser.Converters.ObjToJme; + +/** + * An ant task for converting various 3D model formats into JME's internal + * format. + */ +public class ConvertModelTask extends Task +{ + public void addFileset (FileSet set) + { + _filesets.add(set); + } + + public void init () throws BuildException + { + // create a dummy display system which the converters need + new DummyDisplaySystem(); + LoggingSystem.getLogger().setLevel(Level.WARNING); + } + + public void execute () throws BuildException + { + for (int i = 0; i < _filesets.size(); i++) { + FileSet fs = (FileSet)_filesets.get(i); + DirectoryScanner ds = fs.getDirectoryScanner(getProject()); + File fromDir = fs.getDir(getProject()); + String[] srcFiles = ds.getIncludedFiles(); + + for (int f = 0; f < srcFiles.length; f++) { + File cfile = new File(fromDir, srcFiles[f]); + String target = srcFiles[f]; + int didx = target.lastIndexOf("."); + target = (didx == -1) ? target : target.substring(0, didx); + target += ".jme"; + convertModel(cfile, new File(fromDir, target)); + } + } + } + + protected void convertModel (File source, File target) + { + if (source.lastModified() < target.lastModified()) { + return; + } + + System.out.println("Converting " + source + "..."); + String path = source.getPath().toLowerCase(); + String type = path.substring(path.lastIndexOf(".") + 1); + + // set up our converter + FormatConverter convert = null; + if (type.equals("obj")) { + convert = new ObjToJme(); + try { + convert.setProperty("mtllib", new URL("file:" + source)); + } catch (Exception e) { + System.err.println("Failed to create material URL: " + e); + return; + } + } else if (type.equals("3ds")) { + convert = new MaxToJme(); + } else if (type.equals("md2")) { + convert = new Md2ToJme(); + } else if (type.equals("md3")) { + convert = new Md3ToJme(); + } else if (type.equals("ase")) { + convert = new AseToJme(); + } else { + System.err.println("Unknown model type '" + type + "'."); + return; + } + + // and do the deed + try { + BufferedOutputStream bout = new BufferedOutputStream( + new FileOutputStream(target)); + BufferedInputStream bin = new BufferedInputStream( + new FileInputStream(source)); + convert.convert(bin, bout); + bout.close(); + } catch (IOException ioe) { + System.err.println("Error converting '" + source + + "' to '" + target + "'."); + ioe.printStackTrace(System.err); + } + } + + /** A list of filesets that contain tileset bundle definitions. */ + protected ArrayList _filesets = new ArrayList(); +} diff --git a/src/java/com/threerings/jme/tools/ModelDef.java b/src/java/com/threerings/jme/tools/ModelDef.java new file mode 100644 index 00000000..84f2f3fd --- /dev/null +++ b/src/java/com/threerings/jme/tools/ModelDef.java @@ -0,0 +1,573 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tools; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import com.jme.bounding.BoundingBox; +import com.jme.bounding.BoundingSphere; +import com.jme.math.FastMath; +import com.jme.scene.Spatial; +import com.jme.util.geom.BufferUtils; + +import com.samskivert.util.PropertiesUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.jme.Log; +import com.threerings.jme.model.Model; +import com.threerings.jme.model.ModelController; +import com.threerings.jme.model.ModelMesh; +import com.threerings.jme.model.ModelNode; +import com.threerings.jme.model.SkinMesh; + +/** + * An intermediate representation for models used to store data parsed from + * XML and convert it into JME nodes. + */ +public class ModelDef +{ + /** The base class of nodes in the model. */ + public abstract static class SpatialDef + { + /** The node's name. */ + public String name; + + /** The name of the node's parent. */ + public String parent; + + /** The node's transformation. */ + public float[] translation; + public float[] rotation; + public float[] scale; + + /** Returns a JME node for this definition. */ + public Spatial getSpatial (Properties props) + { + if (_spatial == null) { + _spatial = createSpatial(new NodeProperties(props, name)); + setTransform(); + } + return _spatial; + } + + /** Sets the transform of the created node. */ + protected void setTransform () + { + _spatial.getLocalTranslation().set(translation[0], translation[1], + translation[2]); + _spatial.getLocalRotation().set(rotation[0], rotation[1], + rotation[2], rotation[3]); + _spatial.getLocalScale().set(scale[0], scale[1], scale[2]); + } + + /** Creates a JME node for this definition. */ + public abstract Spatial createSpatial (Properties props); + + /** Resolves any name references using the supplied map. */ + public void resolveReferences ( + HashMap nodes, HashSet referenced) + { + Spatial pnode = nodes.get(parent); + if (pnode instanceof ModelNode) { + ((ModelNode)pnode).attachChild(_spatial); + + } else if (parent != null) { + Log.warning("Missing or invalid parent node [spatial=" + + name + ", parent=" + parent + "]."); + } + } + + /** The JME node created for this definition. */ + protected Spatial _spatial; + } + + /** A rigid triangle mesh. */ + public static class TriMeshDef extends SpatialDef + { + /** The geometry offset transform. */ + public float[] offsetTranslation; + public float[] offsetRotation; + public float[] offsetScale; + + /** Whether or not the mesh allows back face culling. */ + public boolean solid; + + /** The texture of the mesh, if any. */ + public String texture; + + /** Whether or not the mesh is (partially) transparent. */ + public boolean transparent; + + /** The vertices of the mesh. */ + public ArrayList vertices = new ArrayList(); + + /** The triangle indices. */ + public ArrayList indices = new ArrayList(); + + /** Whether or not any of the vertices have texture coordinates. */ + public boolean tcoords; + + public void addVertex (Vertex vertex) + { + int idx = vertices.indexOf(vertex); + if (idx != -1) { + indices.add(idx); + } else { + indices.add(vertices.size()); + vertices.add(vertex); + } + tcoords = tcoords || vertex.tcoords != null; + } + + // documentation inherited + public Spatial createSpatial (Properties props) + { + ModelNode node = new ModelNode(name); + if (indices.size() > 0) { + _mesh = createMesh(); + configureMesh(props); + node.attachChild(_mesh); + } + return node; + } + + /** Creates the mesh to attach to the node. */ + protected ModelMesh createMesh () + { + return new ModelMesh("mesh"); + } + + /** Configures the mesh. */ + protected void configureMesh (Properties props) + { + // set the geometry offset + if (offsetTranslation != null) { + _mesh.getLocalTranslation().set(offsetTranslation[0], + offsetTranslation[1], offsetTranslation[2]); + } + if (offsetRotation != null) { + _mesh.getLocalRotation().set(offsetRotation[0], + offsetRotation[1], offsetRotation[2], offsetRotation[3]); + } + if (offsetScale != null) { + _mesh.getLocalScale().set(offsetScale[0], offsetScale[1], + offsetScale[2]); + } + + // make sure texture is just a filename + int sidx = (texture == null) ? -1 : + Math.max(texture.lastIndexOf('/'), texture.lastIndexOf('\\')); + if (sidx != -1) { + texture = texture.substring(sidx + 1); + } + + // configure using properties + _mesh.configure(solid, texture, transparent, props); + + // set the various buffers + int vsize = vertices.size(); + ByteOrder no = ByteOrder.nativeOrder(); + ByteBuffer vbbuf = ByteBuffer.allocateDirect(vsize*3*4).order(no), + nbbuf = ByteBuffer.allocateDirect(vsize*3*4).order(no), + tbbuf = tcoords ? + ByteBuffer.allocateDirect(vsize*2*4).order(no) : null, + ibbuf = ByteBuffer.allocateDirect(indices.size()*4).order(no); + FloatBuffer vbuf = vbbuf.asFloatBuffer(), + nbuf = nbbuf.asFloatBuffer(), + tbuf = tcoords ? tbbuf.asFloatBuffer() : null; + for (int ii = 0; ii < vsize; ii++) { + vertices.get(ii).setInBuffers(vbuf, nbuf, tbuf); + } + IntBuffer ibuf = ibbuf.asIntBuffer(); + for (int ii = 0, nn = indices.size(); ii < nn; ii++) { + ibuf.put(indices.get(ii)); + } + _mesh.reconstruct(vbbuf, nbbuf, null, tbbuf, ibbuf); + + _mesh.setModelBound("sphere".equals(props.getProperty("bound")) ? + new BoundingSphere() : new BoundingBox()); + _mesh.updateModelBound(); + + // set the mesh's origin to the center of its bounding box + _mesh.centerVertices(); + } + + /** The mesh that contains the actual geometry. */ + protected ModelMesh _mesh; + } + + /** A triangle mesh that deforms according to bone positions. */ + public static class SkinMeshDef extends TriMeshDef + { + @Override // documentation inherited + protected ModelMesh createMesh () + { + return new SkinMesh("mesh"); + } + + @Override // documentation inherited + public void resolveReferences ( + HashMap nodes, HashSet referenced) + { + super.resolveReferences(nodes, referenced); + if (_mesh == null) { + return; + } + + // create and set the final weight groups + SkinMesh.WeightGroup[] wgroups = + new SkinMesh.WeightGroup[_groups.size()]; + HashMap bones = + new HashMap(); + int ii = 0; + for (Map.Entry, WeightGroupDef> entry : + _groups.entrySet()) { + SkinMesh.WeightGroup wgroup = new SkinMesh.WeightGroup(); + wgroup.vertexCount = entry.getValue().indices.size(); + wgroup.bones = new SkinMesh.Bone[entry.getKey().size()]; + int jj = 0; + for (String bname : entry.getKey()) { + SkinMesh.Bone bone = bones.get(bname); + if (bone == null) { + Spatial node = nodes.get(bname); + bones.put(bname, + bone = new SkinMesh.Bone((ModelNode)node)); + referenced.add(node); + } + wgroup.bones[jj++] = bone; + } + wgroup.weights = toArray(entry.getValue().weights); + wgroups[ii++] = wgroup; + } + ((SkinMesh)_mesh).setWeightGroups(wgroups); + } + + @Override // documentation inherited + protected void configureMesh (Properties props) + { + // divide the vertices up by weight groups + _groups = new HashMap, WeightGroupDef>(); + for (int ii = 0, nn = vertices.size(); ii < nn; ii++) { + SkinVertex svertex = (SkinVertex)vertices.get(ii); + Set bones = svertex.boneWeights.keySet(); + WeightGroupDef group = _groups.get(bones); + if (group == null) { + _groups.put(bones, group = new WeightGroupDef()); + } + group.indices.add(ii); + for (String bone : bones) { + group.weights.add(svertex.boneWeights.get(bone).weight); + } + } + + // reorder the vertices by group + ArrayList overts = vertices; + vertices = new ArrayList(); + int[] imap = new int[overts.size()]; + for (Map.Entry, WeightGroupDef> entry : + _groups.entrySet()) { + for (int idx : entry.getValue().indices) { + imap[idx] = vertices.size(); + vertices.add(overts.get(idx)); + } + } + for (int ii = 0, nn = indices.size(); ii < nn; ii++) { + indices.set(ii, imap[indices.get(ii)]); + } + + super.configureMesh(props); + } + + /** The intermediate weight groups, mapped by bone names. */ + protected HashMap, WeightGroupDef> _groups; + } + + /** A generic node. */ + public static class NodeDef extends SpatialDef + { + // documentation inherited + public Spatial createSpatial (Properties props) + { + return new ModelNode(name); + } + } + + /** A basic vertex. */ + public static class Vertex + { + public float[] location; + public float[] normal; + public float[] tcoords; + + public void setInBuffers ( + FloatBuffer vbuf, FloatBuffer nbuf, FloatBuffer tbuf) + { + vbuf.put(location); + nbuf.put(normal); + + if (tbuf != null) { + if (tcoords != null) { + tbuf.put(tcoords); + } else { + tbuf.put(0f); + tbuf.put(0f); + } + } + } + + public boolean equals (Object obj) + { + Vertex overt = (Vertex)obj; + return Arrays.equals(location, overt.location) && + Arrays.equals(normal, overt.normal) && + Arrays.equals(tcoords, overt.tcoords); + } + } + + /** A vertex influenced by a number of bones. */ + public static class SkinVertex extends Vertex + { + /** The bones influencing the vertex, mapped by name. */ + public HashMap boneWeights = + new HashMap(); + + public void addBoneWeight (BoneWeight weight) + { + if (weight.weight == 0f) { + return; + } + BoneWeight bweight = boneWeights.get(weight.bone); + if (bweight != null) { + bweight.weight += weight.weight; + } else { + boneWeights.put(weight.bone, weight); + } + } + + /** Finds the bone nodes influencing this vertex. */ + public HashSet getBones (HashMap nodes) + { + HashSet bones = new HashSet(); + for (String bone : boneWeights.keySet()) { + Spatial node = nodes.get(bone); + if (node instanceof ModelNode) { + bones.add((ModelNode)node); + } else { + Log.warning("Missing or invalid bone for bone weight " + + "[bone=" + bone + "]."); + } + } + return bones; + } + + /** Returns the weight of the given bone. */ + public float getWeight (ModelNode bone) + { + BoneWeight bweight = boneWeights.get(bone.getName()); + return (bweight == null) ? 0f : bweight.weight; + } + } + + /** The influence of a bone on a vertex. */ + public static class BoneWeight + { + /** The name of the influencing bone. */ + public String bone; + + /** The amount of influence. */ + public float weight; + } + + /** A group of vertices influenced by the same bone. */ + public static class WeightGroupDef + { + /** The indices of the affected vertex. */ + public ArrayList indices = new ArrayList(); + + /** The interleaved vertex weights. */ + public ArrayList weights = new ArrayList(); + } + + /** The meshes and bones comprising the model. */ + public ArrayList spatials = new ArrayList(); + + public void addSpatial (SpatialDef spatial) + { + // put nodes before meshes so that bones are updated before skin + spatials.add(spatial instanceof NodeDef ? 0 : spatials.size(), + spatial); + } + + /** + * Creates the model node defined herein. + * + * @param props the properties of the model + * @param nodes a node map to populate + */ + public Model createModel (Properties props, HashMap nodes) + { + Model model = new Model(props.getProperty("name", "model"), props); + + // set the overall scale + model.setLocalScale(Float.parseFloat(props.getProperty("scale", "1"))); + + // start by creating the spatials and mapping them to their names + for (int ii = 0, nn = spatials.size(); ii < nn; ii++) { + Spatial spatial = spatials.get(ii).getSpatial(props); + nodes.put(spatial.getName(), spatial); + } + + // then go through again, resolving any name references and attaching + // root children + HashSet referenced = new HashSet(); + for (int ii = 0, nn = spatials.size(); ii < nn; ii++) { + SpatialDef sdef = spatials.get(ii); + sdef.resolveReferences(nodes, referenced); + if (sdef.getSpatial(props).getParent() == null) { + model.attachChild(sdef.getSpatial(props)); + } + } + + // create any controllers listed + String[] controllers = StringUtil.parseStringArray( + props.getProperty("controllers", "")); + for (int ii = 0; ii < controllers.length; ii++) { + Spatial target = nodes.get(controllers[ii]); + if (target == null) { + Log.warning("Missing controller node [name=" + + controllers[ii] + "]."); + continue; + } + ModelController ctrl = createController( + PropertiesUtil.getSubProperties(props, controllers[ii]), + target); + if (ctrl != null) { + model.addController(ctrl); + } + } + + // get rid of any nodes that serve no purpose + pruneUnusedNodes(model, nodes, referenced); + + return model; + } + + /** Creates, configures, and returns a model controller. */ + protected ModelController createController ( + Properties props, Spatial target) + { + // attempt to create an instance of the controller + ModelController ctrl; + String cname = props.getProperty("class", ""); + try { + ctrl = (ModelController)Class.forName(cname).newInstance(); + } catch (Exception e) { + Log.warning("Error instantiating controller [class=" + cname + + ", error=" + e + "]."); + return null; + } + ctrl.configure(props, target); + return ctrl; + } + + /** Recursively removes any unused nodes. */ + protected boolean pruneUnusedNodes ( + ModelNode node, HashMap nodes, + HashSet referenced) + { + boolean hasValidChildren = false; + for (int ii = node.getQuantity() - 1; ii >= 0; ii--) { + Spatial child = node.getChild(ii); + if (!(child instanceof ModelNode) || + pruneUnusedNodes((ModelNode)child, nodes, referenced)) { + hasValidChildren = true; + } else { + node.detachChildAt(ii); + nodes.remove(child.getName()); + } + } + return referenced.contains(node) || hasValidChildren; + } + + /** Converts a boxed Integer list to an unboxed int array. */ + protected static int[] toArray (ArrayList list) + { + int[] array = new int[list.size()]; + for (int ii = 0, nn = list.size(); ii < nn; ii++) { + array[ii] = list.get(ii); + } + return array; + } + + /** Converts a boxed Float list to an unboxed float array. */ + protected static float[] toArray (ArrayList list) + { + float[] array = new float[list.size()]; + for (int ii = 0, nn = list.size(); ii < nn; ii++) { + array[ii] = list.get(ii); + } + return array; + } + + /** A wrapper for the model properties providing access to the properties + * of a node within the model. */ + protected static class NodeProperties extends Properties + { + public NodeProperties (Properties mprops, String name) + { + _mprops = mprops; + _prefix = name + "."; + } + + @Override // documentation inherited + public String getProperty (String key) + { + return getProperty(key, null); + } + + @Override // documentation inherited + public String getProperty (String key, String defaultValue) + { + return _mprops.getProperty(_prefix + key, + _mprops.getProperty(key, defaultValue)); + } + + /** The properties of the model. */ + protected Properties _mprops; + + /** The node prefix. */ + protected String _prefix; + } +} diff --git a/src/java/com/threerings/jme/tools/ModelViewer.java b/src/java/com/threerings/jme/tools/ModelViewer.java new file mode 100644 index 00000000..b4f45dd8 --- /dev/null +++ b/src/java/com/threerings/jme/tools/ModelViewer.java @@ -0,0 +1,1019 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tools; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; + +import java.util.HashMap; +import java.util.logging.Level; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.ButtonModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JRadioButtonMenuItem; +import javax.swing.JSlider; +import javax.swing.KeyStroke; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.filechooser.FileFilter; + +import com.jme.bounding.BoundingBox; +import com.jme.bounding.BoundingVolume; +import com.jme.image.Texture; +import com.jme.light.DirectionalLight; +import com.jme.math.FastMath; +import com.jme.math.Vector3f; +import com.jme.renderer.Camera; +import com.jme.renderer.ColorRGBA; +import com.jme.renderer.Renderer; +import com.jme.scene.Controller; +import com.jme.scene.Line; +import com.jme.scene.Node; +import com.jme.scene.Spatial; +import com.jme.scene.state.LightState; +import com.jme.scene.state.MaterialState; +import com.jme.scene.state.TextureState; +import com.jme.scene.state.WireframeState; +import com.jme.scene.state.ZBufferState; +import com.jme.system.JmeException; +import com.jme.util.LoggingSystem; +import com.jme.util.TextureManager; +import com.jme.util.export.binary.BinaryImporter; +import com.jme.util.geom.Debugger; +import com.jmex.effects.particles.ParticleMesh; + +import com.samskivert.swing.GroupLayout; +import com.samskivert.swing.Spacer; +import com.samskivert.util.Config; +import com.samskivert.util.StringUtil; + +import com.threerings.resource.ResourceManager; +import com.threerings.util.MessageBundle; +import com.threerings.util.MessageManager; + +import com.threerings.jme.JmeCanvasApp; +import com.threerings.jme.Log; +import com.threerings.jme.camera.CameraHandler; +import com.threerings.jme.model.Model; +import com.threerings.jme.model.TextureProvider; + +/** + * A simple viewer application that allows users to examine models and their + * animations by loading them from their uncompiled .properties / + * .mxml representations or their compiled .dat + * representations. + */ +public class ModelViewer extends JmeCanvasApp +{ + public static void main (String[] args) + { + // write standard output and error to a log file + if (System.getProperty("no_log_redir") == null) { + String dlog = "viewer.log"; + try { + PrintStream logOut = new PrintStream( + new FileOutputStream(dlog), true); + System.setOut(logOut); + System.setErr(logOut); + + } catch (IOException ioe) { + Log.warning("Failed to open debug log [path=" + dlog + + ", error=" + ioe + "]."); + } + } + LoggingSystem.getLoggingSystem().setLevel(Level.WARNING); + new ModelViewer(args.length > 0 ? args[0] : null); + } + + /** + * Creates and initializes an instance of the model viewer application. + * + * @param path the path of the model to view, or null for + * none + */ + public ModelViewer (String path) + { + super(1024, 768); + _rsrcmgr = new ResourceManager("rsrc"); + _msg = new MessageManager("rsrc.i18n").getBundle("jme.viewer"); + _path = path; + + JPopupMenu.setDefaultLightWeightPopupEnabled(false); + + _frame = new JFrame(_msg.get("m.title")); + _frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + JMenuBar menu = new JMenuBar(); + _frame.setJMenuBar(menu); + + JMenu file = new JMenu(_msg.get("m.file_menu")); + file.setMnemonic(KeyEvent.VK_F); + menu.add(file); + Action load = new AbstractAction(_msg.get("m.file_load")) { + public void actionPerformed (ActionEvent e) { + showLoadDialog(); + } + }; + load.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_L); + load.putValue(Action.ACCELERATOR_KEY, + KeyStroke.getKeyStroke(KeyEvent.VK_L, KeyEvent.CTRL_MASK)); + file.add(load); + + Action importAction = new AbstractAction(_msg.get("m.file_import")) { + public void actionPerformed (ActionEvent e) { + showImportDialog(); + } + }; + importAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_I); + importAction.putValue(Action.ACCELERATOR_KEY, + KeyStroke.getKeyStroke(KeyEvent.VK_I, KeyEvent.CTRL_MASK)); + file.add(importAction); + + file.addSeparator(); + Action quit = new AbstractAction(_msg.get("m.file_quit")) { + public void actionPerformed (ActionEvent e) { + System.exit(0); + } + }; + quit.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_Q); + quit.putValue(Action.ACCELERATOR_KEY, + KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_MASK)); + file.add(quit); + + JMenu view = new JMenu(_msg.get("m.view_menu")); + view.setMnemonic(KeyEvent.VK_V); + menu.add(view); + + Action wireframe = new AbstractAction(_msg.get("m.view_wireframe")) { + public void actionPerformed (ActionEvent e) { + _wfstate.setEnabled(!_wfstate.isEnabled()); + } + }; + wireframe.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_W); + wireframe.putValue(Action.ACCELERATOR_KEY, + KeyStroke.getKeyStroke(KeyEvent.VK_W, KeyEvent.CTRL_MASK)); + view.add(new JCheckBoxMenuItem(wireframe)); + view.addSeparator(); + + _pivots = new JCheckBoxMenuItem(_msg.get("m.view_pivots")); + _pivots.setMnemonic(KeyEvent.VK_P); + _pivots.setAccelerator( + KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.CTRL_MASK)); + view.add(_pivots); + + _bounds = new JCheckBoxMenuItem(_msg.get("m.view_bounds")); + _bounds.setMnemonic(KeyEvent.VK_B); + _bounds.setAccelerator( + KeyStroke.getKeyStroke(KeyEvent.VK_B, KeyEvent.CTRL_MASK)); + view.add(_bounds); + + _normals = new JCheckBoxMenuItem(_msg.get("m.view_normals")); + _normals.setMnemonic(KeyEvent.VK_N); + _normals.setAccelerator( + KeyStroke.getKeyStroke(KeyEvent.VK_N, KeyEvent.CTRL_MASK)); + view.add(_normals); + view.addSeparator(); + + JMenu amode = new JMenu(_msg.get("m.animation_mode")); + final JRadioButtonMenuItem flipbook = + new JRadioButtonMenuItem(_msg.get("m.mode_flipbook")), + morph = new JRadioButtonMenuItem(_msg.get("m.mode_morph")), + skin = new JRadioButtonMenuItem(_msg.get("m.mode_skin")); + ButtonGroup mgroup = new ButtonGroup() { + public void setSelected (ButtonModel model, boolean b) { + super.setSelected(model, b); + if (b) { + if (flipbook.isSelected()) { + _animMode = Model.AnimationMode.FLIPBOOK; + } else if (morph.isSelected()) { + _animMode = Model.AnimationMode.MORPH; + } else { + _animMode = Model.AnimationMode.SKIN; + } + if (_loaded != null) { + loadModel(_loaded); // reload + } + } + } + }; + mgroup.add(flipbook); + mgroup.add(morph); + mgroup.add(skin); + mgroup.setSelected(skin.getModel(), true); + amode.add(skin); + amode.add(morph); + amode.add(flipbook); + view.add(amode); + view.addSeparator(); + + Action rlight = new AbstractAction(_msg.get("m.view_light")) { + public void actionPerformed (ActionEvent e) { + if (_rldialog == null) { + _rldialog = new RotateLightDialog(); + _rldialog.pack(); + } + _rldialog.setVisible(true); + } + }; + rlight.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R); + rlight.putValue(Action.ACCELERATOR_KEY, + KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_MASK)); + view.add(new JMenuItem(rlight)); + + Action rcamera = new AbstractAction(_msg.get("m.view_recenter")) { + public void actionPerformed (ActionEvent e) { + ((OrbitCameraHandler)_camhand).recenter(); + } + }; + rcamera.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_C); + rcamera.putValue(Action.ACCELERATOR_KEY, + KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.CTRL_MASK)); + view.add(new JMenuItem(rcamera)); + + _frame.getContentPane().add(getCanvas(), BorderLayout.CENTER); + + JPanel bpanel = new JPanel(new BorderLayout()); + _frame.getContentPane().add(bpanel, BorderLayout.SOUTH); + + _animctrls = new JPanel(); + _animctrls.setBorder(BorderFactory.createEtchedBorder()); + bpanel.add(_animctrls, BorderLayout.NORTH); + _animctrls.add(new JLabel(_msg.get("m.anim_select"))); + _animctrls.add(_animbox = new JComboBox()); + _animctrls.add(new JButton( + new AbstractAction(_msg.get("m.anim_start")) { + public void actionPerformed (ActionEvent e) { + String anim = (String)_animbox.getSelectedItem(); + if (_model.hasAnimation(anim)) { + _model.startAnimation(anim); + } else { // it's a sequence + if (_model.getAnimation() != null) { + _sequence = null; + _model.stopAnimation(); + } + _sequence = StringUtil.parseStringArray( + _model.getProperties().getProperty( + anim + ".animations", "")); + if (_sequence.length == 0) { + _sequence = null; + } else { + _model.startAnimation(_sequence[_seqidx = 0]); + } + } + } + })); + _animctrls.add(_animstop = new JButton( + new AbstractAction(_msg.get("m.anim_stop")) { + public void actionPerformed (ActionEvent e) { + _model.stopAnimation(); + } + })); + _animstop.setEnabled(false); + _animctrls.add(new Spacer(50, 1)); + _animctrls.add(new JLabel(_msg.get("m.anim_speed"))); + _animctrls.add(_animspeed = new JSlider(-100, +100, 0)); + _animspeed.setMajorTickSpacing(10); + _animspeed.setSnapToTicks(true); + _animspeed.setPaintTicks(true); + _animspeed.addChangeListener(new ChangeListener() { + public void stateChanged (ChangeEvent e) { + updateAnimationSpeed(); + } + }); + _animctrls.setVisible(false); + + _status = new JLabel(" "); + _status.setHorizontalAlignment(JLabel.LEFT); + _status.setBorder(BorderFactory.createEtchedBorder()); + bpanel.add(_status, BorderLayout.SOUTH); + + _frame.pack(); + _frame.setVisible(true); + + run(); + } + + @Override // documentation inherited + public boolean init () + { + if (!super.init()) { + return false; + } + if (_path != null) { + loadModel(new File(_path)); + } + return true; + } + + @Override // documentation inherited + protected void initDisplay () + throws JmeException + { + super.initDisplay(); + _ctx.getRenderer().setBackgroundColor(ColorRGBA.gray); + } + + @Override // documentation inherited + protected void initInput () + { + super.initInput(); + + _camhand.setTiltLimits(FastMath.PI / 16.0f, FastMath.PI * 7.0f / 16.0f); + _camhand.setZoomLimits(1f, 100f); + _camhand.tiltCamera(-FastMath.PI * 7.0f / 16.0f); + + MouseOrbiter orbiter = new MouseOrbiter(); + _canvas.addMouseListener(orbiter); + _canvas.addMouseMotionListener(orbiter); + _canvas.addMouseWheelListener(orbiter); + } + + @Override // documentation inherited + protected CameraHandler createCameraHandler (Camera camera) + { + return new OrbitCameraHandler(camera); + } + + @Override // documentation inherited + protected void initRoot () + { + super.initRoot(); + + // set default states + MaterialState mstate = _ctx.getRenderer().createMaterialState(); + mstate.getDiffuse().set(ColorRGBA.white); + mstate.getAmbient().set(ColorRGBA.white); + _ctx.getGeometry().setRenderState(mstate); + _ctx.getGeometry().setRenderState( + _wfstate = _ctx.getRenderer().createWireframeState()); + _ctx.getGeometry().setNormalsMode(Spatial.NM_GL_NORMALIZE_PROVIDED); + _wfstate.setEnabled(false); + + // create a grid on the XY plane to provide some reference + Vector3f[] points = new Vector3f[GRID_SIZE*2 + GRID_SIZE*2]; + float halfLength = (GRID_SIZE - 1) * GRID_SPACING / 2; + int idx = 0; + for (int xx = 0; xx < GRID_SIZE; xx++) { + points[idx++] = new Vector3f( + -halfLength + xx*GRID_SPACING, -halfLength, 0f); + points[idx++] = new Vector3f( + -halfLength + xx*GRID_SPACING, +halfLength, 0f); + } + for (int yy = 0; yy < GRID_SIZE; yy++) { + points[idx++] = new Vector3f( + -halfLength, -halfLength + yy*GRID_SPACING, 0f); + points[idx++] = new Vector3f( + +halfLength, -halfLength + yy*GRID_SPACING, 0f); + + } + Line grid = new Line("grid", points, null, null, null); + grid.getBatch(0).getDefaultColor().set(0.25f, 0.25f, 0.25f, 1f); + grid.setLightCombineMode(LightState.OFF); + grid.setModelBound(new BoundingBox()); + grid.updateModelBound(); + _ctx.getGeometry().attachChild(grid); + grid.updateRenderState(); + + // attach a dummy node to draw debugging views + _ctx.getGeometry().attachChild(new Node("debug") { + public void onDraw (Renderer r) { + if (_model == null) { + return; + } + if (_pivots.getState()) { + drawPivots(_model, r); + } + if (_bounds.getState()) { + Debugger.drawBounds(_model, r); + } + if (_normals.getState()) { + Debugger.drawNormals(_model, r, 5f, true); + } + } + }); + } + + /** + * Draws the pivot axes of the given node and its children. + */ + protected void drawPivots (Spatial spatial, Renderer r) + { + if (_axes == null) { + _axes = new Line("axes", + new Vector3f[] { Vector3f.ZERO, Vector3f.UNIT_X, Vector3f.ZERO, + Vector3f.UNIT_Y, Vector3f.ZERO, Vector3f.UNIT_Z }, null, + new ColorRGBA[] { ColorRGBA.red, ColorRGBA.red, + ColorRGBA.green, ColorRGBA.green, ColorRGBA.blue, + ColorRGBA.blue }, null); + _axes.setRenderQueueMode(Renderer.QUEUE_SKIP); + _axes.setRenderState(r.createZBufferState()); + LightState lstate = r.createLightState(); + lstate.setEnabled(false); + _axes.setRenderState(lstate); + _axes.updateRenderState(); + } + _axes.getRenderState(ZBufferState.RS_ZBUFFER).apply(); + _axes.getRenderState(LightState.RS_LIGHT).apply(); + _axes.getWorldTranslation().set(spatial.getWorldTranslation()); + _axes.getWorldRotation().set(spatial.getWorldRotation()); + _axes.draw(r); + + if (spatial instanceof Node) { + Node node = (Node)spatial; + for (int ii = 0, nn = node.getQuantity(); ii < nn; ii++) { + drawPivots(node.getChild(ii), r); + } + } + } + + @Override // documentation inherited + protected void initLighting () + { + _dlight = new DirectionalLight(); + _dlight.setEnabled(true); + _dlight.getDirection().set(-1f, 0f, -1f).normalizeLocal(); + _dlight.getAmbient().set(0.25f, 0.25f, 0.25f, 1f); + + LightState lstate = _ctx.getRenderer().createLightState(); + lstate.attach(_dlight); + _ctx.getGeometry().setRenderState(lstate); + _ctx.getGeometry().setLightCombineMode(LightState.REPLACE); + } + + /** + * Shows the load model dialog. + */ + protected void showLoadDialog () + { + if (_chooser == null) { + _chooser = new JFileChooser(); + _chooser.setDialogTitle(_msg.get("m.load_title")); + _chooser.setFileFilter(new FileFilter() { + public boolean accept (File file) { + if (file.isDirectory()) { + return true; + } + String path = file.toString().toLowerCase(); + return path.endsWith(".properties") || + path.endsWith(".dat"); + } + public String getDescription () { + return _msg.get("m.load_filter"); + } + }); + File dir = new File(_config.getValue("dir", ".")); + if (dir.exists()) { + _chooser.setCurrentDirectory(dir); + } + } + if (_chooser.showOpenDialog(_frame) == JFileChooser.APPROVE_OPTION) { + loadModel(_chooser.getSelectedFile()); + } + _config.setValue("dir", _chooser.getCurrentDirectory().toString()); + } + + /** + * Attempts to load a model from the specified location. + */ + protected void loadModel (File file) + { + String fpath = file.toString(); + try { + if (fpath.endsWith(".properties")) { + compileModel(file); + } else if (fpath.endsWith(".dat")) { + loadCompiledModel(file); + } else { + throw new Exception(_msg.get("m.invalid_type")); + } + _status.setText(_msg.get("m.loaded_model", fpath)); + _loaded = file; + + } catch (Exception e) { + e.printStackTrace(); + _status.setText(_msg.get("m.load_error", fpath, e)); + } + } + + /** + * Attempts to compile and load a model. + */ + protected void compileModel (File file) + throws Exception + { + _status.setText(_msg.get("m.compiling_model", file)); + Model model = CompileModel.compile(file); + if (model != null) { + setModel(model, file); + return; + } + // if compileModel returned null, the .dat file is up-to-date + String fpath = file.toString(); + int didx = fpath.lastIndexOf('.'); + fpath = (didx == -1) ? fpath : fpath.substring(0, didx); + loadCompiledModel(new File(fpath + ".dat")); + } + + /** + * Attempts to load a model that has already been compiled. + */ + protected void loadCompiledModel (File file) + throws IOException + { + _status.setText(_msg.get("m.loading_model", file)); + setModel(Model.readFromFile(file, false), file); + } + + /** + * Sets the model once it's been loaded. + * + * @param file the file from which the model was loaded + */ + protected void setModel (Model model, File file) + { + if (_model != null) { + _ctx.getGeometry().detachChild(_model); + } + _ctx.getGeometry().attachChild(_model = model); + _model.setAnimationMode(_animMode); + _model.lockStaticMeshes(_ctx.getRenderer(), true, true); + + // resolve the textures from the file's directory + final File dir = file.getParentFile(); + _model.resolveTextures(new TextureProvider() { + public TextureState getTexture (String name) { + TextureState tstate = _tstates.get(name); + if (tstate == null) { + File file; + if (name.startsWith("/")) { + file = _rsrcmgr.getResourceFile(name.substring(1)); + } else { + file = new File(dir, name); + } + Texture tex = TextureManager.loadTexture(file.toString(), + Texture.MM_LINEAR_LINEAR, Texture.FM_LINEAR); + if (tex == null) { + Log.warning("Couldn't find texture [path=" + file + + "]."); + return null; + } + tex.setWrap(Texture.WM_WRAP_S_WRAP_T); + tstate = _ctx.getRenderer().createTextureState(); + tstate.setTexture(tex); + _tstates.put(name, tstate); + } + return tstate; + } + protected HashMap _tstates = + new HashMap(); + }); + _model.updateRenderState(); + + // recenter the camera + _model.updateGeometricState(0f, true); + ((OrbitCameraHandler)_camhand).recenter(); + + // configure the animation panel + String[] anims = _model.getAnimationNames(); + if (anims.length == 0) { + _animctrls.setVisible(false); + return; + } + _model.addAnimationObserver(_animobs); + _animctrls.setVisible(true); + DefaultComboBoxModel abmodel = new DefaultComboBoxModel(anims); + _animbox.setModel(abmodel); + updateAnimationSpeed(); + + // if there are any sequences, add those as well + String[] seqs = StringUtil.parseStringArray( + _model.getProperties().getProperty("sequences", "")); + for (String seq : seqs) { + abmodel.addElement(seq); + } + } + + /** + * Shows the import particle system dialog. + */ + protected void showImportDialog () + { + if (_ichooser == null) { + _ichooser = new JFileChooser(); + _ichooser.setDialogTitle(_msg.get("m.import_title")); + _ichooser.setFileFilter(new FileFilter() { + public boolean accept (File file) { + if (file.isDirectory()) { + return true; + } + String path = file.toString().toLowerCase(); + return path.endsWith(".jme"); + } + public String getDescription () { + return _msg.get("m.import_filter"); + } + }); + File dir = new File(_config.getValue("import_dir", ".")); + if (dir.exists()) { + _ichooser.setCurrentDirectory(dir); + } + } + if (_ichooser.showOpenDialog(_frame) == JFileChooser.APPROVE_OPTION) { + importFile(_ichooser.getSelectedFile()); + } + _config.setValue("import_dir", + _ichooser.getCurrentDirectory().toString()); + } + + /** + * Attempts to import the specified file as a JME binary scene. + */ + protected void importFile (File file) + { + try { + new ImportDialog(file, + (Spatial)BinaryImporter.getInstance().load( + file)).setVisible(true); + + } catch (Exception e) { + e.printStackTrace(); + _status.setText(_msg.get("m.load_error", file, e)); + } + } + + /** + * Updates the model's animation speed based on the position of the + * animation speed slider. + */ + protected void updateAnimationSpeed () + { + _model.setAnimationSpeed( + FastMath.pow(2f, _animspeed.getValue() / 25f)); + } + + /** The resource manager. */ + protected ResourceManager _rsrcmgr; + + /** The translation bundle. */ + protected MessageBundle _msg; + + /** The path of the initial model to load. */ + protected String _path; + + /** The last model successfully loaded. */ + protected File _loaded; + + /** The viewer frame. */ + protected JFrame _frame; + + /** Debug view switches. */ + protected JCheckBoxMenuItem _pivots, _bounds, _normals; + + /** The animation controls. */ + protected JPanel _animctrls; + + /** The animation selector. */ + protected JComboBox _animbox; + + /** The "stop animation" button. */ + protected JButton _animstop; + + /** The animation speed slider. */ + protected JSlider _animspeed; + + /** The status bar. */ + protected JLabel _status; + + /** The model file chooser. */ + protected JFileChooser _chooser; + + /** The import file chooser. */ + protected JFileChooser _ichooser; + + /** The desired animation mode. */ + protected Model.AnimationMode _animMode; + + /** The light rotation dialog. */ + protected RotateLightDialog _rldialog; + + /** The scene light. */ + protected DirectionalLight _dlight; + + /** Used to toggle wireframe rendering. */ + protected WireframeState _wfstate; + + /** The currently loaded model. */ + protected Model _model; + + /** Reused to draw pivot axes. */ + protected Line _axes; + + /** The current animation sequence, if any. */ + protected String[] _sequence; + + /** The current index in the animation sequence. */ + protected int _seqidx; + + /** Enables and disables the stop button when animations start and stop. */ + protected Model.AnimationObserver _animobs = + new Model.AnimationObserver() { + public boolean animationStarted (Model model, String name) { + _animstop.setEnabled(true); + return true; + } + public boolean animationCompleted (Model model, String name) { + if (_sequence != null && ++_seqidx < _sequence.length) { + _model.startAnimation(_sequence[_seqidx]); + } else { + _animstop.setEnabled(false); + _sequence = null; + } + return true; + } + public boolean animationCancelled (Model model, String name) { + if (_sequence != null && ++_seqidx < _sequence.length && + _model.getAnimation(name).repeatType != Controller.RT_CLAMP) { + _model.startAnimation(_sequence[_seqidx]); + } else { + _animstop.setEnabled(false); + _sequence = null; + } + return true; + } + }; + + /** Allows users to manipulate an imported JME file. */ + protected class ImportDialog extends JDialog + implements ChangeListener + { + public ImportDialog (File file, Spatial spatial) + { + super(_frame, _msg.get("m.import", file), false); + _spatial = spatial; + + // rotate from y-up to z-up and set initial scale + _spatial.getLocalRotation().fromAngleNormalAxis(FastMath.HALF_PI, + Vector3f.UNIT_X); + _spatial.setLocalScale(0.025f); + + JPanel cpanel = GroupLayout.makeVBox(); + getContentPane().add(cpanel, BorderLayout.CENTER); + + JPanel spanel = new JPanel(); + spanel.add(new JLabel(_msg.get("m.scale"))); + spanel.add(_scale = new JSlider(0, 1000, 250)); + _scale.addChangeListener(this); + cpanel.add(spanel); + + JPanel bpanel = new JPanel(); + bpanel.add(new JButton(new AbstractAction( + _msg.get("m.respawn_particles")) { + public void actionPerformed (ActionEvent e) { + forceRespawn(_spatial); + } + })); + bpanel.add(new JButton(new AbstractAction(_msg.get("m.close")) { + public void actionPerformed (ActionEvent e) { + setVisible(false); + } + })); + getContentPane().add(bpanel, BorderLayout.SOUTH); + pack(); + } + + // documentation inherited from interface ChangeListener + public void stateChanged (ChangeEvent e) + { + _spatial.setLocalScale(_scale.getValue() * 0.0001f); + } + + @Override // documentation inherited + public void setVisible (boolean visible) + { + super.setVisible(visible); + if (visible && _spatial.getParent() == null) { + _ctx.getGeometry().attachChild(_spatial); + } else if (!visible && _spatial.getParent() != null) { + _ctx.getGeometry().detachChild(_spatial); + } + } + + /** + * Recursively forces all particles to respawn. + */ + protected void forceRespawn (Spatial spatial) + { + if (spatial instanceof ParticleMesh) { + ((ParticleMesh)spatial).forceRespawn(); + } else if (spatial instanceof Node) { + Node node = (Node)spatial; + for (int ii = 0, nn = node.getQuantity(); ii < nn; ii++) { + forceRespawn(node.getChild(ii)); + } + } + } + + /** The imported scene. */ + protected Spatial _spatial; + + /** The scale slider. */ + protected JSlider _scale; + } + + /** Allows users to move the directional light around. */ + protected class RotateLightDialog extends JDialog + implements ChangeListener + { + public RotateLightDialog () + { + super(_frame, _msg.get("m.rotate_light"), false); + + JPanel cpanel = GroupLayout.makeVBox(); + getContentPane().add(cpanel, BorderLayout.CENTER); + + JPanel apanel = new JPanel(); + apanel.add(new JLabel(_msg.get("m.azimuth"))); + apanel.add(_azimuth = new JSlider(-180, +180, 0)); + _azimuth.addChangeListener(this); + cpanel.add(apanel); + + JPanel epanel = new JPanel(); + epanel.add(new JLabel(_msg.get("m.elevation"))); + epanel.add(_elevation = new JSlider(-90, +90, 45)); + _elevation.addChangeListener(this); + cpanel.add(epanel); + + JPanel bpanel = new JPanel(); + bpanel.add(new JButton(new AbstractAction(_msg.get("m.close")) { + public void actionPerformed (ActionEvent e) { + setVisible(false); + } + })); + getContentPane().add(bpanel, BorderLayout.SOUTH); + } + + // documentation inherited from interface ChangeListener + public void stateChanged (ChangeEvent e) + { + float az = _azimuth.getValue() * FastMath.DEG_TO_RAD, + el = _elevation.getValue() * FastMath.DEG_TO_RAD; + _dlight.getDirection().set( + -FastMath.cos(az) * FastMath.cos(el), + -FastMath.sin(az) * FastMath.cos(el), + -FastMath.sin(el)); + } + + /** Azimuth and elevation sliders. */ + protected JSlider _azimuth, _elevation; + } + + /** Moves the camera using mouse input. */ + protected class MouseOrbiter extends MouseAdapter + implements MouseMotionListener, MouseWheelListener + { + @Override // documentation inherited + public void mousePressed (MouseEvent e) + { + _mloc.setLocation(e.getX(), e.getY()); + } + + // documentation inherited from interface MouseMotionListener + public void mouseMoved (MouseEvent e) + { + } + + // documentation inherited from interface MouseMotionListener + public void mouseDragged (MouseEvent e) + { + int dx = e.getX() - _mloc.x, dy = e.getY() - _mloc.y; + _mloc.setLocation(e.getX(), e.getY()); + int mods = e.getModifiers(); + if ((mods & MouseEvent.BUTTON1_MASK) != 0) { + _camhand.tiltCamera(dy * FastMath.PI / 1000); + _camhand.orbitCamera(-dx * FastMath.PI / 1000); + } else if ((mods & MouseEvent.BUTTON2_MASK) != 0) { + _camhand.zoomCamera(dy/8f); + } else { + _camhand.panCamera(-dx/8f, dy/8f); + } + } + + // documentation inherited from interface MouseWheelListener + public void mouseWheelMoved (MouseWheelEvent e) + { + _camhand.zoomCamera(e.getWheelRotation() * 10f); + } + + /** The last recorded position of the mouse cursor. */ + protected Point _mloc = new Point(); + } + + /** A camera handler that pans in directions orthogonal to the camera + * direction. */ + protected class OrbitCameraHandler extends CameraHandler + { + public OrbitCameraHandler (Camera camera) + { + super(camera); + _gpoint = super.getGroundPoint(); + } + + @Override // documentation inherited + public void panCamera (float x, float y) { + Vector3f offset = _camera.getLeft().mult(-x).addLocal( + _camera.getUp().mult(y)); + getGroundPoint().addLocal(offset); + _camera.getLocation().addLocal(offset); + _camera.onFrameChange(); + } + + @Override // documentation inherited + public Vector3f getGroundPoint () + { + return _gpoint; + } + + /** + * Resets the ground point to the center of the grid or, if there is + * one, the center of the model. + */ + public void recenter () + { + Vector3f target = new Vector3f(); + if (_model != null) { + BoundingVolume bound = (BoundingVolume)_model.getWorldBound(); + if (bound != null) { + bound.getCenter(target); + } + } + Vector3f offset = target.subtract(_gpoint); + _camera.getLocation().addLocal(offset); + _camera.onFrameChange(); + _gpoint.set(target); + } + + /** The point at which the camera is looking. */ + protected Vector3f _gpoint; + } + + /** The app configuration. */ + protected static Config _config = + new Config("com/threerings/jme/tools/ModelViewer"); + + /** The number of lines on the grid in each direction. */ + protected static final int GRID_SIZE = 32; + + /** The spacing between lines on the grid. */ + protected static final float GRID_SPACING = 2.5f; +} diff --git a/src/java/com/threerings/jme/tools/xml/AnimationParser.java b/src/java/com/threerings/jme/tools/xml/AnimationParser.java new file mode 100644 index 00000000..5da48a96 --- /dev/null +++ b/src/java/com/threerings/jme/tools/xml/AnimationParser.java @@ -0,0 +1,88 @@ +// +// $Id: SceneParser.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tools.xml; + +import java.io.FileInputStream; +import java.io.IOException; + +import org.xml.sax.SAXException; +import org.apache.commons.digester.Digester; + +import com.samskivert.xml.SetPropertyFieldsRule; + +import com.threerings.jme.tools.AnimationDef; + +/** + * Parses XML files containing animations. + */ +public class AnimationParser +{ + public AnimationParser () + { + // create and configure our digester + _digester = new Digester(); + + // add the rules + String anim = "animation"; + _digester.addObjectCreate(anim, AnimationDef.class.getName()); + _digester.addRule(anim, new SetPropertyFieldsRule()); + _digester.addSetNext(anim, "setAnimation", + AnimationDef.class.getName()); + + String frame = anim + "/frame"; + _digester.addObjectCreate(frame, + AnimationDef.FrameDef.class.getName()); + _digester.addSetNext(frame, "addFrame", + AnimationDef.FrameDef.class.getName()); + + String xform = frame + "/transform"; + _digester.addObjectCreate(xform, + AnimationDef.TransformDef.class.getName()); + _digester.addRule(xform, new SetPropertyFieldsRule()); + _digester.addSetNext(xform, "addTransform", + AnimationDef.TransformDef.class.getName()); + } + + /** + * Parses the XML file at the specified path into an animation + * definition. + */ + public AnimationDef parseAnimation (String path) + throws IOException, SAXException + { + _animation = null; + _digester.push(this); + _digester.parse(new FileInputStream(path)); + return _animation; + } + + /** + * Called by the parser once the animation is parsed. + */ + public void setAnimation (AnimationDef animation) + { + _animation = animation; + } + + protected Digester _digester; + protected AnimationDef _animation; +} diff --git a/src/java/com/threerings/jme/tools/xml/ModelParser.java b/src/java/com/threerings/jme/tools/xml/ModelParser.java new file mode 100644 index 00000000..e55469fa --- /dev/null +++ b/src/java/com/threerings/jme/tools/xml/ModelParser.java @@ -0,0 +1,109 @@ +// +// $Id: SceneParser.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.tools.xml; + +import java.io.FileInputStream; +import java.io.IOException; + +import org.xml.sax.SAXException; +import org.apache.commons.digester.Digester; + +import com.samskivert.xml.SetPropertyFieldsRule; + +import com.threerings.jme.tools.ModelDef; + +/** + * Parses XML files containing 3D models. + */ +public class ModelParser +{ + public ModelParser () + { + // create and configure our digester + _digester = new Digester(); + + // add the rules + String model = "model"; + _digester.addObjectCreate(model, ModelDef.class.getName()); + _digester.addSetNext(model, "setModel", ModelDef.class.getName()); + + String tmesh = model + "/triMesh"; + _digester.addObjectCreate(tmesh, ModelDef.TriMeshDef.class.getName()); + _digester.addRule(tmesh, new SetPropertyFieldsRule()); + _digester.addSetNext(tmesh, "addSpatial", + ModelDef.SpatialDef.class.getName()); + + String smesh = model + "/skinMesh"; + _digester.addObjectCreate(smesh, + ModelDef.SkinMeshDef.class.getName()); + _digester.addRule(smesh, new SetPropertyFieldsRule()); + _digester.addSetNext(smesh, "addSpatial", + ModelDef.SpatialDef.class.getName()); + + String node = model + "/node"; + _digester.addObjectCreate(node, ModelDef.NodeDef.class.getName()); + _digester.addRule(node, new SetPropertyFieldsRule()); + _digester.addSetNext(node, "addSpatial", + ModelDef.SpatialDef.class.getName()); + + String vertex = tmesh + "/vertex", svertex = smesh + "/vertex"; + _digester.addObjectCreate(vertex, ModelDef.Vertex.class.getName()); + _digester.addObjectCreate(svertex, + ModelDef.SkinVertex.class.getName()); + _digester.addRule(vertex, new SetPropertyFieldsRule()); + _digester.addRule(svertex, new SetPropertyFieldsRule()); + _digester.addSetNext(vertex, "addVertex", + ModelDef.Vertex.class.getName()); + _digester.addSetNext(svertex, "addVertex", + ModelDef.Vertex.class.getName()); + + String bweight = smesh + "/vertex/boneWeight"; + _digester.addObjectCreate(bweight, + ModelDef.BoneWeight.class.getName()); + _digester.addRule(bweight, new SetPropertyFieldsRule()); + _digester.addSetNext(bweight, "addBoneWeight", + ModelDef.BoneWeight.class.getName()); + } + + /** + * Parses the XML file at the specified path into a model definition. + */ + public ModelDef parseModel (String path) + throws IOException, SAXException + { + _model = null; + _digester.push(this); + _digester.parse(new FileInputStream(path)); + return _model; + } + + /** + * Called by the parser once the model is parsed. + */ + public void setModel (ModelDef model) + { + _model = model; + } + + protected Digester _digester; + protected ModelDef _model; +} diff --git a/src/java/com/threerings/jme/util/LinearTimeFunction.java b/src/java/com/threerings/jme/util/LinearTimeFunction.java new file mode 100644 index 00000000..d29ccd7a --- /dev/null +++ b/src/java/com/threerings/jme/util/LinearTimeFunction.java @@ -0,0 +1,42 @@ +// +// $Id: LinearTimeFunction.java 3122 2004-09-18 22:57:08Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.util; + +/** + * Varies a value linearly with time. + */ +public class LinearTimeFunction extends TimeFunction +{ + public LinearTimeFunction (float start, float end, float duration) + { + super(start, end, duration); + _range = (end - start); + } + + // documentation inherited + protected float computeValue () + { + return (_elapsed * _range / _duration) + _start; + } + + protected float _range; +} diff --git a/src/java/com/threerings/jme/util/TimeFunction.java b/src/java/com/threerings/jme/util/TimeFunction.java new file mode 100644 index 00000000..73afaa0f --- /dev/null +++ b/src/java/com/threerings/jme/util/TimeFunction.java @@ -0,0 +1,99 @@ +// +// $Id: TimeFunction.java 3122 2004-09-18 22:57:08Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.util; + +/** + * Used to vary a value over time where time is provided at discrete increments + * (on the frame tick) and the value is computed appropriately. This can be + * used for fades and other effects where different functions (linear, ease-in + * ease-out, etc.) should be easy to plug in. + */ +public abstract class TimeFunction +{ + /** + * Every time function varies a value from some starting value to some + * ending value over some duration. The way in which it varies + * (linearly, for example) is up to the derived class. + * + * @param start the starting value. + * @param end the ending value. + * @param duration the duration in seconds. + */ + public TimeFunction (float start, float end, float duration) + { + _start = start; + _end = end; + _duration = duration; + } + + /** + * Returns the current value given the supplied elapsed time. The value + * will be bounded to the originally supplied starting and ending values at + * times 0 (and below) and {@link #_duration} (and above) respectively. + * + * @param deltaTime the amount of time that has elapsed since the last call + * to this method. + */ + public float getValue (float deltaTime) + { + _elapsed += deltaTime; + + if (_elapsed <= 0) { + return _start; + } else if (_elapsed >= _duration) { + return _end; + } else { + return computeValue(); + } + } + + /** + * Returns true if this function has proceeded the full length of its + * duration. This should be called after a call to {@link #getValue} has + * been made to update our internal elapsed time. + */ + public boolean isComplete () + { + return _elapsed >= _duration; + } + + /** Generates a string representation of this instance. */ + public String toString () + { + return _start + " to " + _end + ", " + _elapsed + " of " + _duration; + } + + /** + * This must be implemented by our derived class to compute our value given + * the currently stored {@link #_elapsed} time. + */ + protected abstract float computeValue (); + + /** Our starting and ending values. */ + protected float _start, _end; + + /** The number of milliseconds over which we vary our value. */ + protected float _duration; + + /** The number of seconds that have elapsed since we started. */ + protected float _elapsed; +} diff --git a/src/java/com/threerings/media/AbstractMedia.java b/src/java/com/threerings/media/AbstractMedia.java new file mode 100644 index 00000000..da4b7902 --- /dev/null +++ b/src/java/com/threerings/media/AbstractMedia.java @@ -0,0 +1,332 @@ +// +// $Id: AbstractMedia.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; + +import java.awt.geom.AffineTransform; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import com.samskivert.util.ObserverList; +import com.samskivert.util.StringUtil; + +/** + * Something that can be rendered on the media panel. + */ +public abstract class AbstractMedia + implements Shape +{ + /** A {@link #_renderOrder} value at or above which, indicates that this + * media is in the HUD (heads up display) and should not scroll when the + * view scrolls. */ + public static final int HUD_LAYER = 65536; + + /** + * Instantiate an abstract media object. + */ + public AbstractMedia (Rectangle bounds) + { + _bounds = bounds; + } + + /** + * Called periodically by this media's manager to give it + * a chance to do its thing. + * + * @param tickStamp a time stamp associated with this tick. + * Note: this is not obtained from a call to {@link + * System#currentTimeMillis} and cannot be compared to timestamps + * obtained there from. + */ + public abstract void tick (long tickStamp); + + /** + * Called by the appropriate manager to request that the + * media render itself with the given graphics context. The + * media may wish to inspect the clipping region that has been set + * on the graphics context to render itself more efficiently. This + * method will only be called after it has been established that this + * media's bounds intersect the clipping region. + */ + public abstract void paint (Graphics2D gfx); + + /** + * Called when the appropriate media manager has been paused for some + * length of time and is then unpaused. Media should adjust any time stamps + * that are maintained internally forward by the delta so that time + * maintains the illusion of flowing smoothly forward. + */ + public void fastForward (long timeDelta) + { + // adjust our first tick stamp + _firstTick += timeDelta; + } + + /** + * Invalidate the media's bounding rectangle for later painting. + */ + public void invalidate () + { + if (_mgr != null) { + _mgr.getRegionManager().invalidateRegion(_bounds); + } + } + + /** + * Set the location. + */ + public void setLocation (int x, int y) + { + _bounds.x = x; + _bounds.y = y; + } + + /** + * Returns a rectangle containing all the pixels rendered by this media. + */ + public Rectangle getBounds () + { + return _bounds; + } + + // documentation inherited from interface Shape + public Rectangle2D getBounds2D () + { + return _bounds; + } + + // documentation inherited from interface Shape + public boolean contains (double x, double y) + { + return _bounds.contains(x, y); + } + + // documentation inherited from interface Shape + public boolean contains (Point2D p) + { + return _bounds.contains(p); + } + + // documentation inherited from interface Shape + public boolean intersects (double x, double y, double w, double h) + { + return _bounds.intersects(x, y, w, h); + } + + // documentation inherited from interface Shape + public boolean intersects (Rectangle2D r) + { + return _bounds.intersects(r); + } + + // documentation inherited from interface Shape + public boolean contains (double x, double y, double w, double h) + { + return _bounds.contains(x, y, w, h); + } + + // documentation inherited from interface Shape + public boolean contains (Rectangle2D r) + { + return _bounds.contains(r); + } + + // documentation inherited from interface Shape + public PathIterator getPathIterator (AffineTransform at) + { + return _bounds.getPathIterator(at); + } + + // documentation inherited from interface Shape + public PathIterator getPathIterator (AffineTransform at, double flatness) + { + return _bounds.getPathIterator(at, flatness); + } + + /** + * Sets the render order associated with this media. Media + * can be rendered in two layers; those with negative render order and + * those with positive render order. In the same layer, they + * will be rendered according to their render order's cardinal value + * (least to greatest). Those with the same render order value will be + * rendered in arbitrary order. + * + *

This method may not be called during a tick. + * + * @see #HUD_LAYER + */ + public void setRenderOrder (int renderOrder) + { + if (_renderOrder != renderOrder) { + _renderOrder = renderOrder; + if (_mgr != null) { + _mgr.renderOrderDidChange(this); + invalidate(); + } + } + } + + /** + * Returns the render order of this media element. + */ + public int getRenderOrder () + { + return _renderOrder; + } + + /** + * Queues the supplied notification up to be dispatched to this + * abstract media's observers. + */ + public void queueNotification (ObserverList.ObserverOp amop) + { + if (_observers != null) { + if (_mgr != null) { + _mgr.queueNotification(_observers, amop); + } else { + Log.warning("Have no manager, dropping notification " + + "[media=" + this + ", op=" + amop + "]."); + } + } + } + + /** + * Called by the {@link AbstractMediaManager} when we are in a + * {@link VirtualMediaPanel} that just scrolled. + */ + public void viewLocationDidChange (int dx, int dy) + { + if (_renderOrder >= HUD_LAYER) { + setLocation(_bounds.x + dx, _bounds.y + dy); + } + } + + /** + * Dumps this media to a String object. + */ + public String toString () + { + StringBuilder buf = new StringBuilder(); + buf.append(StringUtil.shortClassName(this)); + buf.append("["); + toString(buf); + return buf.append("]").toString(); + } + + /** + * Initialize the media. + */ + protected final void init (AbstractMediaManager manager) + { + _mgr = manager; + init(); + } + + /** + * Called when the media has had its manager set. + * Derived classes may override this method, but should be sure to + * call super.init(). + */ + protected void init () + { + } + + /** + * Prior to the first call to {@link #tick} on an abstract media, this + * method is called by the {@link AbstractMediaManager}. It is called + * during the normal tick cycle, immediately prior to the first call + * to {@link #tick}. + * + *

Note: It is imperative that + * super.willStart() is called by any entity that + * overrides this method because the {@link AbstractMediaManager} + * depends on the setting of the {@link #_firstTick} value to know + * whether or not to call this method. + */ + protected void willStart (long tickStamp) + { + _firstTick = tickStamp; + } + + /** + * Called by the media manager after the media is removed from service. + * Derived classes may override this method, but should be sure to + * call super.shutdown(). + */ + protected void shutdown () + { + invalidate(); + _mgr = null; + } + + /** + * Add the specified observer to this media element. + */ + protected void addObserver (Object obs) + { + if (_observers == null) { + _observers = new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY); + } + _observers.add(obs); + } + + /** + * Remove the specified observer from this media element. + */ + protected void removeObserver (Object obs) + { + if (_observers != null) { + _observers.remove(obs); + } + } + + /** + * This should be overridden by derived classes (which should be sure + * to call super.toString()) to append the derived class + * specific information to the string buffer. + */ + protected void toString (StringBuilder buf) + { + buf.append("bounds=").append(StringUtil.toString(_bounds)); + buf.append(", renderOrder=").append(_renderOrder); + } + + /** The layer in which to render. */ + protected int _renderOrder = 0; + + /** The bounds of the media's rendering area. */ + protected Rectangle _bounds; + + /** Our manager. */ + protected AbstractMediaManager _mgr; + + /** Our observers. */ + protected ObserverList _observers = null; + + /** The tick stamp associated with our first call to {@link #tick}. + * This is set up automatically in {@link #willStart}. */ + protected long _firstTick; +} diff --git a/src/java/com/threerings/media/AbstractMediaManager.java b/src/java/com/threerings/media/AbstractMediaManager.java new file mode 100644 index 00000000..2526e906 --- /dev/null +++ b/src/java/com/threerings/media/AbstractMediaManager.java @@ -0,0 +1,335 @@ +// +// $Id: AbstractMediaManager.java 3551 2005-05-13 22:22:56Z andrzej $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.Graphics2D; +import java.awt.Shape; + +import java.util.ArrayList; +import java.util.Comparator; + +import com.samskivert.util.ObserverList.ObserverOp; +import com.samskivert.util.ObserverList; +import com.samskivert.util.SortableArrayList; +import com.samskivert.util.Tuple; + +/** + * Manages, ticks, and paints {@link AbstractMedia}. + */ +public abstract class AbstractMediaManager + implements MediaConstants +{ + /** + * Default constructor. + */ + public AbstractMediaManager (MediaPanel panel) + { + _panel = panel; + _remgr = panel.getRegionManager(); + } + + /** + * Returns region manager in use by this manager. + */ + public RegionManager getRegionManager () + { + return _remgr; + } + + /** + * Returns the media panel with which we are coordinating. + */ + public MediaPanel getMediaPanel () + { + return _panel; + } + + /** + * Must be called every frame so that the media can be properly + * updated. + */ + public void tick (long tickStamp) + { + _tickStamp = tickStamp; + tickAllMedia(tickStamp); + dispatchNotifications(); + // we clear our tick stamp when we're about to be painted, this + // lets us handle situations when yet more media is slipped in + // between our being ticked and our being painted + } + + /** + * This will always be called prior to the {@link #paint} calls for a + * particular tick. Because it is possible that there will be no dirty + * regions and thus no calls to {@link #paint} this method exists so + * that the media manager can guarantee that it will be notified when + * all ticking is complete and the painting phase has begun. + */ + public void willPaint () + { + // now that we're done ticking, we can safely clear this + _tickStamp = 0; + } + + /** + * Renders all registered media in the given layer that intersect + * the supplied clipping rectangle to the given graphics context. + * + * @param layer the layer to render; one of {@link #FRONT}, {@link + * #BACK}, or {@link #ALL}. The front layer contains all animations + * with a positive render order; the back layer contains all + * animations with a negative render order; all, both. + */ + public void paint (Graphics2D gfx, int layer, Shape clip) + { + for (int ii = 0, nn = _media.size(); ii < nn; ii++) { + AbstractMedia media = (AbstractMedia)_media.get(ii); + int order = media.getRenderOrder(); + try { + if (((layer == ALL) || + (layer == FRONT && order >= 0) || + (layer == BACK && order < 0)) && + clip.intersects(media.getBounds())) { + media.paint(gfx); + } + + } catch (Exception e) { + Log.warning("Failed to render media " + + "[media=" + media + ", e=" + e + "]."); + Log.logStackTrace(e); + } + } + } + + /** + * If the manager is paused for some length of time, it should + * be fast forwarded by the appropriate number of milliseconds. This + * allows media to smoothly pick up where they left off rather than + * abruptly jumping into the future, thinking that some outrageous + * amount of time passed since their last tick. + */ + public void fastForward (long timeDelta) + { + if (_tickStamp > 0) { + Log.warning("Egads! Asked to fastForward() during a tick."); + Thread.dumpStack(); + } + + for (int ii=0, nn=_media.size(); ii < nn; ii++) { + ((AbstractMedia) _media.get(ii)).fastForward(timeDelta); + } + } + + /** + * Returns true if the specified media is being managed by this media + * manager. + */ + public boolean isManaged (AbstractMedia media) + { + return _media.contains(media); + } + + /** + * Called by a {@link VirtualMediaPanel} when the view that contains our + * media is scrolled. + * + * @param dx the scrolled distance in the x direction (in pixels). + * @param dy the scrolled distance in the y direction (in pixels). + */ + public void viewLocationDidChange (int dx, int dy) + { + // let our media know + for (int ii = 0, ll = _media.size(); ii < ll; ii++) { + ((AbstractMedia)_media.get(ii)).viewLocationDidChange(dx, dy); + } + } + + /** + * Called by a {@link AbstractMedia} when its render order has changed. + */ + public void renderOrderDidChange (AbstractMedia media) + { + if (_tickStamp > 0) { + Log.warning("Egads! Render order changed during a tick."); + Thread.dumpStack(); + } + + _media.remove(media); + _media.insertSorted(media, RENDER_ORDER); + } + + /** + * Calls {@link AbstractMedia#tick} on all media to give them a chance + * to move about, change their look, generate dirty regions, and so + * forth. + */ + protected void tickAllMedia (long tickStamp) + { + // we use _tickpos so that it can be adjusted if media is added or + // removed during the tick dispatch + for (_tickpos = 0; _tickpos < _media.size(); _tickpos++) { + tickMedia((AbstractMedia) _media.get(_tickpos), tickStamp); + } + _tickpos = -1; + } + + /** + * Inserts the specified media into this manager, return true on + * success. + */ + protected boolean insertMedia (AbstractMedia media) + { + if (_media.contains(media)) { + Log.warning("Attempt to insert media more than once " + + "[media=" + media + "]."); + Thread.dumpStack(); + return false; + } + + media.init(this); + int ipos = _media.insertSorted(media, RENDER_ORDER); + + // if we've started our tick but have not yet painted our media, + // we need to take care that this newly added media will be ticked + // before our upcoming render + if (_tickStamp > 0L) { + if (_tickpos == -1) { + // if we're done with our own call to tick(), we + // definitely need to tick this new media + tickMedia(media, _tickStamp); + } else if (ipos <= _tickpos) { + // otherwise, we're in the middle of our call to tick() + // and we only need to tick this guy if he's being + // inserted before our current tick position (if he's + // inserted after our current position, we'll get to him + // as part of this tick iteration) + _tickpos++; + tickMedia(media, _tickStamp); + } + } + + return true; + } + + /** A helper function used to call {@link AbstractMedia#tick}. */ + protected final void tickMedia (AbstractMedia media, long tickStamp) + { + if (media._firstTick == 0L) { + media.willStart(tickStamp); + } + media.tick(tickStamp); + } + + /** + * Removes the specified media from this manager, return true on + * success. + */ + protected boolean removeMedia (AbstractMedia media) + { + int mpos = _media.indexOf(media); + if (mpos != -1) { + _media.remove(mpos); + media.invalidate(); + media.shutdown(); + // if we're in the middle of ticking, we need to adjust the + // _tickpos if necessary + if (mpos <= _tickpos) { + _tickpos--; + } + return true; + } + Log.warning("Attempt to remove media that wasn't inserted " + + "[media=" + media + "]."); + return false; + } + + /** + * Clears all media from the manager and calls {@link + * AbstractMedia#shutdown} on each. This does not invalidate the + * media's vacated bounds; it is assumed that it will be ok. + */ + protected void clearMedia () + { + if (_tickStamp > 0) { + Log.warning("Egads! Requested to clearMedia() during a tick."); + Thread.dumpStack(); + } + + for (int ii=_media.size() - 1; ii >= 0; ii--) { + AbstractMedia media = (AbstractMedia) _media.remove(ii); + media.shutdown(); + } + } + + /** + * Queues the notification for dispatching after we've ticked all the + * media. + */ + public void queueNotification (ObserverList observers, ObserverOp event) + { + _notify.add(new Tuple(observers, event)); + } + + /** + * Dispatches all queued media notifications. + */ + protected void dispatchNotifications () + { + for (int ii = 0, nn = _notify.size(); ii < nn; ii++) { + Tuple tuple = (Tuple)_notify.get(ii); + ((ObserverList)tuple.left).apply((ObserverOp)tuple.right); + } + _notify.clear(); + } + + /** The media panel we're working with. */ + protected MediaPanel _panel; + + /** The region manager. */ + protected RegionManager _remgr; + + /** List of observers to notify at the end of the tick. */ + protected ArrayList _notify = new ArrayList(); + + /** Our render-order sorted list of media. */ + protected SortableArrayList _media = new SortableArrayList(); + + /** The position in our media list that we're ticking in the middle of + * a call to {@link #tick} otherwise -1. */ + protected int _tickpos = -1; + + /** The tick stamp if the manager is in the midst of a call to {@link + * #tick}, else 0. */ + protected long _tickStamp; + + /** Used to sort media by render order. */ + protected static final Comparator RENDER_ORDER = new Comparator() { + public int compare (Object o1, Object o2) { + int result = (((AbstractMedia)o1)._renderOrder - + ((AbstractMedia)o2)._renderOrder); + return (result != 0) ? result : + // find some other way to keep them stable relative to + // each other + o1.hashCode() - o2.hashCode(); + } + }; +} diff --git a/src/java/com/threerings/media/ActiveRepaintManager.java b/src/java/com/threerings/media/ActiveRepaintManager.java new file mode 100644 index 00000000..fc812378 --- /dev/null +++ b/src/java/com/threerings/media/ActiveRepaintManager.java @@ -0,0 +1,502 @@ +// +// $Id: ActiveRepaintManager.java 4181 2006-06-07 21:54:12Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.applet.Applet; + +import java.awt.Component; +import java.awt.Graphics2D; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.Window; + +import javax.swing.CellRendererPane; +import javax.swing.JComponent; +import javax.swing.JEditorPane; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.RepaintManager; +import javax.swing.SwingUtilities; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; + +import com.samskivert.util.ListUtil; +import com.samskivert.util.RunAnywhere; +import com.samskivert.util.StringUtil; + +/** + * Used to get Swing's repainting to jive with our active rendering + * strategy. + * + * @see FrameManager + */ +public class ActiveRepaintManager extends RepaintManager +{ + /** + * Components that are rooted in this component (which must be a {@link + * Window} or an {@link Applet}) will be rendered into the offscreen buffer + * managed by the frame manager. Other components will be rendered into + * separate offscreen buffers and repainted in the normal Swing manner. + */ + public ActiveRepaintManager (Component root) + { + _root = root; + } + + // documentation inherited + public synchronized void addInvalidComponent (JComponent comp) + { + Component vroot = null; + if (DEBUG) { + Log.info("Maybe invalidating " + toString(comp) + "."); + } + + // locate the validation root for this component + for (Component c = comp; c != null; c = c.getParent()) { + // if the component is not part of an active widget hierarcy, + // we can stop now; if the component is a cell render pane, + // we're apparently supposed to ignore it as wel + if (!c.isDisplayable() || c instanceof CellRendererPane) { + return; + } + + // skip non-Swing components + if (!(c instanceof JComponent)) { + continue; + } + + // if we find our validate root, we can stop looking; NOTE: + // JTextField incorrectly claims to be a validate root thereby + // fucking up the program something serious; we jovially + // ignore it's claims here and restore order to the universe; + // see bug #403550 for more fallout from Sun's fuckup + if (!(c instanceof JTextField) && + !(c instanceof JScrollPane) && + ((JComponent)c).isValidateRoot()) { + vroot = c; + break; + } + } + + // if we found no validation root we can abort as this component + // is not part of any valid widget hierarchy + if (vroot == null) { + if (DEBUG) { + Log.info("Skipping vrootless component: " + toString(comp)); + } + + return; + } + + // make sure that the component is actually in a window or applet + // that is showing + if (getRoot(vroot) == null) { + if (DEBUG) { + Log.info("Skipping rootless component " + + "[comp=" + toString(comp) + + ", vroot=" + toString(vroot) + "]."); + } + return; + } + + // add the invalid component to our list and we'll validate it on + // the next frame + if (!ListUtil.containsRef(_invalid, vroot)) { + if (DEBUG) { + Log.info("Invalidating " + toString(vroot) + "."); + } + _invalid = ListUtil.add(_invalid, vroot); + + // on the mac, components frequently do not repaint themselves + // after being invalidated so we have to force a repaint from + // the validation roon on down + if (RunAnywhere.isMacOS() && vroot instanceof JComponent) { + addDirtyRegion((JComponent)vroot, 0, 0, + vroot.getWidth(), vroot.getHeight()); + } + } + } + + // documentation inherited + public synchronized void addDirtyRegion ( + JComponent comp, int x, int y, int width, int height) + { + // ignore invalid requests + if ((width <= 0) || (height <= 0) || (comp == null) || + (comp.getWidth() <= 0) || (comp.getHeight() <= 0)) { +// Log.info("Skipping bogus region " + comp.getClass().getName() + +// ", x=" + x + ", y=" + y + ", width=" + width + ", height=" + height + "."); + return; + } + + // if this component is already dirty, simply expand their + // existing dirty rectangle + Rectangle drect = (Rectangle)_dirty.get(comp); + if (drect != null) { + drect.add(x, y); + drect.add(x+width, y+height); + return; + } + + // make sure this component has a valid root + if (getRoot(comp) == null) { +// Log.info("Skipping rootless repaint " + comp + "."); + return; + } + + if (DEBUG) { + Log.info("Dirtying component [comp=" + toString(comp) + + ", drect=" + StringUtil.toString( + new Rectangle(x, y, width, height)) + "]."); + } + + // if we made it this far, we can queue up a dirty region for this + // component to be repainted on the next tick + _dirty.put(comp, new Rectangle(x, y, width, height)); + } + + /** + * Returns the root component for the supplied component or null if it + * is not part of a rooted hierarchy or if any parent along the way is + * found to be hidden or without a peer. + */ + protected Component getRoot (Component comp) + { + for (Component c = comp; c != null; c = c.getParent()) { + boolean hidden = !c.isDisplayable(); + // on the mac, the JRootPane is invalidated before it is + // visible and is never again invalidated or repainted, so we + // punt and allow all invisible components to be invalidated + // and revalidated + if (!RunAnywhere.isMacOS()) { + hidden |= !c.isVisible(); + } + if (hidden) { + return null; + } + if (c instanceof Window || c instanceof Applet) { + return c; + } + } + return null; + } + + // documentation inherited + public synchronized Rectangle getDirtyRegion (JComponent comp) + { + Rectangle drect = (Rectangle)_dirty.get(comp); + // copy the rectangle if we found one, otherwise create an empty + // rectangle because we don't want them leaving empty handed + return (drect == null) ? + new Rectangle(0, 0, 0, 0) : new Rectangle(drect); + } + + // documentation inherited + public synchronized void markCompletelyClean (JComponent comp) + { + _dirty.remove(comp); + } + + /** + * Validates the invalid components that have been queued up since the + * last frame tick. + */ + public void validateComponents () + { + // swap out our invalid array + Object[] invalid = null; + synchronized (this) { + invalid = _invalid; + _invalid = null; + } + + // if there's nothing to validate, we're home free + if (invalid == null) { + return; + } + + // validate everything therein + int icount = invalid.length; + for (int ii = 0; ii < icount; ii++) { + if (invalid[ii] != null) { + if (DEBUG) { + Log.info("Validating " + invalid[ii]); + } + ((Component)invalid[ii]).validate(); + } + } + } + + /** + * Paints the components that have become dirty since the last tick. + * + * @return true if any components were painted. + */ + public boolean paintComponents (Graphics g, FrameManager fmgr) + { + synchronized (this) { + // exit now if there are no dirty rectangles to paint + if (_dirty.isEmpty()) { + return false; + } + + // otherwise, swap our hashmaps + HashMap tmap = _spare; + _spare = _dirty; + _dirty = tmap; + } + + // scan through the list, looking for components for whom a parent + // component is also dirty. in such a case, the dirty rectangle + // for the parent component is expanded to contain the dirty + // rectangle of the child and the child is removed from the + // repaint list (painting the parent will repaint the child) + Iterator iter = _spare.entrySet().iterator(); + PRUNE: + while (iter.hasNext()) { + Entry entry = (Entry)iter.next(); + JComponent comp = (JComponent)entry.getKey(); + Rectangle drect = (Rectangle)entry.getValue(); + int x = comp.getX() + drect.x, y = comp.getY() + drect.y; + + // climb up the parent hierarchy, looking for the first opaque + // parent as well as the root component + for (Component c = comp.getParent(); c != null; c = c.getParent()) { + // stop looking for combinable parents for non-visible or + // non-JComponents + if (!c.isVisible() || !c.isDisplayable() || + !(c instanceof JComponent)) { + break; + } + + // check to see if this parent is dirty + Rectangle prect = (Rectangle)_spare.get(c); + if (prect != null) { + // that we were going to merge it with its parent and + // blow it away + drect.x = x; + drect.y = y; + + if (DEBUG) { + Log.info("Found dirty parent " + + "[comp=" + toString(comp) + + ", drect=" + StringUtil.toString(drect) + + ", pcomp=" + toString(c) + + ", prect=" + StringUtil.toString(prect) + + "]."); + } + prect.add(drect); + + if (DEBUG) { + Log.info("New prect " + StringUtil.toString(prect)); + } + + // remove the child component and be on our way + iter.remove(); + continue PRUNE; + } + + // translate the coordinates into this component's + // coordinate system + x += c.getX(); + y += c.getY(); + } + } + + // now paint each of the dirty components, by setting the clipping + // rectangle appropriately and calling paint() on the associated + // root component + iter = _spare.entrySet().iterator(); + while (iter.hasNext()) { + Entry entry = (Entry)iter.next(); + JComponent comp = (JComponent)entry.getKey(); + Rectangle drect = (Rectangle)entry.getValue(); + + // get the root component, adjust the clipping (dirty) + // rectangle and obtain the bounds of the client in absolute + // coordinates + Component root = null, ocomp = null; + + // start with the components bounds which we'll switch to the + // opaque parent component's bounds if and when we find one + _cbounds.setBounds(0, 0, comp.getWidth(), comp.getHeight()); + + // climb up the parent hierarchy, looking for the first opaque + // parent as well as the root component + for (Component c = comp; c != null; c = c.getParent()) { + if (!c.isVisible() || !c.isDisplayable()) { + break; + } + + if (c instanceof JComponent) { + // make a note of the first opaque parent we find + if (ocomp == null && ((JComponent)c).isOpaque()) { + ocomp = c; + // we need to obtain the opaque parent's coordinates + // in the root coordinate system for when we repaint + _cbounds.setBounds( + 0, 0, ocomp.getWidth(), ocomp.getHeight()); + } + + } else { + // oh god the hackery. apparently the fscking + // JEditorPane wraps a heavy weight component around + // every swing component it uses when doing forms + Component tp = c.getParent(); + if (!(tp instanceof JEditorPane)) { + root = c; + break; + } + } + + // translate the coordinates into this component's + // coordinate system + drect.x += c.getX(); + drect.y += c.getY(); + _cbounds.x += c.getX(); + _cbounds.y += c.getY(); + + // clip the dirty region to the bounds of this component + SwingUtilities.computeIntersection( + c.getX(), c.getY(), c.getWidth(), c.getHeight(), drect); + } + + // if we found no opaque parent, just paint the component + // itself (this seems to happen with the top-level layered + // pane) + if (ocomp == null) { + ocomp = comp; + } + + // if this component is rooted in our frame, repaint it into + // the supplied graphics instance + if (root == _root) { + if (DEBUG) { + Log.info("Repainting [comp=" + toString(comp) + + StringUtil.toString(_cbounds) + + ", ocomp=" + toString(ocomp) + + ", drect=" + StringUtil.toString(drect) + "]."); + } + + g.setClip(drect); + g.translate(_cbounds.x, _cbounds.y); + try { + // some components are ill-behaved and may throw an + // exception while painting themselves, and so we + // needs must deal with these fellows gracefully + ocomp.paint(g); + + } catch (Exception e) { + Log.warning("Exception while painting component " + + "[comp=" + ocomp + ", e=" + e + "]."); + Log.logStackTrace(e); + } + g.translate(-_cbounds.x, -_cbounds.y); + + // we also need to repaint any components in this layer + // that are above our freshly repainted component + fmgr.renderLayers((Graphics2D)g, ocomp, _cbounds, _clipped); + + } else if (root != null) { + if (DEBUG) { + Log.info("Repainting old-school " + + "[comp=" + toString(comp) + + ", ocomp=" + toString(ocomp) + + ", root=" + toString(root) + + ", bounds=" + StringUtil.toString(_cbounds) + + "]."); + dumpHierarchy(comp); + } + + // otherwise, repaint with standard swing double buffers + Image obuf = getOffscreenBuffer( + ocomp, _cbounds.width, _cbounds.height); + Graphics og = null, cg = null; + try { + og = obuf.getGraphics(); + ocomp.paint(og); + cg = ocomp.getGraphics(); + cg.drawImage(obuf, 0, 0, null); + + } finally { + if (og != null) { + og.dispose(); + } + if (cg != null) { + cg.dispose(); + } + } + } + } + + // clear out the mapping of dirty components + _spare.clear(); + + return true; + } + + /** + * Used to dump a component when debugging. + */ + protected static String toString (Component comp) + { + return comp.getClass().getName() + + StringUtil.toString(comp.getBounds()); + } + + /** + * Dumps the containment hierarchy for the supplied component. + */ + protected static void dumpHierarchy (Component comp) + { + for (String indent = ""; comp != null; indent += " ") { + Log.info(indent + toString(comp)); + comp = comp.getParent(); + } + } + + /** The root of our interface. */ + protected Component _root; + + /** A list of invalid components. */ + protected Object[] _invalid; + + /** A mapping of invalid rectangles for each widget that is dirty. */ + protected HashMap _dirty = new HashMap(); + + /** A spare hashmap that we swap in while repainting dirty components + * in the old hashmap. */ + protected HashMap _spare = new HashMap(); + + /** Used to compute dirty components' bounds. */ + protected Rectangle _cbounds = new Rectangle(); + + /** Used when rendering "layered" components. */ + protected boolean[] _clipped = new boolean[] { true }; + + /** We debug so much that we have to make it easy to enable and + * disable debug logging. Yay! */ + protected static final boolean DEBUG = false; +} diff --git a/src/java/com/threerings/media/BackFrameManager.java b/src/java/com/threerings/media/BackFrameManager.java new file mode 100644 index 00000000..f8488ac2 --- /dev/null +++ b/src/java/com/threerings/media/BackFrameManager.java @@ -0,0 +1,148 @@ +// +// $Id: BackFrameManager.java 4181 2006-06-07 21:54:12Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Rectangle; +import java.awt.image.VolatileImage; + +/** + * A {@link FrameManager} extension that uses a volatile off-screen image + * to do its rendering. + */ +public class BackFrameManager extends FrameManager +{ + // documentation inherited + protected void paint (long tickStamp) + { + // start out assuming we can do an incremental render + boolean incremental = true; + + do { + GraphicsConfiguration gc = _window.getGraphicsConfiguration(); + + // create our off-screen buffer if necessary + if (_backimg == null || _backimg.getWidth() != _window.getWidth() || + _backimg.getHeight() != _window.getHeight()) { + createBackBuffer(gc); + } + + // make sure our back buffer hasn't disappeared + int valres = _backimg.validate(gc); + + // if we've changed resolutions, recreate the buffer + if (valres == VolatileImage.IMAGE_INCOMPATIBLE) { + Log.info("Back buffer incompatible, recreating."); + createBackBuffer(gc); + } + + // if the image wasn't A-OK, we need to rerender the whole + // business rather than just the dirty parts + if (valres != VolatileImage.IMAGE_OK) { + // Log.info("Lost back buffer, redrawing " + valres); + if (_bgfx != null) { + _bgfx.dispose(); + } + _bgfx = (Graphics2D)_backimg.getGraphics(); + if (_fgfx != null) { + _fgfx.dispose(); + _fgfx = null; + } + incremental = false; + } + + // dirty everything if we're not incrementally rendering + if (!incremental) { + _root.getRootPane().revalidate(); + _root.getRootPane().repaint(); + } + + if (!paint(_bgfx)) { + return; + } + + // we cache our frame's graphics object so that we can avoid + // instantiating a new one on every tick + if (_fgfx == null) { + _fgfx = (Graphics2D)_window.getGraphics(); + } + _fgfx.drawImage(_backimg, 0, 0, null); + + // if we loop through a second time, we'll need to rerender + // everything + incremental = false; + + } while (_backimg.contentsLost()); + } + + // documentation inherited + protected void restoreFromBack (Rectangle dirty) + { + if (_fgfx == null || _backimg == null) { + return; + } +// Log.info("Restoring from back " + StringUtil.toString(dirty) + "."); + _fgfx.setClip(dirty); + _fgfx.drawImage(_backimg, 0, 0, null); + _fgfx.setClip(null); + } + + /** + * Creates the off-screen buffer used to perform double buffered + * rendering of the animated panel. + */ + protected void createBackBuffer (GraphicsConfiguration gc) + { + // if we have an old image, clear it out + if (_backimg != null) { + _backimg.flush(); + _bgfx.dispose(); + } + + // create the offscreen buffer + int width = _window.getWidth(), height = _window.getHeight(); + _backimg = gc.createCompatibleVolatileImage(width, height); + + // fill the back buffer with white + _bgfx = (Graphics2D)_backimg.getGraphics(); + _bgfx.fillRect(0, 0, width, height); + + // clear out our frame graphics in case that became invalid for + // the same reasons our back buffer became invalid + if (_fgfx != null) { + _fgfx.dispose(); + _fgfx = null; + } + +// Log.info("Created back buffer [" + width + "x" + height + "]."); + } + + /** The image used to render off-screen. */ + protected VolatileImage _backimg; + + /** The graphics object from our back buffer. */ + protected Graphics2D _bgfx; + + /** The graphics object from our frame. */ + protected Graphics2D _fgfx; +} diff --git a/src/java/com/threerings/media/FlipFrameManager.java b/src/java/com/threerings/media/FlipFrameManager.java new file mode 100644 index 00000000..0e309b55 --- /dev/null +++ b/src/java/com/threerings/media/FlipFrameManager.java @@ -0,0 +1,101 @@ +// +// $Id: FlipFrameManager.java 4181 2006-06-07 21:54:12Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.AWTException; +import java.awt.BufferCapabilities; +import java.awt.Graphics2D; +import java.awt.ImageCapabilities; +import java.awt.Rectangle; +import java.awt.image.BufferStrategy; + +/** + * A {@link FrameManager} extension that uses a flip-buffer (via {@link + * BufferStrategy} to do its rendering. + */ +public class FlipFrameManager extends FrameManager +{ + // documentation inherited + protected void paint (long tickStamp) + { + // create our buffer strategy if we don't already have one + if (_bufstrat == null) { + BufferCapabilities cap = new BufferCapabilities( + new ImageCapabilities(true), new ImageCapabilities(true), + BufferCapabilities.FlipContents.COPIED); + try { + _window.createBufferStrategy(2, cap); + } catch (AWTException ae) { + Log.warning("Failed creating flip bufstrat: " + ae + "."); + // fall back to one without custom capabilities + _window.createBufferStrategy(2); + } + _bufstrat = _window.getBufferStrategy(); + } + + // start out assuming we can do an incremental render + boolean incremental = true; + + do { + Graphics2D gfx = null; + try { + gfx = (Graphics2D)_bufstrat.getDrawGraphics(); + + // dirty everything if we're not incrementally rendering + if (!incremental) { + Log.info("Doing non-incremental render; contents lost " + + "[lost=" + _bufstrat.contentsLost() + + ", rest=" + _bufstrat.contentsRestored() + "]."); + _root.getRootPane().revalidate(); + _root.getRootPane().repaint(); + } + + // request to paint our participants and components and bail + // if they paint nothing + if (!paint(gfx)) { + return; + } + + // flip our buffer to visible + _bufstrat.show(); + + // if we loop through a second time, we'll need to rerender + // everything + incremental = false; + + } finally { + if (gfx != null) { + gfx.dispose(); + } + } + } while (_bufstrat.contentsLost()); + } + + // documentation inherited + protected void restoreFromBack (Rectangle dirty) + { + // nothing doing + } + + /** The buffer strategy used to do our rendering. */ + protected BufferStrategy _bufstrat; +} diff --git a/src/java/com/threerings/media/FrameInterval.java b/src/java/com/threerings/media/FrameInterval.java new file mode 100644 index 00000000..f168c396 --- /dev/null +++ b/src/java/com/threerings/media/FrameInterval.java @@ -0,0 +1,111 @@ +package com.threerings.media; + +import java.awt.Component; + +import com.threerings.media.FrameManager; +import com.threerings.media.FrameParticipant; + +public abstract class FrameInterval + implements FrameParticipant +{ + /** + * Constructor - registers the interval as a frame participant + */ + public FrameInterval (FrameManager mgr) + { + _mgr = mgr; + } + + // documentation inhertied from FrameParticipant + public Component getComponent () + { + return null; + } + + // documentation inherited from FrameParticipant + public boolean needsPaint () + { + return false; + } + + // documentation inherited from FrameParticipant + public void tick (long tickStamp) + { + if (_nextTime == -1) { + // First time through + _nextTime = tickStamp + _initDelay; + } else if (tickStamp >= _nextTime) { + + // If we're repeating, set the next time to run, otherwise, reset + if (_repeatDelay != 0L) { + _nextTime += _repeatDelay; + } else { + _nextTime = -1; + cancel(); + } + expired(); + } + } + + /** + * + * The main method where your interval should do its work. + * + */ + public abstract void expired (); + + /** + * Schedule the interval to execute once, after the specified delay. + * Supersedes any previous schedule that this Interval may have had. + */ + public final void schedule (long delay) + { + schedule(delay, 0L); + } + + /** + * Schedule the interval to execute repeatedly, with the same delay. + * Supersedes any previous schedule that this Interval may have had. + */ + public final void schedule (long delay, boolean repeat) + { + schedule(delay, repeat ? delay : 0L); + } + + /** + * Schedule the interval to execute repeatedly with the specified + * initial delay and repeat delay. + * Supersedes any previous schedule that this Interval may have had. + */ + public final void schedule (long initialDelay, long repeatDelay) + { + if (!_mgr.isRegisteredFrameParticipant(this)) { + _mgr.registerFrameParticipant(this); + } + + _repeatDelay = repeatDelay; + _initDelay = initialDelay; + _nextTime = -1; + } + + /** + * Cancel the current schedule, and ensure that any expirations that + * are queued up but have not yet run do not run. + */ + public final void cancel () + { + _mgr.removeFrameParticipant(this); + } + + /** Time of the next expiration. */ + protected long _nextTime; + + /** Time between expirations. */ + protected long _repeatDelay; + + /** Time between expirations. */ + protected long _initDelay; + + /** The context whose FrameManager we are using. */ + protected FrameManager _mgr; +} diff --git a/src/java/com/threerings/media/FrameManager.java b/src/java/com/threerings/media/FrameManager.java new file mode 100644 index 00000000..ef73e9e2 --- /dev/null +++ b/src/java/com/threerings/media/FrameManager.java @@ -0,0 +1,846 @@ +// +// $Id: FrameManager.java 4181 2006-06-07 21:54:12Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.applet.Applet; + +import java.awt.Component; +import java.awt.Graphics2D; +import java.awt.GraphicsDevice; +import java.awt.KeyEventDispatcher; +import java.awt.KeyboardFocusManager; +import java.awt.Rectangle; +import java.awt.Window; + +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyEvent; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; + +import java.awt.EventQueue; + +import javax.swing.JLayeredPane; +import javax.swing.RepaintManager; +import javax.swing.JRootPane; +import javax.swing.RootPaneContainer; +import javax.swing.event.AncestorEvent; +import javax.swing.event.AncestorListener; + +import com.samskivert.swing.RuntimeAdjust; +import com.samskivert.util.ListUtil; +import com.samskivert.util.RunAnywhere; +import com.samskivert.util.StringUtil; + +import com.threerings.media.timer.MediaTimer; +import com.threerings.media.timer.SystemMediaTimer; +import com.threerings.media.util.TrailingAverage; +import com.threerings.util.unsafe.Unsafe; + +/** + * Provides a central point from which the computation for each "frame" or tick + * can be dispatched. This assumed that the application structures its activity + * around the rendering of each frame, which is a common architecture for + * games. The animation and sprite support provided by other classes in this + * package are structured for use in an application that uses a frame manager + * to tick everything once per frame. + * + *

The frame manager goes through a simple two part procedure every frame: + * + *

    + *
  • Ticking all of the frame participants: in {@link + * FrameParticipant#tick}, any processing that need be performed during this + * frame should be performed. Care should be taken not to execute code that + * will take unduly long, instead such processing should be broken up so that + * it can be performed in small pieces every frame (or performed on a separate + * thread with the results safely communicated back to the frame participants + * for incorporation into the rendering loop). + * + *
  • Painting the user interface hierarchy: the top-level component (the + * frame) is painted (via a call to {@link JRootPane#paint}) into a flip buffer + * (if supported, an off-screen buffer if not). Updates that were computed + * during the tick should be rendered in this call to paint. The paint call + * will propagate down to all components in the UI hierarchy, some of which may + * be {@link FrameParticipant}s and will have prepared themselves for their + * upcoming painting in the previous call to {@link + * FrameParticipant#tick}. When the call to paint completes, the flip buffer is + * flipped and the process starts all over again.
+ * + *

The ticking and rendering takes place on the AWT thread so as to + * avoid the need for complicated coordination between AWT event handler + * code and frame code. However, this means that all AWT (and Swing) event + * handlers must not perform any complicated processing. After + * each frame, control of the AWT thread is given back to the AWT which + * processes all pending AWT events before giving the frame manager an + * opportunity to process the next frame. Thus the convenience of + * everything running on the AWT thread comes with the price of requiring + * that AWT event handlers not block or perform any intensive processing. + * In general, this is a sensible structure for an application anyhow, so + * this organization tends to be preferable to an organization where the + * AWT and frame threads are separate and must tread lightly so as not to + * collide. + * + *

Note: the way that JScrollPane goes about improving + * performance when scrolling complicated contents cannot work with active + * rendering. If you use a JScrollPane in an application that + * uses the frame manager, you should either use the provided {@link + * SafeScrollPane} or set your scroll panes' viewports to + * SIMPLE_SCROLL_MODE. + */ +public abstract class FrameManager +{ + /** + * Normally, the frame manager will repaint any component in a {@link + * JLayeredPane} layer (popups, overlays, etc.) that overlaps a frame + * participant on every tick because the frame participant could + * have changed underneath the overlay which would require that the overlay + * be repainted. If the application knows that the frame participant + * beneath the overlay will never change, it can have its overlay implement + * this interface and avoid the expense of forcibly fully repainting the + * overlay on every frame. + */ + public static interface SafeLayerComponent + { + } + + /** + * Creates a frame manager that will use a {@link SystemMediaTimer} to + * obtain timing information, which is available on every platform, but + * returns inaccurate time stamps on many platforms. + * + * @see #newInstance(Window, RootPaneContainer, MediaTimer) + */ + public static FrameManager newInstance ( + Window window, RootPaneContainer root) + { + // first try creating a PerfTimer which is the best if we're using + // JDK1.4.2 + MediaTimer timer = null; + try { + timer = (MediaTimer)Class.forName(PERF_TIMER).newInstance(); + } catch (Throwable t) { + Log.info("Can't use PerfTimer (" + t + ") reverting to " + + "System.currentTimeMillis() based timer."); + timer = new SystemMediaTimer(); + } + return newInstance(window, root, timer); + } + + /** + * Constructs a frame manager that will do its rendering to the + * supplied frame. It is likely that the caller will want to have put + * the frame into full-screen exclusive mode prior to providing it to + * the frame manager so that the frame manager can take advantage of + * optimizations available in that mode. + * + * @see GraphicsDevice#setFullScreenWindow + */ + public static FrameManager newInstance ( + Window window, RootPaneContainer root, MediaTimer timer) + { + FrameManager fmgr; + if (_useFlip.getValue()) { + Log.info("Creating flip frame manager."); + fmgr = new FlipFrameManager(); + } else { + Log.info("Creating back frame manager."); + fmgr = new BackFrameManager(); + } + fmgr.init(window, root, timer); + return fmgr; + } + + /** + * Initializes this frame manager and prepares it for operation. + */ + protected void init ( + Window window, RootPaneContainer root, MediaTimer timer) + { + _window = window; + _root = root; + if (window instanceof ManagedJFrame) { + ((ManagedJFrame)window).init(this); + } + _timer = timer; + + // set up our custom repaint manager + _remgr = new ActiveRepaintManager(_window); + RepaintManager.setCurrentManager(_remgr); + + // turn off double buffering for the whole business because we + // handle repaints + _remgr.setDoubleBufferingEnabled(false); + + if (DEBUG_EVENTS) { + addTestListeners(); + } + } + + /** + * Adds a variety of listeners to the frame in order to provide + * visibility into the various events received by the frame. + */ + protected void addTestListeners () + { + // add a test window listener + _window.addWindowListener(new WindowListener() { + public void windowActivated (WindowEvent e) { + Log.info("Window activated [evt=" + e + "]."); + } + + public void windowClosed (WindowEvent e) { + Log.info("Window closed [evt=" + e + "]."); + } + + public void windowClosing (WindowEvent e) { + Log.info("Window closing [evt=" + e + "]."); + } + + public void windowDeactivated (WindowEvent e) { + Log.info("Window deactivated [evt=" + e + "]."); + } + + public void windowDeiconified (WindowEvent e) { + Log.info("Window deiconified [evt=" + e + "]."); + } + + public void windowIconified (WindowEvent e) { + Log.info("Window iconified [evt=" + e + "]."); + } + + public void windowOpened (WindowEvent e) { + Log.info("Window opened [evt=" + e + "]."); + } + }); + + // add a component listener + _window.addComponentListener(new ComponentListener() { + public void componentHidden (ComponentEvent e) { + Log.info("Window component hidden [evt=" + e + "]."); + } + + public void componentShown (ComponentEvent e) { + Log.info("Window component shown [evt=" + e + "]."); + } + + public void componentMoved (ComponentEvent e) { + Log.info("Window component moved [evt=" + e + "]."); + } + + public void componentResized (ComponentEvent e) { + Log.info("Window component resized [evt=" + e + "]."); + } + }); + + // add test ancestor focus listener + _root.getRootPane().addAncestorListener( + new AncestorListener() { + public void ancestorAdded (AncestorEvent e) { + Log.info("Root pane ancestor added [e=" + e + "]."); + } + + public void ancestorRemoved (AncestorEvent e) { + Log.info("Root pane ancestor removed [e=" + e + "]."); + } + + public void ancestorMoved (AncestorEvent e) { + Log.info("Root pane ancestor moved [e=" + e + "]."); + } + }); + + // add test key event dispatcher + KeyboardFocusManager.getCurrentKeyboardFocusManager(). + addKeyEventDispatcher(new KeyEventDispatcher() { + public boolean dispatchKeyEvent (KeyEvent e) { +// if ((e.getModifiersEx() & KeyEvent.ALT_DOWN_MASK) != 0 && +// e.getKeyCode() == KeyEvent.VK_TAB) { +// Log.info("Detected alt-tab key event " + +// "[e=" + e + "]."); +// // attempt to eat the event so that windows +// // doesn't alt-tab into unhappy land +// e.consume(); +// return true; +// } + + return false; + } + }); + } + + /** + * Instructs the frame manager to target the specified number of + * frames per second. If the computation and rendering for a frame are + * completed with time to spare, the frame manager will wait until the + * proper time to begin processing for the next frame. If a frame + * takes longer than its alotted time, the frame manager will + * immediately begin processing on the next frame. + */ + public void setTargetFrameRate (int fps) + { + // compute the number of milliseconds per frame + _millisPerFrame = 1000/fps; + } + + /** + * Registers a frame participant. The participant will be given the + * opportunity to do processing and rendering on each frame. + */ + public void registerFrameParticipant (FrameParticipant participant) + { + Object[] nparts = ListUtil.testAndAddRef(_participants, participant); + if (nparts == null) { + Log.warning("Refusing to add duplicate frame participant! " + + participant); + } else { + _participants = nparts; + } + } + + /** + * Returns true if the specified participant is registered. + */ + public boolean isRegisteredFrameParticipant (FrameParticipant participant) + { + return ListUtil.containsRef(_participants, participant); + } + + /** + * Removes a frame participant. + */ + public void removeFrameParticipant (FrameParticipant participant) + { + ListUtil.clearRef(_participants, participant); + } + + /** + * Returns a millisecond granularity time stamp using the {@link + * MediaTimer} with which this frame manager was configured. + * Note: this should only be called from the AWT thread. + */ + public long getTimeStamp () + { + return _timer.getElapsedMillis(); + } + + /** + * Starts up the per-frame tick + */ + public void start () + { + if (_ticker == null) { + _ticker = new Ticker(); + _ticker.start(); + _lastTickStamp = 0; + } + } + + /** + * Stops the per-frame tick. + */ + public synchronized void stop () + { + if (_ticker != null) { + _ticker.cancel(); + _ticker = null; + } + } + + /** + * Returns true if the tick interval is be running (not necessarily at + * that instant, but in general). + */ + public synchronized boolean isRunning () + { + return (_ticker != null); + } + + /** + * Returns the number of ticks executed in the last second. + */ + public int getPerfTicks () + { + return Math.round(_fps[1]); + } + + /** + * Returns the number of ticks requested in the last second. + */ + public int getPerfTries () + { + return Math.round(_fps[0]); + } + + /** + * Returns debug performance metrics. + */ + public TrailingAverage[] getPerfMetrics () + { + if (_metrics == null) { + _metrics = new TrailingAverage[] { + new TrailingAverage(150), + new TrailingAverage(150), + new TrailingAverage(150) }; + } + return _metrics; + } + + /** + * Called to perform the frame processing and rendering. + */ + protected void tick (long tickStamp) + { + long start = 0L, paint = 0L; + if (MediaPanel._perfDebug.getValue()) { + start = paint = _timer.getElapsedMicros(); + } + // if our frame is not showing (or is impossibly sized), don't try + // rendering anything + if (_window.isShowing() && + _window.getWidth() > 0 && _window.getHeight() > 0) { + // tick our participants + tickParticipants(tickStamp); + paint = _timer.getElapsedMicros(); + // repaint our participants and components + paint(tickStamp); + } + if (MediaPanel._perfDebug.getValue()) { + long end = _timer.getElapsedMicros(); + getPerfMetrics()[1].record((int)(paint-start)/100); + getPerfMetrics()[2].record((int)(end-paint)/100); + } + } + + /** + * Called once per frame to invoke {@link FrameParticipant#tick} on + * all of our frame participants. + */ + protected void tickParticipants (long tickStamp) + { + long gap = tickStamp - _lastTickStamp; + if (_lastTickStamp != 0 && gap > (HANG_DEBUG ? HANG_GAP : BIG_GAP)) { + Log.debug("Long tick delay [delay=" + gap + "ms]."); + } + _lastTickStamp = tickStamp; + + // validate any invalid components + try { + _remgr.validateComponents(); + } catch (Throwable t) { + Log.warning("Failure validating components."); + Log.logStackTrace(t); + } + + // tick all of our frame participants + for (int ii = 0; ii < _participants.length; ii++) { + FrameParticipant part = (FrameParticipant)_participants[ii]; + if (part == null) { + continue; + } + + try { + long start = 0L; + if (HANG_DEBUG) { + start = System.currentTimeMillis(); + } + + part.tick(tickStamp); + + if (HANG_DEBUG) { + long delay = (System.currentTimeMillis() - start); + if (delay > HANG_GAP) { + Log.info("Whoa nelly! Ticker took a long time " + + "[part=" + part + ", time=" + delay + "ms]."); + } + } + + } catch (Throwable t) { + Log.warning("Frame participant choked during tick " + + "[part=" + StringUtil.safeToString(part) + "]."); + Log.logStackTrace(t); + } + } + } + + /** + * Called once per frame to invoke {@link Component#paint} on all of + * our frame participants' components and all dirty components managed + * by our {@link ActiveRepaintManager}. + */ + protected abstract void paint (long tickStamp); + + /** + * Paints our frame participants and any dirty components via the + * repaint manager. + * + * @return true if anything was painted, false if not. + */ + protected boolean paint (Graphics2D gfx) + { + // paint our frame participants (which want to be handled + // specially) + int painted = 0; + for (int ii = 0; ii < _participants.length; ii++) { + FrameParticipant part = (FrameParticipant)_participants[ii]; + if (part == null) { + continue; + } + + Component pcomp = part.getComponent(); + if (pcomp == null || !part.needsPaint()) { + continue; + } + + long start = 0L; + if (HANG_DEBUG) { + start = System.currentTimeMillis(); + } + + // get the bounds of this component + pcomp.getBounds(_tbounds); + + // the bounds adjustment we're about to call will add in the + // components initial bounds offsets, so we remove them here + _tbounds.setLocation(0, 0); + + // convert them into top-level coordinates; also note that if + // this component does not have a valid or visible root, we + // don't want to paint it either + if (getRoot(pcomp, _tbounds) == null) { + continue; + } + + try { + // render this participant; we don't set the clip because + // frame participants are expected to handle clipping + // themselves; otherwise we might pointlessly set the clip + // here, creating a few Rectangle objects in the process, + // only to have the frame participant immediately set the + // clip to something more sensible + gfx.translate(_tbounds.x, _tbounds.y); + pcomp.paint(gfx); + gfx.translate(-_tbounds.x, -_tbounds.y); + painted++; + + } catch (Throwable t) { + String ptos = StringUtil.safeToString(part); + Log.warning("Frame participant choked during paint " + + "[part=" + ptos + "]."); + Log.logStackTrace(t); + } + + // render any components in our layered pane that are not in + // the default layer + _clipped[0] = false; + renderLayers(gfx, pcomp, _tbounds, _clipped); + + if (HANG_DEBUG) { + long delay = (System.currentTimeMillis() - start); + if (delay > HANG_GAP) { + Log.warning("Whoa nelly! Painter took a long time " + + "[part=" + part + ", time=" + delay + "ms]."); + } + } + } + + // repaint any widgets that have declared they need to be + // repainted since the last tick + boolean pcomp = _remgr.paintComponents(gfx, this); + + // let the caller know if anybody painted anything + return ((painted > 0) || pcomp); + } + + /** + * Called by the {@link ManagedJFrame} when our window was hidden and + * reexposed. + */ + protected abstract void restoreFromBack (Rectangle dirty); + + /** + * Renders all components in all {@link JLayeredPane} layers that + * intersect the supplied bounds. + */ + protected void renderLayers (Graphics2D g, Component pcomp, + Rectangle bounds, boolean[] clipped) + { + JLayeredPane lpane = + JLayeredPane.getLayeredPaneAbove(pcomp); + if (lpane != null) { + renderLayer(g, bounds, lpane, clipped, JLayeredPane.PALETTE_LAYER); + renderLayer(g, bounds, lpane, clipped, JLayeredPane.MODAL_LAYER); + renderLayer(g, bounds, lpane, clipped, JLayeredPane.POPUP_LAYER); + renderLayer(g, bounds, lpane, clipped, JLayeredPane.DRAG_LAYER); + } + } + + /** + * Renders all components in the specified layer of the supplied + * layered pane that intersect the supplied bounds. + */ + protected void renderLayer (Graphics2D g, Rectangle bounds, + JLayeredPane pane, boolean[] clipped, + Integer layer) + { + // stop now if there are no components in that layer + int ccount = pane.getComponentCountInLayer(layer.intValue()); + if (ccount == 0) { + return; + } + + // render them up + Component[] comps = pane.getComponentsInLayer(layer.intValue()); + for (int ii = 0; ii < ccount; ii++) { + Component comp = comps[ii]; + if (!comp.isVisible() || comp instanceof SafeLayerComponent) { + continue; + } + + // if this overlay does not intersect the component we just + // rendered, we don't need to repaint it + _tbounds.setBounds(0, 0, comp.getWidth(), comp.getHeight()); + getRoot(comp, _tbounds); + if (!_tbounds.intersects(bounds)) { + continue; + } + + // if the clipping region has not yet been set during this + // render pass, the time has come to do so + if (!clipped[0]) { + g.setClip(bounds); + clipped[0] = true; + } + + // translate into the components coordinate system and render + g.translate(_tbounds.x, _tbounds.y); + try { + comp.paint(g); + } catch (Exception e) { + Log.warning("Component choked while rendering."); + Log.logStackTrace(e); + } + g.translate(-_tbounds.x, -_tbounds.y); + } + } + + // documentation inherited + public void checkpoint (String name, int ticks) + { + Log.info("Frames in last second: " + ticks); + } + + /** + * Returns the root component for the supplied component or null if it + * is not part of a rooted hierarchy or if any parent along the way is + * found to be hidden or without a peer. Along the way, it adjusts the + * supplied component-relative rectangle to be relative to the + * returned root component. + */ + public static Component getRoot (Component comp, Rectangle rect) + { + for (Component c = comp; c != null; c = c.getParent()) { + if (!c.isVisible() || !c.isDisplayable()) { + return null; + } + if (c instanceof Window || c instanceof Applet) { + return c; + } + rect.x += c.getX(); + rect.y += c.getY(); + } + return null; + } + + /** Used to effect periodic calls to {@link #tick}. */ + protected class Ticker extends Thread + { + public void run () + { + Log.info("Frame manager ticker running " + + "[sleepGran=" + _sleepGranularity.getValue() + "]."); + while (_running) { + long start = 0L; + if (MediaPanel._perfDebug.getValue()) { + start = _timer.getElapsedMicros(); + } + Unsafe.sleep(_sleepGranularity.getValue()); + + long woke = _timer.getElapsedMicros(); + if (start > 0L) { + getPerfMetrics()[0].record((int)(woke-start)/100); + int elapsed = (int)(woke-start); + if (elapsed > _sleepGranularity.getValue()*1500) { + Log.warning("Long tick [elapsed=" + elapsed + "us]."); + } + } + + // work around sketchy bug on WinXP that causes the clock + // to leap into the past from time to time + if (woke < _lastAttempt) { + Log.warning("Zoiks! We've leapt into the past, coping " + + "as best we can [dt=" + + (woke - _lastAttempt) + "]."); + _lastAttempt = woke; + } + + if (woke - _lastAttempt >= _millisPerFrame * 1000) { + _lastAttempt = woke; + if (testAndSet()) { + EventQueue.invokeLater(_awtTicker); + } + // else: drop the frame + } + } + } + + public void cancel () + { + _running = false; + } + + protected final synchronized boolean testAndSet () + { + _tries++; + if (!_ticking) { + _ticking = true; + return true; + } + return false; + } + + protected final synchronized void clearTicking (long elapsed) + { + if (++_ticks == 100) { + long time = (elapsed - _lastTick); + _fps[0] = _tries * 1000f / time; + _fps[1] = _ticks * 1000f / time; + _lastTick = elapsed; + _ticks = _tries = 0; + } + _ticking = false; + } + + /** Used to invoke the call to {@link #tick} on the AWT event + * queue thread. */ + protected Runnable _awtTicker = new Runnable () + { + public void run () + { + long elapsed = _timer.getElapsedMillis(); + try { + tick(elapsed); + } finally { + clearTicking(elapsed); + } + } + }; + + /** Used to stick a fork in our ticker when desired. */ + protected transient boolean _running = true; + + /** Used to detect when we need to drop frames. */ + protected boolean _ticking; + + /** The time at which we last attempted to tick. */ + protected long _lastAttempt; + + /** Used to compute metrics. */ + protected int _tries, _ticks, _time; + + /** Used to compute metrics. */ + protected long _lastTick; + }; + + /** The window into which we do our rendering. */ + protected Window _window; + + /** Provides access to our Swing bits. */ + protected RootPaneContainer _root; + + /** Used to obtain timing measurements. */ + protected MediaTimer _timer; + + /** Our custom repaint manager. */ + protected ActiveRepaintManager _remgr; + + /** The number of milliseconds per frame (14 by default, which gives + * an fps of ~71). */ + protected long _millisPerFrame = 14; + + /** Used to track big delays in calls to our tick method. */ + protected long _lastTickStamp; + + /** The thread that dispatches our frame ticks. */ + protected Ticker _ticker; + + /** Used to track and report frames per second. */ + protected float[] _fps = new float[2]; + + /** Used to track performance metrics. */ + protected TrailingAverage[] _metrics; + + /** A temporary bounds rectangle used to avoid lots of object creation. */ + protected Rectangle _tbounds = new Rectangle(); + + /** Used to lazily set the clip when painting popups and other + * "layered" components. */ + protected boolean[] _clipped = new boolean[1]; + + /** The entites that are ticked each frame. */ + protected Object[] _participants = new Object[4]; + + /** If we don't get ticked for 500ms, that's worth complaining about. */ + protected static final long BIG_GAP = 500L; + + /** If we don't get ticked for 100ms and we're hang debugging, + * complain. */ + protected static final long HANG_GAP = 100L; + + /** Enable this to log warnings when ticking or painting takes too + * long. */ + protected static final boolean HANG_DEBUG = false; + + /** A debug hook that toggles debug rendering of sprite paths. */ + protected static RuntimeAdjust.BooleanAdjust _useFlip = + new RuntimeAdjust.BooleanAdjust( + "When active a flip-buffer will be used to manage our " + + "rendering, otherwise a volatile back buffer is used " + + "[requires restart]", "narya.media.frame", + // back buffer rendering doesn't work on the Mac, so we + // default to flip buffer on that platform; we still allow it + // to be toggled so that we can easily test things when they + // release new JVMs + MediaPrefs.config, RunAnywhere.isMacOS()); + + /** Allows us to tweak the sleep granularity. */ + protected static RuntimeAdjust.IntAdjust _sleepGranularity = + new RuntimeAdjust.IntAdjust( + "The number of milliseconds slept before checking to see if " + + "it's time to queue up a new frame tick.", "narya.media.sleep_gran", + MediaPrefs.config, RunAnywhere.isWindows() ? 10 : 7); + + /** Whether to enable AWT event debugging for the frame. */ + protected static final boolean DEBUG_EVENTS = false; + + /** The name of the high-performance timer class we attempt to load. */ + protected static final String PERF_TIMER = + "com.threerings.media.timer.PerfTimer"; +} diff --git a/src/java/com/threerings/media/FrameParticipant.java b/src/java/com/threerings/media/FrameParticipant.java new file mode 100644 index 00000000..9611101d --- /dev/null +++ b/src/java/com/threerings/media/FrameParticipant.java @@ -0,0 +1,64 @@ +// +// $Id: FrameParticipant.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.Component; + +/** + * Provides a mechanism for participating in the frame tick managed by the + * {@link FrameManager}. + */ +public interface FrameParticipant +{ + /** + * This is called on all registered frame participants, one for every + * frame. Following the tick the interface will be rendered, so + * participants can prepare themselves for their upcoming render in + * this method (making use of the timestamp provided for the frame if + * choreography is desired between different participants). + */ + public void tick (long tickStamp); + + /** + * Called immediately prior to {@link #getComponent} and then {@link + * Component#paint} on said component, to determine whether or not + * this frame participant needs to be painted. + */ + public boolean needsPaint (); + + /** + * If a frame participant wishes also to be actively rendered every + * frame rather than use passive rendering (which for Swing, at least, + * is hijacked when using the frame manager such that we take care of + * repainting dirty Swing components every frame into our off-screen + * buffer), it can return a component here which will have {@link + * Component#paint} called on it once per frame with a translated but + * unclipped graphics object. + * + *

Because clipping is expensive in terms of rectangle object + * allocation, frame participants are given the opportunity to do + * their own clipping because they are likely to want to clip to a + * more fine grained region than their entire bounds. If a particpant + * does not wish to be actively rendered, it can safely return null. + */ + public Component getComponent (); +} diff --git a/src/java/com/threerings/media/HourglassView.java b/src/java/com/threerings/media/HourglassView.java new file mode 100644 index 00000000..50dc608c --- /dev/null +++ b/src/java/com/threerings/media/HourglassView.java @@ -0,0 +1,175 @@ +// +// $Id: HourglassView.java 18862 2005-01-27 00:34:51Z tedv $ + +package com.threerings.media; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Shape; + +import javax.swing.JComponent; + +import com.samskivert.util.Interval; +import com.samskivert.util.ResultListener; + +import com.threerings.media.image.Mirage; +import com.threerings.media.util.MultiFrameImage; + +/** + * Displays an hourglass with the sand level representing the amount of + * time remaining. + */ +public class HourglassView extends TimerView +{ + /** + * Constructs an hourglass view. + */ + public HourglassView (FrameManager fmgr, JComponent host, int x, int y, + Mirage glassImage, Mirage topImage, Rectangle topRect, + Mirage botImage, Rectangle botRect, + MultiFrameImage sandTrickle) + { + this(fmgr, host, x, y, glassImage, topImage, topRect, new Point(0, 0), + botImage, botRect, new Point(0, 0), sandTrickle); + } + + /** + * Constructs an hourglass view. + */ + public HourglassView ( + FrameManager fmgr, JComponent host, int x, int y, Mirage glassImage, + Mirage topImage, Rectangle topRect, Point topOff, + Mirage botImage, Rectangle botRect, Point botOff, + MultiFrameImage sandTrickle) + { + super(fmgr, host, new Rectangle(x, y, glassImage.getWidth(), + glassImage.getHeight())); + + // Store relevant coordinate data + _topRect = topRect; + _topOff = topOff; + _botRect = botRect; + _botOff = botOff; + + // Save hourglass images + _hourglass = glassImage; + _sandTop = topImage; + _sandBottom = botImage; + _sandTrickle = sandTrickle; + + // Initialize the falling grain of sand + _sandY = 0; + + // Percentage changes smaller than one pixel in the hourglass + // itself definitely won't be noticed + _changeThreshold = 1.0f / _bounds.height; + } + + // documentation inherited + public void changeComplete (float complete) + { + super.changeComplete(complete); + setSandTrickleY(); + } + + // documentation inherited + public void tick (long tickStamp) + { + // Let the parent handle its stuff + super.tick(tickStamp); + + // check if the sand should be updated + if (_enabled && _running && tickStamp > _sandStamp) { + + // update the sand frame + setSandTrickleY(); + _sandFrame = (_sandFrame + 1) % _sandTrickle.getFrameCount(); + _sandStamp = tickStamp + SAND_RATE; + + // Make sure the timer gets repainted + invalidate(); + } + } + + // documentation inherited + public void paint (Graphics2D gfx, float completed) + { + // Handle processing from parent class + super.paint(gfx, completed); + + // Paint the hourglass + gfx.translate(_bounds.x, _bounds.y); + _hourglass.paint(gfx, 0, 0); + + // Paint the remaining top sand level + Shape oclip = gfx.getClip(); + int top = _topRect.y + (int)(_topRect.height * completed); + gfx.clipRect(_topRect.x, top, _topRect.width, + _topRect.height - (top-_topRect.y)); + _sandTop.paint(gfx, _topOff.x, _topOff.y); + + // paint the current sand frame + gfx.setClip(oclip); + int sandtop = _topRect.y + _topRect.height; + if (_sandY < _botRect.height) { + gfx.clipRect(_botRect.x, sandtop, _botRect.width, _sandY); + } + _sandTrickle.paintFrame(gfx, _sandFrame, + _botRect.x + (_botRect.width - _sandTrickle.getWidth(_sandFrame))/2, + sandtop); + gfx.setClip(oclip); + + // Paint the current bottom sand level + top = getSandBottomTop(completed); + gfx.clipRect(_botRect.x, top, _botRect.width, + _botRect.height-(top-_botRect.y)); + _sandBottom.paint(gfx, _botOff.x, _botOff.y); + gfx.setClip(oclip); + + // untranslate + gfx.translate(-_bounds.x, -_bounds.y); + } + + /** + * Set the y position of the trickle. + */ + protected void setSandTrickleY () + { + _sandY = (int) (_botRect.height * Math.min(1f, (_complete / .025))); + } + + /** + * Returns the current top coordinate of the bottom sand pile. + */ + protected int getSandBottomTop (float completed) + { + return _botRect.y + _botRect.height - + (int)(_botRect.height * completed); + } + + /** The bounds of the sand within the top and bottom sand images. */ + protected Rectangle _topRect, _botRect; + + /** Offsets at which to render the sand images. */ + protected Point _topOff, _botOff; + + /** Our images. */ + protected Mirage _hourglass, _sandTop, _sandBottom; + + /** The sand trickle. */ + protected MultiFrameImage _sandTrickle; + + /** The last time the sand updated moved. */ + protected long _sandStamp; + + /** The next sand frame to be painted. */ + protected int _sandFrame; + + /** The position of the top of the sand. */ + protected int _sandY; + + /** How often the sand frame updates. */ + protected static final long SAND_RATE = 80; +} diff --git a/src/java/com/threerings/media/IconManager.java b/src/java/com/threerings/media/IconManager.java new file mode 100644 index 00000000..c3aedf17 --- /dev/null +++ b/src/java/com/threerings/media/IconManager.java @@ -0,0 +1,183 @@ +// +// $Id: IconManager.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.io.IOException; +import java.util.Properties; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; + +import javax.swing.Icon; +import javax.swing.ImageIcon; + +import com.samskivert.util.ConfigUtil; +import com.samskivert.util.LRUHashMap; +import com.samskivert.util.StringUtil; + +import com.threerings.media.image.ImageUtil; + +import com.threerings.media.tile.TileIcon; +import com.threerings.media.tile.TileManager; +import com.threerings.media.tile.TileSet; + +/** + * Manages the creation of icons from tileset images. The icon manager is + * provided with a configuration file, which maps icon set identifiers to + * uniform tilesets and provides the metric information for said tilesets. + * UI code can subsequently request icons from the icon manager based on + * icon set identifier and index. + * + *

The configuration might look like the following: + * + *

+ * arrows.path = /rsrc/media/icons/arrows.png
+ * arrows.metrics = 20, 25  # icons that are 20 pixels wide and 25 pixels tall
+ *
+ * smileys.path = /rsrc/media/icons/smileys.png
+ * smileys.metrics = 16, 16  # icons that are 16 pixels square
+ * 
+ * + * A user could then request an arrows icon like so: + * + *
+ * Icon icon = iconmgr.getIcon("arrows", 2);
+ * 
+ */ +public class IconManager +{ + /** + * Creates an icon manager that will obtain tilesets from the supplied + * tile manager and which will load its configuration information from + * the specified properties file. + * + * @param tmgr the tile manager to use when fetching tilesets. + * @param configPath the path (relative to the classpath) from which + * the icon manager configuration can be loaded. + * + * @exception IOException thrown if an error occurs loading the + * configuration file. + */ + public IconManager (TileManager tmgr, String configPath) + throws IOException + { + this(tmgr, ConfigUtil.loadProperties(configPath)); + } + + /** + * Creates an icon manager that will obtain tilesets from the supplied + * tile manager and which will read its configuration information from + * the supplied properties file. + */ + public IconManager (TileManager tmgr, Properties config) + { + // save these for later + _tilemgr = tmgr; + _config = config; + } + + /** + * If icon images should be loaded from a set of resource bundles + * rather than the classpath, that set can be set here. + */ + public void setSource (String resourceSet) + { + _rsrcSet = resourceSet; + } + + /** + * Fetches the icon with the specified index from the named icon set. + */ + public Icon getIcon (String iconSet, int index) + { + try { + // see if the tileset is already loaded + TileSet set = (TileSet)_icons.get(iconSet); + + // load it up if not + if (set == null) { + String path = _config.getProperty(iconSet + PATH_SUFFIX); + if (StringUtil.isBlank(path)) { + throw new Exception("No path specified for icon set"); + } + + String metstr = _config.getProperty(iconSet + METRICS_SUFFIX); + if (StringUtil.isBlank(metstr)) { + throw new Exception("No metrics specified for icon set"); + } + + int[] metrics = StringUtil.parseIntArray(metstr); + if (metrics == null || metrics.length != 2) { + throw new Exception("Invalid icon set metrics " + + "[metrics=" + metstr + "]"); + } + + // load up the tileset + if (_rsrcSet == null) { + set = _tilemgr.loadTileSet( + path, metrics[0], metrics[1]); + } else { + set = _tilemgr.loadTileSet( + _rsrcSet, path, metrics[0], metrics[1]); + } + + // cache it + _icons.put(iconSet, set); + } + + // fetch the appropriate image and create an image icon + return new TileIcon(set.getTile(index)); + + } catch (Exception e) { + Log.warning("Unable to load icon [iconSet=" + iconSet + + ", index=" + index + ", error=" + e + "]."); + } + + // return an error icon + return new ImageIcon(ImageUtil.createErrorImage(32, 32)); + } + + /** The tile manager we use to load tilesets. */ + protected TileManager _tilemgr; + + /** Our configuration information. */ + protected Properties _config; + + /** The resource bundle from which we load icon images, or null if + * they should be loaded from the classpath. */ + protected String _rsrcSet; + + /** A cache of our icon tilesets. */ + protected LRUHashMap _icons = new LRUHashMap(ICON_CACHE_SIZE); + + /** The suffix we append to an icon set name to obtain the tileset + * image path configuration parameter. */ + protected static final String PATH_SUFFIX = ".path"; + + /** The suffix we append to an icon set name to obtain the tileset + * metrics configuration parameter. */ + protected static final String METRICS_SUFFIX = ".metrics"; + + /** The maximum number of icon tilesets that may be cached at once. */ + protected static final int ICON_CACHE_SIZE = 10; +} diff --git a/src/java/com/threerings/media/Log.java b/src/java/com/threerings/media/Log.java new file mode 100644 index 00000000..4a98701a --- /dev/null +++ b/src/java/com/threerings/media/Log.java @@ -0,0 +1,63 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +/** + * A placeholder class that contains a reference to the log object used by + * the media services package. + */ +public class Log +{ + public static final String PACKAGE = "media"; + + public static com.samskivert.util.Log log = + new com.samskivert.util.Log(PACKAGE); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } + + public static int getLevel () + { + return com.samskivert.util.Log.getLevel(PACKAGE); + } +} diff --git a/src/java/com/threerings/media/ManagedJFrame.java b/src/java/com/threerings/media/ManagedJFrame.java new file mode 100644 index 00000000..fdc1182e --- /dev/null +++ b/src/java/com/threerings/media/ManagedJFrame.java @@ -0,0 +1,117 @@ +// +// $Id: ManagedJFrame.java 3285 2004-12-28 03:48:40Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.Graphics; +import java.awt.GraphicsConfiguration; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.Shape; + +import javax.swing.JFrame; + +import com.samskivert.util.StringUtil; +import com.threerings.media.Log; + +/** + * When using the {@link FrameManager}, one must use this top-level frame + * class. + */ +public class ManagedJFrame extends JFrame +{ + /** + * Constructs a managed frame with no title. + */ + public ManagedJFrame () + { + this(""); + } + + /** + * Constructs a managed frame with the specified title. + */ + public ManagedJFrame (String title) + { + super(title); + } + + /** + * Constructs a managed frame in the specified graphics configuration. + */ + public ManagedJFrame (GraphicsConfiguration gc) + { + super(gc); + } + + /** + * Called by our frame manager when it's ready to go. + */ + public void init (FrameManager fmgr) + { + _fmgr = fmgr; + } + + /** + * Returns the frame manager managing this frame. + */ + public FrameManager getFrameManager () + { + return _fmgr; + } + + /** + * We catch paint requests and forward them on to the repaint + * infrastructure. + */ + public void paint (Graphics g) + { + update(g); + } + + /** + * We catch update requests and forward them on to the repaint + * infrastructure. + */ + public void update (Graphics g) + { + Shape clip = g.getClip(); + Rectangle dirty; + if (clip != null) { + dirty = clip.getBounds(); + } else { + dirty = getRootPane().getBounds(); + // account for our frame insets + Insets insets = getInsets(); + dirty.x += insets.left; + dirty.y += insets.top; + } + + if (_fmgr != null) { + _fmgr.restoreFromBack(dirty); + } else { + Log.info("Dropping AWT dirty rect " + StringUtil.toString(dirty) + + " [clip=" + StringUtil.toString(clip) + "]."); + } + } + + protected FrameManager _fmgr; +} diff --git a/src/java/com/threerings/media/MediaConstants.java b/src/java/com/threerings/media/MediaConstants.java new file mode 100644 index 00000000..2f063541 --- /dev/null +++ b/src/java/com/threerings/media/MediaConstants.java @@ -0,0 +1,37 @@ +// +// $Id: MediaConstants.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +/** + * Constants specific to the media package. + */ +public interface MediaConstants +{ + /** Identifies the back "layer" of sprites and animations. */ + public static final int BACK = 1; + + /** Identifies the front "layer" of sprites and animations. */ + public static final int FRONT = 2; + + /** Identifies the all "layers" of sprites and animations. */ + public static final int ALL = 3; +} diff --git a/src/java/com/threerings/media/MediaPanel.java b/src/java/com/threerings/media/MediaPanel.java new file mode 100644 index 00000000..abe2e174 --- /dev/null +++ b/src/java/com/threerings/media/MediaPanel.java @@ -0,0 +1,783 @@ +// +// $Id: MediaPanel.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.Shape; + +import java.awt.event.ActionEvent; +import java.awt.event.MouseEvent; + +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import javax.swing.event.AncestorEvent; +import javax.swing.event.MouseInputAdapter; + +import java.util.Arrays; +import java.util.ArrayList; + +import com.samskivert.swing.Controller; +import com.samskivert.swing.Label; +import com.samskivert.swing.RuntimeAdjust; +import com.samskivert.swing.event.AncestorAdapter; +import com.samskivert.swing.event.CommandEvent; +import com.samskivert.util.IntListUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.media.timer.MediaTimer; + +import com.threerings.media.animation.Animation; +import com.threerings.media.animation.AnimationManager; + +import com.threerings.media.sprite.ButtonSprite; +import com.threerings.media.sprite.Sprite; +import com.threerings.media.sprite.SpriteManager; + +import com.threerings.media.sprite.action.ActionSprite; +import com.threerings.media.sprite.action.ArmingSprite; +import com.threerings.media.sprite.action.CommandSprite; +import com.threerings.media.sprite.action.DisableableSprite; +import com.threerings.media.sprite.action.HoverSprite; + +/** + * Provides a useful extensible framework for rendering animated displays + * that use sprites and animations. Sprites and animations can be added to + * this panel and they will automatically be ticked and rendered (see + * {@link #addSprite} and {@link #addAnimation}). + * + *

To facilitate optimized sprite and animation rendering, the panel + * provides a dirty region manager which is used to only repaint dirtied + * regions on each frame. Derived classes can add dirty regions to the + * scene and/or augment the dirty regions created by moving sprites and + * changing animations. + * + *

Sprite and animation rendering is done in two layers: front and + * back. Callbacks are provided to render behind the back layer ({@link + * #paintBehind}), in between front and back ({@link #paintBetween}) and + * in front of the front layer ({@link #paintInFront}). + * + *

The animated panel automatically registers with the {@link + * FrameManager} to participate in the frame tick. It only does so while + * it is a visible part of the UI hierarchy, so animations and sprites are + * paused while the animated panel is hidden. + */ +public class MediaPanel extends JComponent + implements FrameParticipant, MediaConstants +{ + /** + * Constructs a media panel. + */ + public MediaPanel (FrameManager framemgr) + { + // keep this for later + _framemgr = framemgr; + setOpaque(true); // our repaints shouldn't cause other jcomponents to + + // create our region manager + _remgr = new RegionManager(); + + // create our animation and sprite managers + _animmgr = new AnimationManager(this); + _spritemgr = new SpriteManager(this); + + // participate in the frame when we're visible + addAncestorListener(new AncestorAdapter() { + public void ancestorAdded (AncestorEvent event) { + _framemgr.registerFrameParticipant(MediaPanel.this); + } + public void ancestorRemoved (AncestorEvent event) { + _framemgr.removeFrameParticipant(MediaPanel.this); + } + }); + } + + /** + * Get the bounds of the viewport, in media coordinates. For the base + * MediaPanel, this will always be (0, 0, width, height). + */ + public Rectangle getViewBounds () + { + return new Rectangle(getWidth(), getHeight()); + } + + /** + * Returns a reference to the animation manager used by this media + * panel. + */ + public AnimationManager getAnimationManager () + { + return _animmgr; + } + + /** + * Returns a reference to the sprite manager used by this media panel. + */ + public SpriteManager getSpriteManager () + { + return _spritemgr; + } + + /** + * Pauses the sprites and animations that are currently active on this + * media panel. Also stops listening to the frame tick while paused. + */ + public void setPaused (boolean paused) + { + // sanity check + if ((paused && (_pauseTime != 0)) || + (!paused && (_pauseTime == 0))) { + Log.warning("Requested to pause when paused or vice-versa " + + "[paused=" + paused + "]."); + return; + } + + if (_paused = paused) { + // make a note of our pause time + _pauseTime = _framemgr.getTimeStamp(); + + } else { + // let the animation and sprite managers know that we just + // warped into the future + long delta = _framemgr.getTimeStamp() - _pauseTime; + _animmgr.fastForward(delta); + _spritemgr.fastForward(delta); + + // clear out our pause time + _pauseTime = 0; + } + } + + /** + * Returns a reference to the region manager that is used to + * accumulate dirty regions each frame. + */ + public RegionManager getRegionManager () + { + return _remgr; + } + + /** + * Returns a timestamp from the {@link MediaTimer} used to track time + * intervals for this media panel. Note: this should only be + * called from the AWT thread. + */ + public long getTimeStamp () + { + return _framemgr.getTimeStamp(); + } + + // documentation inherited from interface + public void getPerformanceStatus (StringBuilder buf) + { + } + + /** + * Adds a sprite to this panel. + */ + public void addSprite (Sprite sprite) + { + _spritemgr.addSprite(sprite); + + if (((sprite instanceof ActionSprite) || + (sprite instanceof HoverSprite)) && (_actionSpriteCount++ == 0)) { + if (_actionHandler == null) { + _actionHandler = createActionSpriteHandler(); + } + addMouseListener(_actionHandler); + addMouseMotionListener(_actionHandler); + } + } + + /** + * @return true if the sprite is already added to this panel. + */ + public boolean isManaged (Sprite sprite) + { + return _spritemgr.isManaged(sprite); + } + + /** + * Removes a sprite from this panel. + */ + public void removeSprite (Sprite sprite) + { + _spritemgr.removeSprite(sprite); + + if (((sprite instanceof ActionSprite) || + (sprite instanceof HoverSprite)) && (--_actionSpriteCount == 0)) { + removeMouseListener(_actionHandler); + removeMouseMotionListener(_actionHandler); + } + } + + /** + * Removes all sprites from this panel. + */ + public void clearSprites () + { + _spritemgr.clearMedia(); + + if (_actionHandler != null) { + removeMouseListener(_actionHandler); + removeMouseMotionListener(_actionHandler); + _actionSpriteCount = 0; + } + } + + /** + * Adds an animation to this panel. Animations are automatically + * removed when they finish. + */ + public void addAnimation (Animation anim) + { + _animmgr.registerAnimation(anim); + } + + /** + * @return true if the animation is already added to this panel. + */ + public boolean isManaged (Animation anim) + { + return _animmgr.isManaged(anim); + } + + /** + * Aborts a currently running animation and removes it from this + * panel. Animations are normally automatically removed when they + * finish. + */ + public void abortAnimation (Animation anim) + { + _animmgr.unregisterAnimation(anim); + } + + /** + * Removes all animations from this panel. + */ + public void clearAnimations () + { + _animmgr.clearMedia(); + } + + // documentation inherited from interface + public void tick (long tickStamp) + { + // bail if ticking is currently disabled + if (_paused) { + return; + } + + // let derived classes do their business + willTick(tickStamp); + + // now tick our animations and sprites + _animmgr.tick(tickStamp); + _spritemgr.tick(tickStamp); + + // if performance debugging is enabled, + if (_perfDebug.getValue()) { + if (_perfLabel == null) { + _perfLabel = new Label( + "", Label.OUTLINE, Color.white, Color.black, + new Font("Arial", Font.PLAIN, 10)); + } + if (_perfRect == null) { + _perfRect = new Rectangle(5, 5, 0, 0); + } + + StringBuilder perf = new StringBuilder(); + perf.append("[FPS: "); + perf.append(_framemgr.getPerfTicks()).append("/"); + perf.append(_framemgr.getPerfTries()); + perf.append(" PM:"); + StringUtil.toString(perf, _framemgr.getPerfMetrics()); +// perf.append(" MP:").append(_dirtyPerTick); + perf.append("]"); + + String perfStatus = perf.toString(); + if (!_perfStatus.equals(perfStatus)) { + _perfStatus = perfStatus; + _perfLabel.setText(perfStatus); + + Graphics2D gfx = (Graphics2D)getGraphics(); + if (gfx != null) { + _perfLabel.layout(gfx); + gfx.dispose(); + + // make sure the region we dirty contains the old and + // the new text (which we ensure by never letting the + // rect shrink) + Dimension psize = _perfLabel.getSize(); + _perfRect.width = Math.max(_perfRect.width, psize.width); + _perfRect.height = Math.max(_perfRect.height, psize.height); + dirtyScreenRect(new Rectangle(_perfRect)); + } + } + } + + // let derived classes do their business + didTick(tickStamp); + + // make a note that the next paint will correspond to a call to + // tick() + _tickPaintPending = true; + } + + /** + * Derived classes can override this method and perform computation + * prior to the ticking of the sprite and animation managers. + */ + protected void willTick (long tickStamp) + { + } + + /** + * Derived classes can override this method and perform computation + * subsequent to the ticking of the sprite and animation managers. + */ + protected void didTick (long tickStamp) + { + } + + // documentation inherited from interface + public boolean needsPaint () + { + // compute our average dirty regions per tick + if (_tick++ == 99) { + _tick = 0; + int dirty = IntListUtil.sum(_dirty); + Arrays.fill(_dirty, 0); + _dirtyPerTick = (float)dirty/100; + } + + // if we have no dirty regions, clear our pending tick indicator + // because we're not going to get painted + boolean needsPaint = _remgr.haveDirtyRegions(); + if (!needsPaint) { + _tickPaintPending = false; + } + + // regardless of whether or not we paint, we need to let our + // abstract media managers know that we've gotten to the point of + // painting because they need to remain prepared to deal with + // media changes that happen any time between the tick() and the + // paint() and thus need to know when paint() happens + _animmgr.willPaint(); + _spritemgr.willPaint(); + + return needsPaint; + } + + // documentation inherited from interface + public Component getComponent () + { + return this; + } + + // documentation inherited + public void setOpaque (boolean opaque) + { + if (!opaque) { + Log.warning("Media panels shouldn't be setOpaque(false)."); + Thread.dumpStack(); + } + super.setOpaque(true); + } + + // documentation inherited + public void repaint (long tm, int x, int y, int width, int height) + { + if (width > 0 && height > 0) { + dirtyScreenRect(new Rectangle(x, y, width, height)); + } + } + + // documentation inherited + public void paint (Graphics g) + { + Graphics2D gfx = (Graphics2D)g; + + // no use in painting if we're not showing or if we've not yet + // been validated + if (!isValid() || !isShowing()) { + return; + } + + // if this isn't a tick paint, then we need to grab the clipping + // rectangle and mark it as dirty + if (!_tickPaintPending) { + Shape clip = g.getClip(); + if (clip == null) { + // mark the whole component as dirty + repaint(); + } else { + dirtyScreenRect(clip.getBounds()); + } + + // we used to bail out here and not render until our next + // tick, but it turns out that we need to render here because + // Swing may have repainted our parent over us and expect that + // we're going to paint ourselves on top of whatever it just + // painted, so we go ahead and paint now to avoid flashing + + } else { + _tickPaintPending = false; + } + + // if we have no invalid rects, there's no need to repaint + if (!_remgr.haveDirtyRegions()) { + return; + } + + // get our dirty rectangles and delegate the main painting to a + // method that can be more easily overridden + Rectangle[] dirty = _remgr.getDirtyRegions(); + _dirty[_tick] = dirty.length; + try { + paint(gfx, dirty); + } catch (Throwable t) { + Log.warning(this + " choked in paint(" + dirty + ")."); + Log.logStackTrace(t); + } + + // render our performance debugging if it's enabled + if (_perfRect != null && _perfDebug.getValue()) { + gfx.setClip(null); + _perfLabel.render(gfx, _perfRect.x, _perfRect.y); + } + } + + /** + * Performs the actual painting of the media panel. Derived methods + * can override this method if they wish to perform pre- and/or + * post-paint activities or if they wish to provide their own painting + * mechanism entirely. + */ + protected void paint (Graphics2D gfx, Rectangle[] dirty) + { + int dcount = dirty.length; + + for (int ii = 0; ii < dcount; ii++) { + Rectangle clip = dirty[ii]; + // sanity-check the dirty rectangle + if (clip == null) { + Log.warning("Found null dirty rect painting media panel?!"); + Thread.dumpStack(); + continue; + } + + // constrain this dirty region to the bounds of the component + constrainToBounds(clip); + + // ignore rectangles that were reduced to nothingness + if (clip.width == 0 || clip.height == 0) { + continue; + } + + // clip to this dirty region + clipToDirtyRegion(gfx, clip); + + // paint the region + paintDirtyRect(gfx, clip); + } + } + + /** + * Paints all the layers of the specified dirty region. + */ + protected void paintDirtyRect (Graphics2D gfx, Rectangle rect) + { + // paint the behind the scenes stuff + paintBehind(gfx, rect); + + // paint back sprites and animations + paintBits(gfx, AnimationManager.BACK, rect); + + // paint the between the scenes stuff + paintBetween(gfx, rect); + + // paint front sprites and animations + paintBits(gfx, AnimationManager.FRONT, rect); + + // paint anything in front + paintInFront(gfx, rect); + } + + /** + * Called by the main rendering code to constrain this dirty rectangle + * to the bounds of the media panel. If a derived class is using dirty + * rectangles that live in some sort of virtual coordinate system, + * they'll want to override this method and constraint the rectangles + * properly. + */ + protected void constrainToBounds (Rectangle dirty) + { + SwingUtilities.computeIntersection( + 0, 0, getWidth(), getHeight(), dirty); + } + + /** + * This is called to clip the rendering output to the supplied dirty + * region. This should use {@link Graphics#setClip} because the + * clipping region will need to be replaced as we iterate through our + * dirty regions. By default, a region is assumed to represent screen + * coordinates, but if a derived class wishes to maintain dirty + * regions in non-screen coordinates, it can override this method to + * properly clip to the dirty region. + */ + protected void clipToDirtyRegion (Graphics2D gfx, Rectangle dirty) + { +// Log.info("MP: Clipping to [clip=" + StringUtil.toString(dirty) + "]."); + gfx.setClip(dirty); + } + + /** + * Called to mark the specified rectangle (in screen coordinates) as + * dirty. The rectangle will be bent, folded and mutilated, so be sure + * you're not passing a rectangle into this method that is being used + * elsewhere. + * + *

If derived classes wish to convert from screen coordinates to + * some virtual coordinate system to be used by their repaint manager, + * this is the place to do it. + */ + protected void dirtyScreenRect (Rectangle rect) + { + _remgr.addDirtyRegion(rect); + } + + /** + * Paints behind all sprites and animations. The supplied invalid + * rectangle should be redrawn in the supplied graphics context. + * Sub-classes should override this method to do the actual rendering + * for their display. + */ + protected void paintBehind (Graphics2D gfx, Rectangle dirtyRect) + { + } + + /** + * Paints between the front and back layer of sprites and animations. + * The supplied invalid rectangle should be redrawn in the supplied + * graphics context. Sub-classes should override this method to do the + * actual rendering for their display. + */ + protected void paintBetween (Graphics2D gfx, Rectangle dirtyRect) + { + } + + /** + * Paints in front of all sprites and animations. The supplied invalid + * rectangle should be redrawn in the supplied graphics context. + * Sub-classes should override this method to do the actual rendering + * for their display. + */ + protected void paintInFront (Graphics2D gfx, Rectangle dirtyRect) + { + } + + /** + * Renders the sprites and animations that intersect the supplied + * dirty region in the specified layer. Derived classes can override + * this method if they need to do custom sprite or animation rendering + * (if they need to do special sprite z-order handling, for example). + * The clipping region will already be set appropriately. + */ + protected void paintBits (Graphics2D gfx, int layer, Rectangle dirty) + { + if (layer == FRONT) { + _spritemgr.paint(gfx, layer, dirty); + _animmgr.paint(gfx, layer, dirty); + } else { + _animmgr.paint(gfx, layer, dirty); + _spritemgr.paint(gfx, layer, dirty); + } + } + + /** + * Creates the mouse listener that will handle action sprites and their + * variants. + */ + protected ActionSpriteHandler createActionSpriteHandler () + { + return new ActionSpriteHandler(); + } + + /** The frame manager with whom we register. */ + protected FrameManager _framemgr; + + /** The animation manager in use by this panel. */ + protected AnimationManager _animmgr; + + /** The sprite manager in use by this panel. */ + protected SpriteManager _spritemgr; + + /** Used to accumulate and merge dirty regions on each tick. */ + protected RegionManager _remgr; + + /** Used to correlate tick()s with paint()s. */ + protected boolean _tickPaintPending = false; + + /** Whether we're currently paused. */ + protected boolean _paused; + + /** Used to track the clock time at which we were paused. */ + protected long _pauseTime; + + /** Used to keep metrics. */ + protected int[] _dirty = new int[200]; + + /** Used to keep metrics. */ + protected int _tick; + + /** Used to keep metrics. */ + protected float _dirtyPerTick; + + /** Handles ActionSprite/HoverSprite/ArmingSprite manipulation. */ + protected class ActionSpriteHandler extends MouseInputAdapter + { + // documentation inherited + public void mousePressed (MouseEvent me) + { + if (_activeSprite == null) { + // see if we can find one + Sprite s = getHit(me); + if (s instanceof ActionSprite) { + _activeSprite = s; + } + } + + if (_activeSprite instanceof ArmingSprite) { + ((ArmingSprite) _activeSprite).setArmed(true); + } + } + + // documentation inherited + public void mouseReleased (MouseEvent me) + { + if (_activeSprite instanceof ArmingSprite) { + ((ArmingSprite)_activeSprite).setArmed(false); + } + + if ((_activeSprite instanceof ActionSprite) && + _activeSprite.hitTest(me.getX(), me.getY())) { + ActionEvent event; + if (_activeSprite instanceof CommandSprite) { + CommandSprite cs = (CommandSprite) _activeSprite; + event = new CommandEvent( + MediaPanel.this, cs.getActionCommand(), + cs.getCommandArgument(), me.getWhen(), + me.getModifiers()); + + } else { + ActionSprite as = (ActionSprite) _activeSprite; + event = new ActionEvent( + MediaPanel.this, ActionEvent.ACTION_PERFORMED, + as.getActionCommand(), me.getWhen(), me.getModifiers()); + } + Controller.postAction(event); + } + + if (!(_activeSprite instanceof HoverSprite)) { + _activeSprite = null; + } + + mouseMoved(me); + } + + // documentation inherited + public void mouseDragged (MouseEvent me) + { + if (_activeSprite instanceof ArmingSprite) { + ((ArmingSprite) _activeSprite).setArmed( + _activeSprite.hitTest(me.getX(), me.getY())); + } + } + + // documentation inherited + public void mouseMoved (MouseEvent me) + { + Sprite s = getHit(me); + if (_activeSprite == s) { + return; + } + if (_activeSprite instanceof HoverSprite) { + ((HoverSprite) _activeSprite).setHovered(false); + } + _activeSprite = s; + if (_activeSprite instanceof HoverSprite) { + ((HoverSprite) _activeSprite).setHovered(true); + } + } + + /** + * Utility method, get the highest non-disabled action/hover sprite. + */ + protected Sprite getHit (MouseEvent me) + { + ArrayList list = new ArrayList(); + _spritemgr.getHitSprites(list, me.getX(), me.getY()); + for (int ii = 0, nn = list.size(); ii < nn; ii++) { + Object o = list.get(ii); + if ((o instanceof HoverSprite || o instanceof ActionSprite) && + (!(o instanceof DisableableSprite) || + ((DisableableSprite) o).isEnabled())) { + return (Sprite) o; + } + } + return null; + } + + /** The active hover sprite, or action sprite. */ + protected Sprite _activeSprite; + } + + /** The action sprite handler, or null for none. */ + protected ActionSpriteHandler _actionHandler; + + /** The number of action/hover sprites being managed. */ + protected int _actionSpriteCount; + + // used to render performance metrics + protected String _perfStatus = ""; + protected Label _perfLabel; + protected static Rectangle _perfRect; + + /** A debug hook that toggles FPS rendering. */ + protected static RuntimeAdjust.BooleanAdjust _perfDebug = + new RuntimeAdjust.BooleanAdjust( + "Toggles frames per second and dirty regions per tick rendering.", + "narya.media.fps_display", MediaPrefs.config, false) { + protected void adjusted (boolean newValue) { + // clear out some things if we're turned off + if (!newValue) { + _perfRect = null; + } + } + }; +} diff --git a/src/java/com/threerings/media/MediaPrefs.java b/src/java/com/threerings/media/MediaPrefs.java new file mode 100644 index 00000000..1cd06317 --- /dev/null +++ b/src/java/com/threerings/media/MediaPrefs.java @@ -0,0 +1,35 @@ +// +// $Id: MediaPrefs.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import com.samskivert.util.Config; + +/** + * Provides access to runtime configuration parameters for the media + * package and its subpackages. + */ +public class MediaPrefs +{ + /** Used to load our preferences from a properties file and map them + * to the persistent Java preferences repository. */ + public static Config config = new Config("rsrc/config/media"); +} diff --git a/src/java/com/threerings/media/RegionManager.java b/src/java/com/threerings/media/RegionManager.java new file mode 100644 index 00000000..72df0423 --- /dev/null +++ b/src/java/com/threerings/media/RegionManager.java @@ -0,0 +1,165 @@ +// +// $Id: RegionManager.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.EventQueue; +import java.awt.Rectangle; + +import java.util.ArrayList; + +import com.samskivert.util.StringUtil; + +/** + * Manages regions (rectangles) that are invalidated in the process of + * ticking animations and sprites and generally doing other display + * related business. + */ +public class RegionManager +{ + /** + * Invalidates the specified region. + */ + public void invalidateRegion (int x, int y, int width, int height) + { + if (isValidSize(width, height)) { + addDirtyRegion(new Rectangle(x, y, width, height)); + } + } + + /** + * Invalidates the specified region (the supplied rectangle will be + * cloned as the region manager fiddles with the rectangles it uses + * internally). + */ + public void invalidateRegion (Rectangle rect) + { + if (isValidSize(rect.width, rect.height)) { + addDirtyRegion((Rectangle)rect.clone()); + } + } + + /** + * Adds the supplied rectangle to the dirty regions. Control of the + * rectangle is given to the region manager as it may choose to bend, + * fold or mutilate it later. If you don't want the region manager + * messing with your rectangle, use {@link #invalidateRegion}. + */ + public void addDirtyRegion (Rectangle rect) + { + // make sure we're on an AWT thread + if (!EventQueue.isDispatchThread()) { + Log.warning("Oi! Region dirtied on non-AWT thread " + + "[rect=" + rect + "]."); + Thread.dumpStack(); + } + + // sanity check + if (rect == null) { + Log.warning("Attempt to dirty a null rect!?"); + Thread.dumpStack(); + return; + } + + // more sanity checking + long x = rect.x, y = rect.y; + if ((Math.abs(x) > Integer.MAX_VALUE/2) || + (Math.abs(y) > Integer.MAX_VALUE/2)) { + Log.warning("Requested to dirty questionable region " + + "[rect=" + StringUtil.toString(rect) + "]."); + if (Log.getLevel() == Log.log.DEBUG) { + Thread.dumpStack(); + } + return; // Let's not do it! + } + + if (isValidSize(rect.width, rect.height)) { + // Log.info("Invalidating " + StringUtil.toString(rect)); + _dirty.add(rect); + } + } + + /** Used to ensure our dirty regions are not invalid. */ + protected final boolean isValidSize (int width, int height) + { + if (width < 0 || height < 0) { + Log.warning("Attempt to add invalid dirty region?! " + + "[size=" + width + "x" + height + "]."); + Thread.dumpStack(); + return false; + + } else if (width == 0 || height == 0) { + // no need to complain about zero sized rectangles, just + // ignore them + return false; + + } else { + return true; + } + } + + /** + * Returns true if dirty regions have been accumulated since the last + * call to {@link #getDirtyRegions}. + */ + public boolean haveDirtyRegions () + { + return (_dirty.size() > 0); + } + + /** + * Merges all outstanding dirty regions into a single list of + * rectangles and returns that to the caller. Interally, the list of + * accumulated dirty regions is cleared out and prepared for the next + * frame. + */ + public Rectangle[] getDirtyRegions () + { + ArrayList merged = new ArrayList(); + + for (int ii = _dirty.size() - 1; ii >= 0; ii--) { + // pop the next rectangle from the dirty list + Rectangle mr = (Rectangle)_dirty.remove(ii); + + // merge in any overlapping rectangles + for (int jj = ii - 1; jj >= 0; jj--) { + Rectangle r = (Rectangle)_dirty.get(jj); + if (mr.intersects(r)) { + // remove the overlapping rectangle from the list + _dirty.remove(jj); + ii--; + // grow the merged dirty rectangle + mr.add(r); + } + } + + // add the merged rectangle to the list + merged.add(mr); + } + + Rectangle[] rects = new Rectangle[merged.size()]; + merged.toArray(rects); + return rects; + } + + /** A list of dirty rectangles. */ + protected ArrayList _dirty = new ArrayList(); +} diff --git a/src/java/com/threerings/media/SafeScrollPane.java b/src/java/com/threerings/media/SafeScrollPane.java new file mode 100644 index 00000000..0e978cae --- /dev/null +++ b/src/java/com/threerings/media/SafeScrollPane.java @@ -0,0 +1,89 @@ +// +// $Id: SafeScrollPane.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Point; + +import javax.swing.JComponent; +import javax.swing.JScrollPane; +import javax.swing.JViewport; + +/** + * A scroll pane that is safe to use in frame managed views. + */ +public class SafeScrollPane extends JScrollPane +{ + public SafeScrollPane () + { + } + + public SafeScrollPane (Component view) + { + super(view); + } + + public SafeScrollPane (Component view, int owidth, int oheight) + { + super(view); + if (owidth != 0 || oheight != 0) { + _override = new Dimension(owidth, oheight); + } + } + + // documentation inherited + public Dimension getPreferredSize () + { + Dimension d = super.getPreferredSize(); + if (_override != null) { + if (_override.width != 0) { + d.width = _override.width; + } + if (_override.height != 0) { + d.height = _override.height; + } + } + return d; + } + + protected JViewport createViewport () + { + JViewport vp = new JViewport() { + public void setViewPosition (Point p) { + super.setViewPosition(p); + // simple scroll mode results in setViewPosition causing + // our view to become invalid, but nothing ever happens to + // queue up a revalidate for said view, so we have to do + // it here + Component c = getView(); + if (c instanceof JComponent) { + ((JComponent)c).revalidate(); + } + } + }; + vp.setScrollMode(JViewport.SIMPLE_SCROLL_MODE); + return vp; + } + + protected Dimension _override; +} diff --git a/src/java/com/threerings/media/TimerView.java b/src/java/com/threerings/media/TimerView.java new file mode 100644 index 00000000..c3767e32 --- /dev/null +++ b/src/java/com/threerings/media/TimerView.java @@ -0,0 +1,513 @@ +// +// $Id: HourglassView.java 18697 2005-01-19 01:18:47Z tedv $ + +package com.threerings.media; + +import java.awt.Component; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; + +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; + +import javax.swing.JComponent; +import javax.swing.event.AncestorEvent; + +import com.samskivert.swing.event.AncestorAdapter; +import com.samskivert.util.ResultListener; + +/** + * A generic timer class that can be rendered on screen. + * + * NOTE: This base class doesn't actually render anything, but it's still + * useful for triggering user supplied callback functions. Derived classes + * are more than welcome to setup their own rendering, of course. + */ +public class TimerView + implements FrameParticipant, HierarchyListener +{ + /** + * Constructs a timer view that fires at the default rate. + */ + public TimerView (FrameManager fmgr, JComponent host, Rectangle bounds) + { + // Cache the input arguments + _fmgr = fmgr; + _host = host; + _bounds = new Rectangle(bounds); + + // Watch for changes in the timer's state so it can effectively + // register or unregister with the frame manager as necessary + _host.addHierarchyListener(this); + checkFrameParticipation(); + } + + /** + * Sets whether this timer should be rendered. + */ + public void setEnabled (boolean enabled) + { + if (_enabled != enabled) + { + _enabled = enabled; + invalidate(); + } + } + + /** + * Get the amount of time it takes to render digital changes in the + * completion state. + */ + public long getTransitionTime () + { + return _transitionTime; + } + + /** + * Set the amount of time it takes to render digital changes in the + * completion state (probably due to a timer reset). + */ + public void setTransitionTime (long time) + { + _transitionTime = Math.max(0, time); + } + + /** + * Setup a warning to trigger after the timer is "warnPercent" + * or more completed. If "warner" is non-null, it will be + * called at that time. + */ + public void setWarning (float warnPercent, ResultListener warner) + { + // This warning hasn't triggered yet + _warned = false; + + // Here are the details + _warnPercent = warnPercent; + _warner = warner; + } + + /** + * Remove any warning this timer might have had. + */ + public void removeWarning () + { + // Don't trigger any warnings + _warned = false; + _warnPercent = -1; + _warner = null; + } + + /** Test if the timer is running right now. */ + public boolean running () + { + return _running; + } + + /** + * Start the timer running from the specified percentage complete, + * to expire at the specified time. + * + * @param startPercent a value in [0f, 1f) indicating how much + * hourglass time has already elapsed when the timer starts. + * @param duration The time interval over which the timer would + * run if it started at 0%. + * @param finisher a listener that will be notified when the timer + * finishes, or null if nothing should be notified. + */ + public void start (float startPercent, long duration, + ResultListener finisher) + { + // Sanity check input arguments + if (startPercent < 0.0f || startPercent >= 1.0f) { + throw new IllegalArgumentException( + "Invalid starting percent " + startPercent); + } + if (duration < 0) { + throw new IllegalArgumentException("Invalid duration " + duration); + } + + // Stop any current processing + stop(); + + // Record the timer's full duration and effective start time + _duration = duration; + + // Change the completion percent and make sure the starting + // time gets updated on the next tick + changeComplete(startPercent); + _start = Long.MIN_VALUE; + + // Thank you sir; would you kindly take a chair in the waiting room? + _finisher = finisher; + + // The warning and completion handlers haven't been triggered yet + _warned = false; + _completed = false; + + // Start things running + _running = true; + } + + /** + * Reset the timer. + */ + public void reset () + { + // Stop processing permanently + stop(); + + // Reset the completion percent + changeComplete(0f); + } + + /** + * Stop the timer. + */ + public void stop () + { + // Stop processing + pause(); + + // Prevent it from being unpaused + _lastUpdate = Long.MIN_VALUE; + } + + /** + * Pause the timer from processing. + */ + public void pause () + { + // Stop processing + _running = false; + } + + /** + * Unpause the timer. + */ + public void unpause () + { + // Don't unpause the timer if it wasn't paused to begin with + if (_lastUpdate == Long.MIN_VALUE || _start == Long.MIN_VALUE) { + return; + } + + // Adjust the starting time when the timer next ticks + _processUnpause = true; + + // Start things running again + _running = true; + } + + /** + * Generate an unexpected change in the timer's completion and + * set up the necessary details to render interpolation from the old + * to new states + */ + public void changeComplete (float complete) + { + // Store the new state + _complete = complete; + + // Determine the percentage difference between the "actual" + // completion state and the completion amount to render during + // the smooth interpolation + _renderOffset = _renderComplete - _complete; + + // When the timer next ticks, find out when this interpolation + // should start + _renderOffsetTime = Long.MIN_VALUE; + } + + /** + * Updates the timer for the current inputted time. + */ + protected void update (long now) + { + } + + /** + * Test if the warning was triggered and needs to be processed. + */ + protected boolean triggeredWarning () + { + // Trigger a warning if it exists, it hasn't occured yet, + // and the timer has passed the warning threshold. + return ((_warnPercent >= 0f) && + !_warned && (_warnPercent <= _complete)); + } + + /** + * Test if the completion was triggered and needs to be processed. + */ + protected boolean triggeredCompleted () + { + return (!_completed && (1.0 <= _complete)); + } + + /** + * Handle the trigger of the warning. + */ + protected void handleWarning () + { + // Remember that this warning was handled + _warned = true; + + // Execute the warning listener if one was supplied + if (_warner != null) { + _warner.requestCompleted(this); + } + } + + /** + * Handle the trigger of the timer's completion. + */ + protected void handleCompleted () + { + // Remember that the completion was handled + _completed = true; + + // Stop the timer + stop(); + + // Handle the trigger function if one was supplied + if (_finisher != null) { + _finisher.requestCompleted(this); + } + } + + /** + * Invalidates this view's bounds via the host component. + */ + protected void invalidate () + { + // Schedule the timer's location on screen to get repainted + _invalidated = true; + _host.repaint(_bounds.x, _bounds.y, _bounds.width, _bounds.height); + } + + /** + * Renders the timer to the given graphics context if enabled. + */ + public void render (Graphics2D gfx) + { + // Paint the timer if its enabled + if (_enabled) { + paint(gfx, _renderComplete); + } + _invalidated = false; + } + + /** + * Paint the timer into the given graphics context at the inputted + * percent complete (0f means just started, 1f means just finished). + */ + public void paint (Graphics2D gfx, float complete) + { + // Inheriting classes will want to implement their own + // version of this function. Remember to call this one though! + + // Remember the completion level the last time the timer was painted + _paintComplete = complete; + } + + // documentation inherited + public void tick (long now) + { + if (!_enabled) { + return; + } + + // Initialize the starting time if necessary + if (_start == Long.MIN_VALUE) { + _start = now - Math.round(_duration * _complete); + } + + // Initialize the timestamp of the rendering error if necessary + if (_renderOffsetTime == Long.MIN_VALUE) { + _renderOffsetTime = now; + } + + // If the timer was just unpaused, handle the updates that + // need a timestamp + if (_processUnpause) { + _processUnpause = false; + _start += now - _lastUpdate; + } + + // Update the completion and triggers when the timer is running + if (running()) + { + // Figure out how what percent the timer has completed + _lastUpdate = now; + _complete = ((float) (_lastUpdate - _start)) / _duration; + + // Handle warning trigger if necessary + if (triggeredWarning()) { + handleWarning(); + } + + // Handle completion trigger if necessary + if (triggeredCompleted()) { + handleCompleted(); + } + } + + // Add error to the render completion percent if the interpolation + // hasn't finished yet + _renderComplete = _complete; + float error = _renderOffset; + if (_renderOffsetTime + _transitionTime > now) { + + _renderComplete += _renderOffset * + (1f - (now - _renderOffsetTime) / (float) _transitionTime); + _renderComplete = Math.max(0f, Math.min(1f, _renderComplete)); + } + + // Possibly orce a repaint if the render level changed (highly + // probable but not guaranteed) + if (_renderComplete != _paintComplete) + { + // Always force a repaint when changing to or from a boundary + if (_renderComplete <= 0f || _paintComplete <= 0f || + _renderComplete >= 1f || _paintComplete >= 1f) + { + invalidate(); + } + + // Also force a repaint if the completion state sufficiently + // changed + else if (Math.abs(_renderComplete - _paintComplete) > + _changeThreshold) + { + invalidate(); + } + } + } + + // documentation inherited + public boolean needsPaint () + { + // Always paint if the timer was invalidated + return _invalidated; + } + + // documentation inherited + public Component getComponent () + { + return null; + } + + // documentation inherited + public void hierarchyChanged (HierarchyEvent e) + { + checkFrameParticipation(); + } + + /** Check that the frame knows about the timer. */ + public void checkFrameParticipation () + { + // Determine whether or not the timer should participate in + // media ticks + boolean participate = _host.isShowing(); + + // Start participating if necessary + if (participate && !_participating) + { + _fmgr.registerFrameParticipant(this); + _participating = true; + } + + // Stop participating if necessary + else if (!participate && _participating) + { + _fmgr.removeFrameParticipant(this); + _participating = false; + } + } + + /** The frame manager that manages our animated view. */ + protected FrameManager _fmgr; + + /** The media panel containing the view. */ + protected JComponent _host; + + /** The screen coordinates of the timer's bounding box. */ + protected Rectangle _bounds; + + /** Whether to render the timer. */ + protected boolean _enabled = true; + + /** Whether the timer is running. */ + protected boolean _running = false; + + /** Whether the timer is participating in media tick updates. */ + protected boolean _participating = false; + + /** The last time the timer updated while running. */ + protected long _lastUpdate = Long.MIN_VALUE; + + /** The total amount of time in the timer when it was 0% done. */ + protected long _duration; + + /** The timestamp when the timer effectively started. */ + protected long _start; + + /** The percent of the duration time completed. */ + protected float _complete = 0f; + + /** The difference between the old rendered completion percent and the + * new completion percent the last time the completion percent had + * an unexpected change. In other words, this is the greatest amount + * that _renderComplete will differ from _complete during an + * state interpolation. + */ + protected float _renderOffset = 0f; + + /** The timestamp of _renderOffset. */ + protected long _renderOffsetTime = Long.MIN_VALUE; + + /** The amount of time it takes to fully interpolation a render + * transition from completion 0.0 to 1.0. */ + protected long _transitionTime = 0; + + /** The completion percent at which to render the timer. */ + protected float _renderComplete = 0f; + + /** The completion percent last time the timer was repainted. */ + protected float _paintComplete = -1; + + /** + * The timer will not invalidate itself until the completion level + * to render changes by at least this much from the previous time it + * was invalidated. + */ + protected float _changeThreshold = 0.0f; + + /** True if the timer has been invalidated since its last repaint. */ + protected boolean _invalidated; + + /** Trigger the warning when at least this much time has elapsed, + * or -1 for no warning. */ + protected float _warnPercent = -1; + + /** True if the warning has already been processed. */ + protected boolean _warned; + + /** True if the completion has already been processed. */ + protected boolean _completed; + + /** True if the code should process everything required for an unpause + * on the next tick(). */ + protected boolean _processUnpause = false; + + /** A listener to be notified when the timer finishes. */ + protected ResultListener _finisher; + + /** A listener to be notified when the warning time occurs. */ + protected ResultListener _warner; + + /** The default update date. */ + protected static final long DEFAULT_RATE = 100L; +} diff --git a/src/java/com/threerings/media/ViewTracker.java b/src/java/com/threerings/media/ViewTracker.java new file mode 100644 index 00000000..1f3687cc --- /dev/null +++ b/src/java/com/threerings/media/ViewTracker.java @@ -0,0 +1,18 @@ +// +// $Id: ViewTracker.java 4191 2006-06-13 22:42:20Z ray $ + +package com.threerings.media; + +/** + * An interface used by entities that wish to respond to the scrolling of a + * {@link VirtualMediaPanel}. + * + * @see VirtualMediaPanel#addViewTracker + */ +public interface ViewTracker +{ + /** + * Called by a {@link VirtualMediaPanel} when it scrolls. + */ + public void viewLocationDidChange (int dx, int dy); +} diff --git a/src/java/com/threerings/media/VirtualMediaPanel.java b/src/java/com/threerings/media/VirtualMediaPanel.java new file mode 100644 index 00000000..6ac4aecd --- /dev/null +++ b/src/java/com/threerings/media/VirtualMediaPanel.java @@ -0,0 +1,463 @@ +// +// $Id: VirtualMediaPanel.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.Graphics2D; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.util.ArrayList; + +import javax.swing.SwingUtilities; + +import com.samskivert.util.RunAnywhere; + +import com.threerings.media.image.ImageUtil; +import com.threerings.media.image.Mirage; +import com.threerings.media.util.MathUtil; +import com.threerings.media.util.Pathable; + +/** + * Extends the base media panel with the notion of a virtual coordinate + * system. All entities in the virtual media panel have virtual + * coordinates and the virtual media panel displays a window into that + * virtual view. The panel can be made to scroll by adjusting the view + * offset slightly at the start of each tick and it will efficiently copy + * the unmodified view data and generate repaint requests for the exposed + * regions. + */ +public class VirtualMediaPanel extends MediaPanel +{ + /** The code for the pathable following mode wherein we keep the view + * centered on the pathable's location. */ + public static final byte CENTER_ON_PATHABLE = 0; + + /** The code for the pathable following mode wherein we ensure that + * the marked pathable is always kept within the visible bounds of the + * view. */ + public static final byte ENCLOSE_PATHABLE = 1; + + /** The code for the pathable following mode wherein we set the upper-left + * corner of the view to the coordinates of the pathable. */ + public static final byte TRACK_PATHABLE = 2; + + /** + * Constructs a virtual media panel. + */ + public VirtualMediaPanel (FrameManager framemgr) + { + super(framemgr); + } + + /** + * Set a background image to tile the background of the media panel. + */ + public void setBackground (Mirage background) + { + _background = background; + } + + /** + * Sets the upper-left coordinate of the view port in virtual + * coordinates. The view will be as efficient as possible about + * repainting itself to achieve this new virtual location (meaning + * that if we need only to move one pixel to the left, it will use + * {@link Graphics#copyArea} to move our rendered view over one pixel + * and generate a dirty region for the exposed area). The new location + * will not take effect until the view is {@link #tick}ed, so only the + * last call to this method during a tick will have any effect. + */ + public void setViewLocation (int x, int y) + { + // make a note of our new x and y offsets + _nx = x; + _ny = y; + } + + /** + * Returns the bounds of the viewport in virtual coordinates. The + * returned rectangle must not be modified. + */ + public Rectangle getViewBounds () + { + return _vbounds; + } + + /** + * Adds an entity that will be informed when the view scrolls. + */ + public void addViewTracker (ViewTracker tracker) + { + _trackers.add(tracker); + } + + /** + * Removes an entity from the view trackers list. + */ + public void removeViewTracker (ViewTracker tracker) + { + _trackers.remove(tracker); + } + /** + * Instructs the view to follow the supplied pathable; ensuring that + * the view's coordinates are adjusted according to the follow mode. + * + * @param pable the pathable to follow. + * @param followMode the strategy for keeping the pathable in view. + */ + public void setFollowsPathable (Pathable pable, byte followMode) + { + _fmode = followMode; + _fpath = pable; + trackPathable(); // immediately update our location + } + + /** + * Clears out the pathable that was being enclosed or followed due to + * a previous call to {@link #setFollowsPathable}. + */ + public void clearPathable () + { + _fpath = null; + _fmode = (byte) -1; + } + + /** + * We overload this to translate mouse events into the proper + * coordinates before they are dispatched to any of the mouse + * listeners. + */ + protected void processMouseEvent (MouseEvent event) + { + event.translatePoint(_vbounds.x, _vbounds.y); + super.processMouseEvent(event); + } + + /** + * We overload this to translate mouse events into the proper + * coordinates before they are dispatched to any of the mouse + * listeners. + */ + protected void processMouseMotionEvent (MouseEvent event) + { + event.translatePoint(_vbounds.x, _vbounds.y); + super.processMouseMotionEvent(event); + } + + /** + * We overload this to translate mouse events into the proper + * coordinates before they are dispatched to any of the mouse + * listeners. + */ + protected void processMouseWheelEvent (MouseWheelEvent event) + { + event.translatePoint(_vbounds.x, _vbounds.y); + super.processMouseWheelEvent(event); + } + + // documentation inherited + protected void dirtyScreenRect (Rectangle rect) + { + // translate the screen rect into happy coordinates + rect.translate(_vbounds.x, _vbounds.y); + _remgr.addDirtyRegion(rect); + } + + // documentation inherited + public void doLayout () + { + super.doLayout(); + + // we need to obtain our absolute screen coordinates to work + // around the Windows copyArea() bug + findRootBounds(); + } + + // documentation inherited + public void setBounds (int x, int y, int width, int height) + { + super.setBounds(x, y, width, height); + + // keep track of the size of the viewport + _vbounds.width = getWidth(); + _vbounds.height = getHeight(); + + // we need to obtain our absolute screen coordinates to work + // around the Windows copyArea() bug + findRootBounds(); + } + + /** + * Determines the absolute screen coordinates at which this panel is + * located and stores them for reference later when rendering. This + * is necessary in order to work around the Windows + * copyArea() bug. + */ + protected void findRootBounds () + { + _abounds.setLocation(0, 0); + FrameManager.getRoot(this, _abounds); + } + + // documentation inherited + protected void didTick (long tickStamp) + { + super.didTick(tickStamp); + + int width = getWidth(), height = getHeight(); + + // adjusts our view location to track any pathable we might be + // tracking + trackPathable(); + + // if we have a new target location, we'll need to generate dirty + // regions for the area exposed by the scrolling + if (_nx != _vbounds.x || _ny != _vbounds.y) { + // determine how far we'll be moving on this tick + int dx = _nx - _vbounds.x, dy = _ny - _vbounds.y; + +// Log.info("Scrolling into place [n=(" + _nx + ", " + _ny + +// "), t=(" + _vbounds.x + ", " + _vbounds.y + +// "), d=(" + dx + ", " + dy + +// "), width=" + width + ", height=" + height + "]."); + + + // Mac OS X's redraw breaks on scrolling, so we repaint the + // entire panel + if (RunAnywhere.isMacOS()) { + _remgr.invalidateRegion(_nx, _ny, width, height); + } else { + _dx = dx; + _dy = dy; + + // these are used to prevent the vertical strip from + // overlapping the horizontal strip + int sy = _ny, shei = height; + + // and add invalid rectangles for the exposed areas + if (dy > 0) { + shei = Math.max(shei - dy, 0); + _remgr.invalidateRegion(_nx, _ny + height - dy, width, dy); + } else if (dy < 0) { + sy -= dy; + _remgr.invalidateRegion(_nx, _ny, width, -dy); + } + if (dx > 0) { + _remgr.invalidateRegion(_nx + width - dx, sy, dx, shei); + } else if (dx < 0) { + _remgr.invalidateRegion(_nx, sy, -dx, shei); + } + } + + + // now go ahead and update our location so that changes in + // between here and the call to paint() for this tick don't + // booch everything + _vbounds.x = _nx; _vbounds.y = _ny; + + // let derived classes react if they so desire + viewLocationDidChange(dx, dy); + } + } + + /** + * Called during our tick when we have adjusted the view location. The + * {@link #_vbounds} will already have been updated to reflect our new + * view coordinates. + * + * @param dx the delta scrolled in the x direction (in pixels). + * @param dy the delta scrolled in the y direction (in pixels). + */ + protected void viewLocationDidChange (int dx, int dy) + { + if (_perfRect != null) { + Rectangle sdirty = new Rectangle(_perfRect); + sdirty.translate(-dx, -dy); + dirtyScreenRect(sdirty); + } + + // inform our view trackers + for (int ii = 0, ll = _trackers.size(); ii < ll; ii++) { + ((ViewTracker)_trackers.get(ii)).viewLocationDidChange(dx, dy); + } + + // let our sprites and animations know what's up + _animmgr.viewLocationDidChange(dx, dy); + _spritemgr.viewLocationDidChange(dx, dy); + } + + /** + * Implements the standard pathable tracking support. Derived classes + * may wish to override this if they desire custom tracking + * functionality. + */ + protected void trackPathable () + { + // if we're tracking a pathable, adjust our view coordinates + if (_fpath == null) { + return; + } + + int width = getWidth(), height = getHeight(); + int nx = _vbounds.x, ny = _vbounds.y; + + // figure out where to move + switch (_fmode) { + case TRACK_PATHABLE: + nx = _fpath.getX(); + ny = _fpath.getY(); + break; + + case CENTER_ON_PATHABLE: + nx = _fpath.getX() - width/2; + ny = _fpath.getY() - height/2; + break; + + case ENCLOSE_PATHABLE: + Rectangle bounds = _fpath.getBounds(); + if (nx > bounds.x) { + nx = bounds.x; + } else if (nx + width < bounds.x + bounds.width) { + nx = bounds.x + bounds.width - width; + } + if (ny > bounds.y) { + ny = bounds.y; + } else if (ny + height < bounds.y + bounds.height) { + ny = bounds.y + bounds.height - height; + } + break; + + default: + Log.warning("Eh? Set to invalid pathable mode " + + "[mode=" + _fmode + "]."); + break; + } + +// Log.info("Tracking pathable [mode=" + _fmode + +// ", pable=" + _fpath + ", nx=" + nx + ", ny=" + ny + "]."); + + setViewLocation(nx, ny); + } + + // documentation inherited + protected void paint (Graphics2D gfx, Rectangle[] dirty) + { + // if we're scrolling, go ahead and do the business + if (_dx != 0 || _dy != 0) { + int width = getWidth(), height = getHeight(); + int cx = (_dx > 0) ? _dx : 0; + int cy = (_dy > 0) ? _dy : 0; + + // set the clip to the bounds of the component (we can't + // assume the clip is anything sensible upon entry to paint() + // because the frame manager expects us to set our own clip) + gfx.setClip(0, 0, width, height); + + // on windows, attempting to call copyArea() on a translated + // graphics context results in boochness; so we have to + // untranslate the graphics context, do our copyArea() and + // then translate it back + if (RunAnywhere.isWindows()) { + gfx.translate(-_abounds.x, -_abounds.y); + gfx.copyArea(_abounds.x + cx, _abounds.y + cy, + width - Math.abs(_dx), + height - Math.abs(_dy), -_dx, -_dy); + gfx.translate(_abounds.x, _abounds.y); + } else if (RunAnywhere.isMacOS()) { + try { + gfx.copyArea(cx, cy, + width - Math.abs(_dx), + height - Math.abs(_dy), -_dx, -_dy); + } catch (Exception e) { + // HACK when it throws an exception trying to do the + // copy area, just repaint everything + dirty = new Rectangle[] { new Rectangle(_vbounds) }; + } + } else { + gfx.copyArea(cx, cy, + width - Math.abs(_dx), + height - Math.abs(_dy), -_dx, -_dy); + } + + // and clear out our scroll deltas + _dx = 0; _dy = 0; + } + + // translate into happy space + gfx.translate(-_vbounds.x, -_vbounds.y); + + // now do the actual painting + super.paint(gfx, dirty); + + // translate back out of happy space + gfx.translate(_vbounds.x, _vbounds.y); + } + + // documentation inherited + protected void constrainToBounds (Rectangle dirty) + { + SwingUtilities.computeIntersection( + _vbounds.x, _vbounds.y, getWidth(), getHeight(), dirty); + } + + // documentation inherited + protected void paintBehind (Graphics2D gfx, Rectangle dirtyRect) + { + // if we have a background image specified, tile it! + if (_background != null) { + // make sure it's aligned + int iw = _background.getWidth(); + int ih = _background.getHeight(); + int lowx = iw * MathUtil.floorDiv(dirtyRect.x, iw); + int lowy = ih * MathUtil.floorDiv(dirtyRect.y, ih); + ImageUtil.tileImage(gfx, _background, lowx, lowy, + dirtyRect.width + (dirtyRect.x - lowx), + dirtyRect.height + (dirtyRect.y - lowy)); + } + } + + /** Our viewport bounds in virtual coordinates. */ + protected Rectangle _vbounds = new Rectangle(); + + /** Our target offsets to be effected on the next tick. */ + protected int _nx, _ny; + + /** Our scroll offsets. */ + protected int _dx, _dy; + + /** Our tiling background image. */ + protected Mirage _background; + + /** The mode we're using when following a pathable. */ + protected byte _fmode = -1; + + /** The pathable being followed. */ + protected Pathable _fpath; + + /** We need to know our absolute coordinates in order to work around + * the Windows copyArea() bug. */ + protected Rectangle _abounds = new Rectangle(); + + /** A list of entities to be informed when the view scrolls. */ + protected ArrayList _trackers = new ArrayList(); +} diff --git a/src/java/com/threerings/media/VirtualRangeModel.java b/src/java/com/threerings/media/VirtualRangeModel.java new file mode 100644 index 00000000..fdc80c01 --- /dev/null +++ b/src/java/com/threerings/media/VirtualRangeModel.java @@ -0,0 +1,109 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.Rectangle; + +import javax.swing.BoundedRangeModel; +import javax.swing.DefaultBoundedRangeModel; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.threerings.media.util.MathUtil; + +/** + * Provides a {@link BoundedRangeModel} interface to a virtual media panel + * so that it can easily be wired up to scroll bars or other scrolling + * controls. + */ +public class VirtualRangeModel + implements ChangeListener +{ + /** + * Creates a virtual media panel range model that will interact with + * the supplied virtual media panel. + */ + public VirtualRangeModel (VirtualMediaPanel panel) + { + _panel = panel; + + // listen to our range models and scroll our badself + _hrange.addChangeListener(this); + _vrange.addChangeListener(this); + } + + /** + * Informs the virtual range model the extent of the area over which + * we can scroll. + */ + public void setScrollableArea (int x, int y, int width, int height) + { + Rectangle vb = _panel.getViewBounds(); + int hmax = x + width, vmax = y + height, value; + + if (width > vb.width) { + value = MathUtil.bound(x, _hrange.getValue(), hmax - vb.width); + _hrange.setRangeProperties(value, vb.width, x, hmax, false); + } else { + // Let's center it and lock it down. + int newx = x - (vb.width - width)/2; + _hrange.setRangeProperties(newx, 0, newx, newx, false); + } + if (height > vb.height) { + value = MathUtil.bound(y, _vrange.getValue(), vmax - vb.height); + _vrange.setRangeProperties(value, vb.height, y, vmax, false); + } else { + // Let's center it and lock it down. + int newy = y - (vb.height - height)/2; + _vrange.setRangeProperties(newy, 0, newy, newy, false); + } + } + + /** + * Returns a range model that controls the scrollability of the scene + * in the horizontal direction. + */ + public BoundedRangeModel getHorizModel () + { + return _hrange; + } + + /** + * Returns a range model that controls the scrollability of the scene + * in the vertical direction. + */ + public BoundedRangeModel getVertModel () + { + return _vrange; + } + + // documentation inherited from interface ChangeListener + public void stateChanged (ChangeEvent e) + { + _panel.setViewLocation(_hrange.getValue(), _vrange.getValue()); + } + + protected VirtualMediaPanel _panel; + protected BoundedRangeModel _hrange = new DefaultBoundedRangeModel(); + protected BoundedRangeModel _vrange = new DefaultBoundedRangeModel(); +} diff --git a/src/java/com/threerings/media/animation/Animation.java b/src/java/com/threerings/media/animation/Animation.java new file mode 100644 index 00000000..d0be45d5 --- /dev/null +++ b/src/java/com/threerings/media/animation/Animation.java @@ -0,0 +1,169 @@ +// +// $Id: Animation.java 3822 2006-01-27 03:28:04Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.Rectangle; + +import com.samskivert.util.ObserverList; + +import com.threerings.media.AbstractMedia; + +/** + * The animation class is an abstract class that should be extended to + * provide animation functionality. It is generally used in conjunction + * with an {@link AnimationManager}. + */ +public abstract class Animation extends AbstractMedia +{ + /** + * Constructs an animation. + * + * @param bounds the animation rendering bounds. + */ + public Animation (Rectangle bounds) + { + super(bounds); + } + + /** + * Returns true if the animation has finished all of its business, + * false if not. + */ + public boolean isFinished () + { + return _finished; + } + + /** + * If this animation has run to completion, it can be reset to prepare + * it for another go. + */ + public void reset () + { + _finished = false; + } + + // documentation inherited + public void setLocation (int x, int y) + { + if (_bounds.x == x && _bounds.y == y) { + return; // no-op + } + + // start with our current bounds + Rectangle dirty = new Rectangle(_bounds); + + // move ourselves + super.setLocation(x, y); + + // grow the dirty rectangle to incorporate our new bounds and pass + // the dirty region to our region manager + if (_mgr != null) { + // if our new bounds intersect our old bounds, grow a single + // dirty rectangle to incorporate them both + if (_bounds.intersects(dirty)) { + dirty.add(_bounds); + } else { + // otherwise invalidate our new bounds separately + _mgr.getRegionManager().invalidateRegion(_bounds); + } + _mgr.getRegionManager().addDirtyRegion(dirty); + } + } + + // documentation inherited + protected void willStart (long tickStamp) + { + super.willStart(tickStamp); + queueNotification(new AnimStartedOp(this, tickStamp)); + } + + /** + * Called when the animation is finished and the animation manager is + * about to remove it from service. + */ + protected void willFinish (long tickStamp) + { + queueNotification(new AnimCompletedOp(this, tickStamp)); + } + + /** + * Called when the animation is finished and the animation manager has + * removed it from service. + */ + protected void didFinish (long tickStamp) + { + } + + /** + * Adds an animation observer to this animation's list of observers. + */ + public void addAnimationObserver (AnimationObserver obs) + { + addObserver(obs); + } + + /** + * Removes an animation observer from this animation's list of observers. + */ + public void removeAnimationObserver (AnimationObserver obs) + { + removeObserver(obs); + } + + /** Whether the animation is finished. */ + protected boolean _finished = false; + + /** Used to dispatch {@link AnimationObserver#animationStarted}. */ + protected static class AnimStartedOp implements ObserverList.ObserverOp + { + public AnimStartedOp (Animation anim, long when) { + _anim = anim; + _when = when; + } + + public boolean apply (Object observer) { + ((AnimationObserver)observer).animationStarted(_anim, _when); + return true; + } + + protected Animation _anim; + protected long _when; + } + + /** Used to dispatch {@link AnimationObserver#animationCompleted}. */ + protected static class AnimCompletedOp implements ObserverList.ObserverOp + { + public AnimCompletedOp (Animation anim, long when) { + _anim = anim; + _when = when; + } + + public boolean apply (Object observer) { + ((AnimationObserver)observer).animationCompleted(_anim, _when); + return true; + } + + protected Animation _anim; + protected long _when; + } +} diff --git a/src/java/com/threerings/media/animation/AnimationAdapter.java b/src/java/com/threerings/media/animation/AnimationAdapter.java new file mode 100644 index 00000000..1e1900d0 --- /dev/null +++ b/src/java/com/threerings/media/animation/AnimationAdapter.java @@ -0,0 +1,38 @@ +// +// $Id: AnimationAdapter.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +/** + * An adapter class for {@link AnimationObserver}. + */ +public class AnimationAdapter implements AnimationObserver +{ + // documentation inherited from interface + public void animationStarted (Animation anim, long when) + { + } + + // documentation inherited from interface + public void animationCompleted (Animation anim, long when) + { + } +} diff --git a/src/java/com/threerings/media/animation/AnimationArranger.java b/src/java/com/threerings/media/animation/AnimationArranger.java new file mode 100644 index 00000000..fafd0b4c --- /dev/null +++ b/src/java/com/threerings/media/animation/AnimationArranger.java @@ -0,0 +1,68 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.Rectangle; + +import java.util.ArrayList; + +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.media.Log; + +/** + * A utility class for positioning animations such that they don't overlap, + * as best as possible. + */ +public class AnimationArranger +{ + /** + * Position the specified animation so that it is not overlapping + * any still-running animations previously passed to this method. + */ + public void positionAvoidAnimation (Animation anim, Rectangle viewBounds) + { + Rectangle abounds = new Rectangle(anim.getBounds()); + ArrayList avoidables = (ArrayList) _avoidAnims.clone(); + // if we are able to place it somewhere, do so + if (SwingUtil.positionRect(abounds, viewBounds, avoidables)) { + anim.setLocation(abounds.x, abounds.y); + } + + // add the animation to the list of avoidables + _avoidAnims.add(anim); + // keep an eye on it so that we can remove it when it's finished + anim.addAnimationObserver(_avoidAnimObs); + } + + /** The animations that other animations may wish to avoid. */ + protected ArrayList _avoidAnims = new ArrayList(); + + /** Automatically removes avoid animations when they're done. */ + protected AnimationAdapter _avoidAnimObs = new AnimationAdapter() { + public void animationCompleted (Animation anim, long when) { + if (!_avoidAnims.remove(anim)) { + Log.warning("Couldn't remove avoid animation?! " + anim + "."); + } + } + }; +} diff --git a/src/java/com/threerings/media/animation/AnimationFrameSequencer.java b/src/java/com/threerings/media/animation/AnimationFrameSequencer.java new file mode 100644 index 00000000..f2a55c25 --- /dev/null +++ b/src/java/com/threerings/media/animation/AnimationFrameSequencer.java @@ -0,0 +1,260 @@ +// +// $Id: AnimationFrameSequencer.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import com.samskivert.util.ObserverList; + +import com.threerings.media.util.FrameSequencer; +import com.threerings.media.util.MultiFrameImage; + +/** + * Used to control animation timing when displaying an animation. + */ +public interface AnimationFrameSequencer extends FrameSequencer +{ + /** + * Called after init to set the animation. + */ + public void setAnimation (Animation anim); + + /** + * A sequencer that can step through a series of frames in any order + * and speed and notify (via {@link SequencedAnimationObserver}) when + * specific frames are reached. + */ + public static class MultiFunction implements AnimationFrameSequencer + { + /** + * Creates the simplest multifunction frame sequencer. + * + * @param sequence the ordering to display the frames. + * @param msPerFrame the number of ms to display each frame. + * @param loop if false, the sequencer will report the end of + * the animation after progressing through all of the frames once, + * otherwise it will loop indefinitely. + */ + public MultiFunction (int[] sequence, long msPerFrame, boolean loop) + { + _length = sequence.length; + _sequence = sequence; + setDelay(msPerFrame); + _marks = new boolean[_length]; + _loop = loop; + } + + /** + * Creates a fully-specified multifunction frame sequencer. + * + * @param sequence the ordering to display the frames. + * @param msPerFrame the number of ms to display each frame. + * @param marks if true for a frame, a FrameReachedEvent will + * be generated when that frame is reached. + * @param loop if false, the sequencer will report the end of + * the animation after progressing through all of the frames once, + * otherwise it will loop indefinitely. + */ + public MultiFunction ( + int[] sequence, long[] msPerFrame, boolean[] marks, boolean loop) + { + _length = sequence.length; + if ((_length != msPerFrame.length) || (_length != marks.length)) { + throw new IllegalArgumentException( + "All MultiFunction FrameSequencer arrays must be " + + "the same length."); + } + + _sequence = sequence; + _delays = msPerFrame; + _marks = marks; + _loop = loop; + } + + /** + * Set the delay to use. + */ + public void setDelay (long msPerFrame) + { + _delays = null; + _delay = msPerFrame; + } + + /** + * Set the length of the sequence we are to use, or 0 means set + * it to the length of the sequence array that we were constructed with. + */ + public void setLength (int len) + { + _length = (len == 0) ? _sequence.length : len; + if (_curIdx >= _length) { + _curIdx = 0; + } + } + + /** + * Do we reset the animation on init? Default is true. + */ + public void setResetOnInit (boolean resets) + { + _resets = resets; + } + + // documentation inherited from interface + public void init (MultiFrameImage source) + { + int framecount = source.getFrameCount(); + // let's make sure our frames are valid + for (int ii=0; ii < _length; ii++) { + if (_sequence[ii] >= framecount) { + throw new IllegalArgumentException( + "MultiFunction FrameSequencer was initialized " + + "with a MultiFrameImage with not enough frames " + + "to match the sequence it was constructed with."); + } + } + + if (_resets) { + _lastStamp = 0L; + _curIdx = 0; + } + } + + // documentation inherited from interface + public void setAnimation (Animation anim) + { + _animation = anim; + } + + // documentation inherited from interface + public int tick (long tickStamp) + { + // obtain our starting timestamp if we don't already have one + if (_lastStamp == 0L) { + _lastStamp = tickStamp; + // we might need to notify on the first frame + checkNotify(tickStamp); + } + + // we may have rushed through more than one frame since the last + // tick, but we want to always notify even if we never displayed + long curdelay = getDelay(_curIdx); + while (tickStamp >= (_lastStamp + curdelay)) { + _lastStamp += curdelay; + _curIdx++; + if (_curIdx == _length) { + if (_loop) { + _curIdx = 0; + } else { + return -1; + } + } + checkNotify(tickStamp); + + // get the delay for checking the next frame + curdelay = getDelay(_curIdx); + } + + // return the right frame + return _sequence[_curIdx]; + } + + // documentation inherited from interface + public void fastForward (long timeDelta) + { + // this method should be called "unpause" + _lastStamp += timeDelta; + } + + /** + * Get the delay to use for the specified frame. + */ + protected final long getDelay (int index) + { + return (_delays == null) ? _delay : _delays[index]; + } + + /** + * Check to see if we need to notify that we've reached a marked frame. + */ + protected void checkNotify (long tickStamp) + { + if (_marks[_curIdx]) { + _animation.queueNotification( + new FrameReachedOp(_animation, tickStamp, + _sequence[_curIdx], _curIdx)); + } + } + + /** The current sequence index. */ + protected int _curIdx = 0; + + /** The length of our animation sequence. */ + protected int _length; + + /** Do we reset on init? */ + protected boolean _resets = true; + + /** The sequence of frames to display. */ + protected int[] _sequence; + + /** The corresponding delay for each frame, in ms. */ + protected long[] _delays; + + /** Or a single delay for all frames. */ + protected long _delay; + + /** Whether to send a FrameReachedEvent on a sequence index. */ + protected boolean[] _marks; + + /** Does the animation loop? */ + protected boolean _loop; + + /** The time at which we were last ticked. */ + protected long _lastStamp; + + /** The animation that we're sequencing for. */ + protected Animation _animation; + + /** Used to dispatch {@link SequencedAnimationObserver#frameReached}. */ + protected static class FrameReachedOp implements ObserverList.ObserverOp + { + public FrameReachedOp (Animation anim, long when, + int frameIdx, int frameSeq) { + _anim = anim; + _when = when; + _frameIdx = frameIdx; + _frameSeq = frameSeq; + } + + public boolean apply (Object observer) { + if (observer instanceof SequencedAnimationObserver) { + ((SequencedAnimationObserver)observer).frameReached( + _anim, _when, _frameIdx, _frameSeq); + } + return true; + } + + protected Animation _anim; + protected long _when; + protected int _frameIdx, _frameSeq; + } + } +} diff --git a/src/java/com/threerings/media/animation/AnimationManager.java b/src/java/com/threerings/media/animation/AnimationManager.java new file mode 100644 index 00000000..23a06607 --- /dev/null +++ b/src/java/com/threerings/media/animation/AnimationManager.java @@ -0,0 +1,81 @@ +// +// $Id: AnimationManager.java 3347 2005-02-14 03:00:58Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import com.threerings.media.AbstractMediaManager; +import com.threerings.media.MediaPanel; + +/** + * Manages a collection of animations, ticking them when the animation + * manager itself is ticked and generating events when animations finish + * and suchlike. + */ +public class AnimationManager extends AbstractMediaManager +{ + /** + * Construct and initialize the animation manager which readies itself + * to manage animations. + */ + public AnimationManager (MediaPanel panel) + { + super(panel); + } + + /** + * Registers the given {@link Animation} with the animation manager + * for ticking and painting. + */ + public void registerAnimation (Animation anim) + { + insertMedia(anim); + } + + /** + * Un-registers the given {@link Animation} from the animation + * manager. The bounds of the animation will automatically be + * invalidated so that they are properly rerendered in the absence of + * the animation. + */ + public void unregisterAnimation (Animation anim) + { + removeMedia(anim); + } + + // documentation inherited + protected void tickAllMedia (long tickStamp) + { + super.tickAllMedia(tickStamp); + + for (int ii = _media.size() - 1; ii >= 0; ii--) { + Animation anim = (Animation)_media.get(ii); + if (!anim.isFinished()) { + continue; + } + + // as the anim is finished, remove it and notify observers + anim.willFinish(tickStamp); + unregisterAnimation(anim); + anim.didFinish(tickStamp); + // Log.info("Removed finished animation " + anim + "."); + } + } +} diff --git a/src/java/com/threerings/media/animation/AnimationObserver.java b/src/java/com/threerings/media/animation/AnimationObserver.java new file mode 100644 index 00000000..a35ad11c --- /dev/null +++ b/src/java/com/threerings/media/animation/AnimationObserver.java @@ -0,0 +1,39 @@ +// +// $Id: AnimationObserver.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +/** + * An interface to be implemented by classes that would like to observe an + * {@link Animation} and be notified of interesting events relating to it. + */ +public interface AnimationObserver +{ + /** + * Called the first time this animation is ticked. + */ + public void animationStarted (Animation anim, long when); + + /** + * Called when the observed animation has completed. + */ + public void animationCompleted (Animation anim, long when); +} diff --git a/src/java/com/threerings/media/animation/AnimationSequencer.java b/src/java/com/threerings/media/animation/AnimationSequencer.java new file mode 100644 index 00000000..0bbdda69 --- /dev/null +++ b/src/java/com/threerings/media/animation/AnimationSequencer.java @@ -0,0 +1,295 @@ +// +// $Id: AnimationSequencer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import java.util.ArrayList; + +import com.samskivert.util.StringUtil; + +import com.threerings.media.Log; + +/** + * An animation that provides facilities for adding a sequence of + * animations that are fired after a fixed time interval has elapsed or + * after previous animations in the sequence have completed. Facilities + * are also provided for running code upon the completion of animations in + * the sequence. + */ +public class AnimationSequencer extends Animation +{ + /** + * Constructs an animation sequencer with the expectation that + * animations will be added via subsequent calls to {@link + * #addAnimation}. + * + * @param animmgr the animation manager to which to add our animations + * when they are ready to start. + */ + public AnimationSequencer (AnimationManager animmgr) + { + super(new Rectangle()); + _animmgr = animmgr; + } + + /** + * Adds the supplied animation to the sequence with the given + * parameters. Note that care should be taken if this is called after + * the animation sequence has begun firing animations. Do not add new + * animations after the final animation in the sequence has been + * started or you run the risk of attempting to add an animation to + * the sequence after it thinks that it has finished (in which case + * this method will fail). + * + * @param anim the animation to be sequenced, or null if the + * completion action should be run immediately when this "animation" + * is ready to fired. + * @param delta the number of milliseconds following the + * start of the previous animation in the queue that this + * animation should be started; 0 if it should be started + * simultaneously with its predecessor int the queue; -1 if it should + * be started when its predecessor has completed. + * @param completionAction a runnable to be executed when this + * animation completes. + */ + public void addAnimation ( + Animation anim, long delta, Runnable completionAction) + { + // sanity check + if (_finished) { + throw new IllegalStateException( + "Animation added to finished sequencer"); + } + + // if this guy is triggering on a previous animation, grab that + // good fellow here + AnimRecord trigger = null; + if (delta == -1) { + if (_queued.size() > 0) { + // if there are queued animations we want the most + // recently queued animation + trigger = (AnimRecord)_queued.get(_queued.size()-1); + } else if (_running.size() > 0) { + // otherwise, if there are running animations, we want the + // last one in that list + trigger = (AnimRecord)_running.get(_running.size()-1); + } + // otherwise we have no trigger, we'll just start ASAP + } + + AnimRecord arec = new AnimRecord( + anim, delta, trigger, completionAction); +// Log.info("Queued " + arec + "."); + _queued.add(arec); + } + + /** + * Clears out the animations being managed by this sequencer. + */ + public void clear () + { + _queued.clear(); + _lastStamp = 0; + } + + // documentation inherited + public void tick (long tickStamp) + { + if (_lastStamp == 0) { + _lastStamp = tickStamp; + } + + // add all animations whose time has come + while (_queued.size() > 0) { + AnimRecord arec = (AnimRecord)_queued.get(0); + if (!arec.readyToFire(tickStamp, _lastStamp)) { + // if it's not time to add this animation, all subsequent + // animations must surely wait as well + break; + } + + // remove it from queued and put it on the running list + _queued.remove(0); + _running.add(arec); + + // note that we've advanced to the next animation + _lastStamp = tickStamp; + + // fire in the hole! + arec.fire(tickStamp); + } + + // we're done when both lists are empty +// boolean finished = _finished; + _finished = ((_queued.size() + _running.size()) == 0); +// if (!finished && _finished) { +// Log.info("Finishing sequence at " + (tickStamp%10000) + "."); +// } + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + // don't care + } + + // documentation inherited + public void fastForward (long timeDelta) + { + _lastStamp += timeDelta; + } + + // documentation inherited + public void viewLocationDidChange (int dx, int dy) + { + super.viewLocationDidChange(dx, dy); + + // track cumulative view location changes so that we can adjust our + // animations before we start them + _vdx += dx; + _vdy += dy; + } + + /** + * Called when the time comes to start an animation. Derived classes + * may override this method and pass the animation on to their + * animation manager and do whatever else they need to do with + * operating animations. The default implementation simply adds them + * to the media panel supplied when we were constructed. + * + * @param anim the animation to be displayed. + * @param tickStamp the timestamp at which this animation was fired. + */ + protected void startAnimation (Animation anim, long tickStamp) + { + // account for any view scrolling that happened before this animation + // was actually added to the view + if (_vdx != 0 || _vdy != 0) { + anim.viewLocationDidChange(_vdx, _vdy); + } + + _animmgr.registerAnimation(anim); + } + + protected class AnimRecord extends AnimationAdapter + { + public AnimRecord (Animation anim, long delta, AnimRecord trigger, + Runnable completionAction) { + _anim = anim; + _delta = delta; + _trigger = trigger; + _completionAction = completionAction; + } + + public boolean readyToFire (long now, long lastStamp) + { + if (_delta == -1) { + // if we have no trigger, that means we should start + // immediately, otherwise we wait until our trigger is no + // longer running (they are guaranteed not to be still + // queued at this point because readyToFire is only called + // on an animation after all animations previous in the + // queue have been started) + return (_trigger == null) ? + true : !_running.contains(_trigger); + + } else { + return (lastStamp + _delta <= now); + } + } + + public void fire (long when) + { +// Log.info("Firing " + this + " at " + (when%10000) + "."); + + // if we have an animation, start it up and await its + // completion + if (_anim != null) { + startAnimation(_anim, when); + _anim.addAnimationObserver(this); + + } else { + // since there's no animation, we need to fire our + // completion routine immediately + fireCompletion(when); + } + } + + public void fireCompletion (long when) + { +// Log.info("Completing " + this + " at " + (when%10000) + "."); + + // call the completion action, if there is one + if (_completionAction != null) { + try { + _completionAction.run(); + } catch (Throwable t) { + Log.logStackTrace(t); + } + } + + // make a note that this animation is complete + _running.remove(this); + + // kids, don't try this at home; we call tick() immediately so + // that any animations triggered on the completion of previous + // animations can trigger on the completion of this animation + // rather than having to wait until the next tick to do so + tick(when); + } + + public void animationCompleted (Animation anim, long when) + { + fireCompletion(when); + } + + public String toString () + { + return "[anim=" + StringUtil.shortClassName(_anim) + + ((_anim == null) ? "" : ("/" + _anim.hashCode())) + + ", action=" + _completionAction + + ", delta=" + _delta + ", trig=" + (_trigger != null) + "]"; + } + + protected Animation _anim; + protected Runnable _completionAction; + protected long _delta; + protected AnimRecord _trigger; + } + + /** The animation manager in which we run animations. */ + protected AnimationManager _animmgr; + + /** Animations that have not been fired. */ + protected ArrayList _queued = new ArrayList(); + + /** Animations that are currently running. */ + protected ArrayList _running = new ArrayList(); + + /** The timestamp at which we fired the last animation. */ + protected long _lastStamp; + + /** Used to track view scrolling while animations are in limbo. */ + protected int _vdx, _vdy; +} diff --git a/src/java/com/threerings/media/animation/AnimationWaiter.java b/src/java/com/threerings/media/animation/AnimationWaiter.java new file mode 100644 index 00000000..f1dad948 --- /dev/null +++ b/src/java/com/threerings/media/animation/AnimationWaiter.java @@ -0,0 +1,95 @@ +// +// $Id: AnimationWaiter.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +/** + * An abstract class that simplifies a common animation usage case in + * which a number of animations are created and the creator would like to + * be able to perform specific actions when each animation has completed + * (see {@link #animationDidFinish} and/or when all animations are + * finished (see {@link #allAnimationsFinished}.) + */ +public abstract class AnimationWaiter + implements AnimationObserver +{ + /** + * Adds an animation to the animation waiter for observation. + */ + public void addAnimation (Animation anim) + { + anim.addAnimationObserver(this); + _animCount++; + } + + /** + * Adds the supplied animations to the animation waiter for + * observation. + */ + public void addAnimations (Animation[] anims) + { + int acount = anims.length; + for (int ii = 0; ii < acount; ii++) { + addAnimation(anims[ii]); + } + } + + // documentation inherited from interface + public void animationStarted (Animation anim, long when) + { + } + + // documentation inherited from interface + public void animationCompleted (Animation anim, long when) + { + // note that the animation is finished + animationDidFinish(anim); + _animCount--; + + // let derived classes know when all is done + if (_animCount == 0) { + allAnimationsFinished(); + } + } + + /** + * Called when an animation being observed by the waiter has + * completed its business. Derived classes may wish to override + * this method to engage in their unique antics. + */ + protected void animationDidFinish (Animation anim) + { + // nothing for now + } + + /** + * Called when all animations being observed by the waiter have + * completed their business. Derived classes may wish to override + * this method to engage in their unique antics. + */ + protected void allAnimationsFinished () + { + // nothing for now + } + + /** The number of animations. */ + protected int _animCount; +} diff --git a/src/java/com/threerings/media/animation/BlankAnimation.java b/src/java/com/threerings/media/animation/BlankAnimation.java new file mode 100644 index 00000000..db53fb28 --- /dev/null +++ b/src/java/com/threerings/media/animation/BlankAnimation.java @@ -0,0 +1,67 @@ +// +// $Id: BlankAnimation.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.Graphics2D; +import java.awt.Rectangle; + +/** + * Displays nothing, but does so for a specified amount of time. Useful + * when you want to get an animation completed event in some period of + * time but don't actually need to display anything. + */ +public class BlankAnimation extends Animation +{ + public BlankAnimation (long duration) + { + super(new Rectangle(0, 0, 0, 0)); + _duration = duration; + } + + // documentation inherited + public void tick (long timestamp) + { + if (_start == 0) { + // initialize our starting time + _start = timestamp; + } + + // check whether we're done + _finished = (timestamp - _start >= _duration); + } + + // documentation inherited + public void fastForward (long timeDelta) + { + if (_start > 0) { + _start += timeDelta; + } + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + // nothing doing + } + + protected long _duration, _start; +} diff --git a/src/java/com/threerings/media/animation/BlendAnimation.java b/src/java/com/threerings/media/animation/BlendAnimation.java new file mode 100644 index 00000000..8ffc2bf5 --- /dev/null +++ b/src/java/com/threerings/media/animation/BlendAnimation.java @@ -0,0 +1,87 @@ +// +// $Id: BlendAnimation.java 4191 2006-06-13 22:42:20Z ray $ + +package com.threerings.media.animation; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.threerings.media.image.Mirage; +import com.threerings.media.util.LinearTimeFunction; +import com.threerings.media.util.TimeFunction; + +/** + * Blends between a series of images using alpha. + */ +public class BlendAnimation extends Animation +{ + /** + * Blends from the starting image through each successive image in the + * specified amount of time (blending between each image takes place + * in delay milliseconds). + */ + public BlendAnimation (int x, int y, Mirage[] images, int delay) + { + super(new Rectangle(x, y, images[0].getWidth(), images[0].getHeight())); + _images = images; + int fades = images.length-1; + _tfunc = new LinearTimeFunction(0, 100 * fades, delay * fades); + } + + // documentation inherited + public void tick (long timestamp) + { + // check to see if our blend level has changed + int level = _tfunc.getValue(timestamp); + if (level == _level) { + return; + } + // stop if we reach the end + if (level == 100*(_images.length-1)) { + _finished = true; + } + _level = level; + invalidate(); + } + + // documentation inherited + public void fastForward (long timeDelta) + { + _tfunc.fastForward(timeDelta); + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + int index = _level / 100; + float alpha = 1f - (_level % 100) / 100f; + + Composite ocomp = gfx.getComposite(); + gfx.setComposite(AlphaComposite.getInstance( + AlphaComposite.SRC_OVER, alpha)); + _images[index].paint(gfx, _bounds.x, _bounds.y); + if (index < _images.length-1) { + gfx.setComposite(AlphaComposite.getInstance( + AlphaComposite.SRC_OVER, 1f-alpha)); + _images[index+1].paint(gfx, _bounds.x, _bounds.y); + } + gfx.setComposite(ocomp); + } + + /** The images between which we are blending. */ + protected Mirage[] _images; + + /** The time function we're using to time our blends. */ + protected TimeFunction _tfunc; + + /** Our current blend level and image index all wrapped into one. */ + protected int _level; + + /** The alpha composite used to render our current image. */ + protected AlphaComposite _currentComp; + + /** The alpha composite used to render our "next" image. */ + protected AlphaComposite _nextComp; +} diff --git a/src/java/com/threerings/media/animation/BobbleAnimation.java b/src/java/com/threerings/media/animation/BobbleAnimation.java new file mode 100644 index 00000000..29085fc1 --- /dev/null +++ b/src/java/com/threerings/media/animation/BobbleAnimation.java @@ -0,0 +1,120 @@ +// +// $Id: BobbleAnimation.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.samskivert.util.RandomUtil; +import com.threerings.media.image.Mirage; + +/** + * An animation that bobbles an image around within a specific horizontal + * and vertical pixel range for a given period of time. + */ +public class BobbleAnimation extends Animation +{ + /** + * Constructs a bobble animation. + * + * @param image the image to animate. + * @param sx the starting x-position. + * @param sy the starting y-position. + * @param rx the horizontal bobble range. + * @param ry the vertical bobble range. + * @param duration the time to animate in milliseconds. + */ + public BobbleAnimation ( + Mirage image, int sx, int sy, int rx, int ry, int duration) + { + super(new Rectangle(sx - rx, sy - ry, sx + image.getWidth() + rx, + sy + image.getHeight() + ry)); + + // save things off + _image = image; + _sx = sx; + _sy = sy; + _rx = rx; + _ry = ry; + + // calculate animation ending time + _duration = duration; + } + + // documentation inherited + public void tick (long tickStamp) + { + // grab our ending time on our first tick + if (_end == 0L) { + _end = tickStamp + _duration; + } + + _finished = (tickStamp >= _end); + invalidate(); + + // calculate the latest position + int dx = RandomUtil.getInt(_rx); + int dy = RandomUtil.getInt(_ry); + _x = (_sx + dx) * ((RandomUtil.getInt(2) == 0) ? -1 : 1); + _y = (_sy + dy) * ((RandomUtil.getInt(2) == 0) ? -1 : 1); + } + + // documentation inherited + public void fastForward (long timeDelta) + { + _end += timeDelta; + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + _image.paint(gfx, _x, _y); + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + + buf.append(", x=").append(_x); + buf.append(", y=").append(_y); + buf.append(", rx=").append(_rx); + buf.append(", ry=").append(_ry); + buf.append(", sx=").append(_sx); + buf.append(", sy=").append(_sy); + } + + /** The current position of the image. */ + protected int _x, _y; + + /** The horizontal and vertical bobbling range. */ + protected int _rx, _ry; + + /** The starting position. */ + protected int _sx, _sy; + + /** The image to animate. */ + protected Mirage _image; + + /** Animation ending timing information. */ + protected long _duration, _end; +} diff --git a/src/java/com/threerings/media/animation/ExplodeAnimation.java b/src/java/com/threerings/media/animation/ExplodeAnimation.java new file mode 100644 index 00000000..a272b085 --- /dev/null +++ b/src/java/com/threerings/media/animation/ExplodeAnimation.java @@ -0,0 +1,336 @@ +// +// $Id: ExplodeAnimation.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; + +import com.samskivert.util.RandomUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.media.image.Mirage; + +/** + * An animation that displays an object exploding into chunks, fading out + * as they fly apart. The animation ends when all chunks have exited the + * animation bounds, or when the given delay time (if any is specified) + * has elapsed. + */ +public class ExplodeAnimation extends Animation +{ + /** + * A class that describes an explosion's attributes. + */ + public static class ExplodeInfo + { + /** The bounds within which to animate. */ + public Rectangle bounds; + + /** The number of image chunks on each axis. */ + public int xchunk, ychunk; + + /** The maximum chunk velocity on each axis in pixels per + * millisecond. */ + public float xvel, yvel; + + /** The y-axis chunk acceleration in pixels per millisecond. */ + public float yacc; + + /** The chunk rotational velocity in rotations per millisecond. */ + public float rvel; + + /** The animation length in milliseconds, or -1 if the animation + * should continue until all pieces are outside the bounds. */ + public long delay; + + /** Returns a string representation of this instance. */ + public String toString () + { + return StringUtil.fieldsToString(this); + } + } + + /** + * Constructs an explode animation with the chunks represented as + * filled rectangles of the specified color. + * + * @param color the color to render the chunks in. + * @param info the explode info object. + * @param x the x-position of the object. + * @param y the y-position of the object. + * @param width the width of the object. + * @param height the height of the object. + */ + public ExplodeAnimation ( + Color color, ExplodeInfo info, int x, int y, int width, int height) + { + super(info.bounds); + + _color = color; + init(info, x, y, width, height); + } + + /** + * Constructs an explode animation with the chunks represented as + * portions of the actual image. + * + * @param image the image to animate. + * @param info the explode info object. + * @param x the x-position of the object. + * @param y the y-position of the object. + * @param width the width of the object. + * @param height the height of the object. + */ + public ExplodeAnimation ( + Mirage image, ExplodeInfo info, int x, int y, int width, int height) + { + super(info.bounds); + + _image = image; + init(info, x, y, width, height); + } + + /** + * Initializes the animation with the attributes of the given explode + * info object. + */ + protected void init (ExplodeInfo info, int x, int y, int width, int height) + { + _info = info; + _ox = x; + _oy = y; + _owid = width; + _ohei = height; + + _info.rvel = (float)((2.0f * Math.PI) * _info.rvel); + _chunkcount = (_info.xchunk * _info.ychunk); + _cxpos = new int[_chunkcount]; + _cypos = new int[_chunkcount]; + _sxvel = new float[_chunkcount]; + _syvel = new float[_chunkcount]; + + // determine chunk dimensions + _cwid = _owid / _info.xchunk; + _chei = _ohei / _info.ychunk; + _hcwid = _cwid / 2; + _hchei = _chei / 2; + + // initialize all chunks + for (int ii = 0; ii < _chunkcount; ii++) { + // initialize chunk position + int xpos = ii % _info.xchunk; + int ypos = ii / _info.xchunk; + _cxpos[ii] = _ox + (xpos * _cwid); + _cypos[ii] = _oy + (ypos * _chei); + + // initialize chunk velocity + _sxvel[ii] = RandomUtil.getFloat(_info.xvel) * + ((xpos < (_info.xchunk / 2)) ? -1.0f : 1.0f); + _syvel[ii] = -(RandomUtil.getFloat(_info.yvel)); + } + + // initialize the chunk rotation angle + _angle = 0.0f; + } + + // documentation inherited + public void fastForward (long timeDelta) + { + if (_start > 0) { + _start += timeDelta; + _end += timeDelta; + } + } + + // documentation inherited + public void tick (long timestamp) + { + if (_start == 0) { + // initialize our starting time + _start = timestamp; + if (_info.delay != -1) { + _end = _start + _info.delay; + } + } + + // figure out the distance the chunks have travelled + long msecs = timestamp - _start; + + if (_info.delay != -1) { + // calculate the alpha level with which to render the chunks + float pctdone = msecs / (float)_info.delay; + _alpha = Math.max(0.1f, Math.min(1.0f, 1.0f - pctdone)); + } + + // move all chunks and check whether any remain to be animated + int inside = 0; + for (int ii = 0; ii < _chunkcount; ii++) { + // determine the chunk travel distance + int xtrav = (int)(_sxvel[ii] * msecs); + int ytrav = (int) + ((_syvel[ii] * msecs) + (0.5f * _info.yacc * (msecs * msecs))); + + // determine the chunk movement direction + int xpos = ii % _info.xchunk; + int ypos = ii / _info.xchunk; + + // update the chunk position + _cxpos[ii] = _ox + (xpos * _cwid) + xtrav; + _cypos[ii] = _oy + (ypos * _chei) + ytrav; + + // note whether this chunk is still within our bounds + _wrect.setBounds(_cxpos[ii], _cypos[ii], _cwid, _chei); + if (_bounds.intersects(_wrect)) { + inside++; + } + } + + // increment the rotation angle + _angle += _info.rvel; + + // note whether we're finished + _finished = (inside == 0) || (_info.delay != -1 && timestamp >= _end); + + // dirty ourselves + invalidate(); + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + Shape oclip = gfx.getClip(); + Composite ocomp = null; + + if (_info.delay != -1) { + // set the alpha composite to reflect the current fade-out + ocomp = gfx.getComposite(); + gfx.setComposite( + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha)); + } + + for (int ii = 0; ii < _chunkcount; ii++) { + // get the chunk position within the image + int xpos = ii % _info.xchunk; + int ypos = ii / _info.xchunk; + + // calculate image chunk offset + int xoff = -(xpos * _cwid); + int yoff = -(ypos * _chei); + + // translate the origin to center on the chunk + int tx = _cxpos[ii] + _hcwid, ty = _cypos[ii] + _hchei; + gfx.translate(tx, ty); + + // set up the desired rotation + gfx.rotate(_angle); + + if (_image != null) { + // draw the image chunk + gfx.clipRect(-_hcwid, -_hchei, _cwid, _chei); + _image.paint(gfx, -_hcwid + xoff, -_hchei + yoff); + + } else { + // draw the color chunk + gfx.setColor(_color); + gfx.fillRect(-_hcwid, -_hchei, _cwid, _chei); + } + + // restore the original transform and clip + gfx.rotate(-_angle); + gfx.translate(-tx, -ty); + gfx.setClip(oclip); + } + + if (_info.delay != -1) { + // restore the original composite + gfx.setComposite(ocomp); + } + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + + buf.append(", ox=").append(_ox); + buf.append(", oy=").append(_oy); + buf.append(", cwid=").append(_cwid); + buf.append(", chei=").append(_chei); + buf.append(", hcwid=").append(_hcwid); + buf.append(", hchei=").append(_hchei); + buf.append(", chunkcount=").append(_chunkcount); + buf.append(", info=").append(_info); + } + + /** The current chunk rotation. */ + protected float _angle; + + /** The starting x-axis velocity of each chunk. */ + protected float[] _sxvel; + + /** The starting y-axis velocity of each chunk. */ + protected float[] _syvel; + + /** The current x-axis position of each chunk. */ + protected int[] _cxpos; + + /** The current y-axis position of each chunk. */ + protected int[] _cypos; + + /** The individual chunk dimensions in pixels. */ + protected int _cwid, _chei; + + /** The individual chunk dimensions in pixels, halved for handy use in + * repeated calculations. */ + protected int _hcwid, _hchei; + + /** The total number of image chunks. */ + protected int _chunkcount; + + /** The explode info. */ + protected ExplodeInfo _info; + + /** The exploding object position and dimensions. */ + protected int _ox, _oy, _owid, _ohei; + + /** The color to render the object chunks in if we're using a color. */ + protected Color _color; + + /** The image to animate if we're using an image. */ + protected Mirage _image; + + /** The starting animation time. */ + protected long _start; + + /** The ending animation time. */ + protected long _end; + + /** The percent alpha with which to render the chunks. */ + protected float _alpha; + + /** A reusable working rectangle. */ + protected Rectangle _wrect = new Rectangle(); +} diff --git a/src/java/com/threerings/media/animation/FadeAnimation.java b/src/java/com/threerings/media/animation/FadeAnimation.java new file mode 100644 index 00000000..5c0c7309 --- /dev/null +++ b/src/java/com/threerings/media/animation/FadeAnimation.java @@ -0,0 +1,85 @@ +// +// $Id: FadeAnimation.java 3427 2005-03-24 04:50:52Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.threerings.media.Log; +import com.threerings.media.effects.FadeEffect; + +/** + * An animation that displays an image fading from one alpha level to + * another in specified increments over time. The animation is finished + * when the specified target alpha is reached. + */ +public abstract class FadeAnimation extends Animation +{ + /** + * Constructs a fade animation. + * + * @param bounds our bounds. + * @param alpha the starting alpha. + * @param step the alpha amount to step by each millisecond. + * @param target the target alpha level. + */ + protected FadeAnimation ( + Rectangle bounds, float alpha, float step, float target) + { + super(bounds); + _effect = new FadeEffect(alpha, step, target); + } + + // documentation inherited + public void tick (long timestamp) + { + if (_effect.tick(timestamp)) { + _finished = _effect.finished(); + invalidate(); + } + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + _effect.beforePaint(gfx); + paintAnimation(gfx); + _effect.afterPaint(gfx); + } + + // documentation inherited + protected void willStart (long tickStamp) + { + super.willStart(tickStamp); + _effect.init(tickStamp); + } + + /** + * Here is where derived animations actually render their image. + */ + protected abstract void paintAnimation (Graphics2D gfx); + + /** This handles the main work of fading. */ + protected FadeEffect _effect; +} diff --git a/src/java/com/threerings/media/animation/FadeImageAnimation.java b/src/java/com/threerings/media/animation/FadeImageAnimation.java new file mode 100644 index 00000000..11814efb --- /dev/null +++ b/src/java/com/threerings/media/animation/FadeImageAnimation.java @@ -0,0 +1,52 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.threerings.media.image.Mirage; + +/** + * Fades an image in or out by varying the alpha level during rendering. + */ +public class FadeImageAnimation extends FadeAnimation +{ + /** + * Creates an image fading animation. + */ + public FadeImageAnimation (Mirage image, int x, int y, + float alpha, float step, float target) + { + super(new Rectangle(x, y, image.getWidth(), image.getHeight()), + alpha, step, target); + _image = image; + } + + // documentation inherited + protected void paintAnimation (Graphics2D gfx) + { + _image.paint(gfx, _bounds.x, _bounds.y); + } + + protected Mirage _image; +} diff --git a/src/java/com/threerings/media/animation/FadeLabelAnimation.java b/src/java/com/threerings/media/animation/FadeLabelAnimation.java new file mode 100644 index 00000000..68837883 --- /dev/null +++ b/src/java/com/threerings/media/animation/FadeLabelAnimation.java @@ -0,0 +1,99 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; + +import com.samskivert.swing.Label; +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.media.Log; + +/** + * Does something extraordinary. + */ +public class FadeLabelAnimation extends FadeAnimation +{ + /** + * Creates a label fading animation. + */ + public FadeLabelAnimation (Label label, int x, int y, + float alpha, float step, float target) + { + super(new Rectangle(x, y, 0, 0), alpha, step, target); + _label = label; + } + + /** + * Indicates that our label should be rendered with antialiased text. + */ + public void setAntiAliased (boolean antiAliased) + { + _antiAliased = antiAliased; + } + + // documentation inherited + protected void init () + { + super.init(); + + // if our label is not yet laid out, do the deed + if (!_label.isLaidOut()) { + Graphics2D gfx = (Graphics2D)_mgr.getMediaPanel().getGraphics(); + if (gfx != null) { + gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + _antiAliased ? + RenderingHints.VALUE_ANTIALIAS_ON : + RenderingHints.VALUE_ANTIALIAS_OFF); + _label.layout(gfx); + gfx.dispose(); + } + } + + // size the bounds to fit our label + Dimension size = _label.getSize(); + _bounds.width = size.width; + _bounds.height = size.height; + } + + // documentation inherited + protected void paintAnimation (Graphics2D gfx) + { + Object ohints = null; + if (_antiAliased) { + ohints = SwingUtil.activateAntiAliasing(gfx); + } + _label.render(gfx, _bounds.x, _bounds.y); + if (_antiAliased) { + SwingUtil.restoreAntiAliasing(gfx, ohints); + } + } + + /** The label we are rendering. */ + protected Label _label; + + /** Whether or not to use anti-aliased rendering. */ + protected boolean _antiAliased; +} diff --git a/src/java/com/threerings/media/animation/FloatingTextAnimation.java b/src/java/com/threerings/media/animation/FloatingTextAnimation.java new file mode 100644 index 00000000..d0f25131 --- /dev/null +++ b/src/java/com/threerings/media/animation/FloatingTextAnimation.java @@ -0,0 +1,248 @@ +// +// $Id: FloatingTextAnimation.java 3212 2004-11-12 00:33:38Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.samskivert.swing.Label; + +public class FloatingTextAnimation extends Animation +{ + /** + * Constructs an animation for the given text centered at the given + * coordinates. + */ + public FloatingTextAnimation (Label label, int x, int y) + { + this(label, x, y, DEFAULT_FLOAT_PERIOD); + } + + /** + * Constructs an animation for the given text centered at the given + * coordinates. The animation will float up the screen for 30 pixels. + */ + public FloatingTextAnimation (Label label, int x, int y, long floatPeriod) + { + this(label, x, y, x, y - DELTA_Y, floatPeriod); + } + + /** + * Constructs an animation for the given text starting at the given + * coordinates and floating toward the specified coordinates. + */ + public FloatingTextAnimation (Label label, int sx, int sy, + int destx, int desty, long floatPeriod) + { + super(new Rectangle(sx, sy, label.getSize().width, + label.getSize().height)); + + // save things off + _label = label; + _startX = _x = sx; + _startY = _y = sy; + _destx = destx; + _desty = desty; + _floatPeriod = floatPeriod; + + // calculate our deltas + _dx = (destx - sx); + _dy = (desty - sy); + + // initialize our starting alpha + _alpha = 1.0f; + } + + /** + * Called to change the direction 180 degrees. + */ + public void flipDirection () + { + _dx = -_dx; + _dy = -_dy; + } + + /** + * Returns the label used to render this score animation. + */ + public Label getLabel () + { + return _label; + } + + // documentation inherited + public void setLocation (int x, int y) + { + super.setLocation(x, y); + + // update our destination coordinates + _destx += (x - _startX); + _desty += (y - _startY); + + // update our initial coordinates + _startX = _x = x; + _startY = _y = y; + + // recalculate our deltas + _dx = (_destx - x); + _dy = (_desty - y); + } + + /** + * Sets the duration of this score animation to the specified time in + * milliseconds. This should be called before the animation is added + * to the animation manager. + */ + public void setFloatPeriod (long floatPeriod) + { + _floatPeriod = floatPeriod; + } + + // documentation inherited + public void tick (long timestamp) + { + boolean invalid = false; + if (_start == 0) { + // initialize our starting time + _start = timestamp; + // we need to make sure to invalidate ourselves initially + invalid = true; + } + + long fadeDelay = _floatPeriod/2; + long fadePeriod = _floatPeriod - fadeDelay; + + // figure out the current alpha + long msecs = timestamp - _start; + float oalpha = _alpha; + if (msecs > fadeDelay) { + long rmsecs = msecs - fadeDelay; + _alpha = Math.max(1.0f - (rmsecs / (float)fadePeriod), 0.0f); + _alpha = Math.min(_alpha, 1.0f); + } + + // get the alpha composite + _comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha); + + // determine the new y-position of the score + float pctdone = (float)msecs / _floatPeriod; + int ox = _x, oy = _y; + _x = _startX + (int)(_dx * pctdone); + _y = _startY + (int)(_dy * pctdone); + + // only update our location and dirty ourselves if we actually + // moved or our alpha changed + if (ox != _x || oy != _y) { + // dirty our old location + invalidate(); + _bounds.setLocation(_x, _y); + invalid = true; + + } else if (oalpha != _alpha) { + invalid = true; + } + + if (invalid) { + // dirty our current location + invalidate(); + } + + // note whether we're done + _finished = (msecs >= _floatPeriod); + } + + // documentation inherited + public void fastForward (long timeDelta) + { + if (_start > 0) { + _start += timeDelta; + } + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + Composite ocomp = gfx.getComposite(); + if (_comp != null) { + gfx.setComposite(_comp); + } + paintLabels(gfx, _x, _y); + gfx.setComposite(ocomp); + } + + /** + * Derived classes may wish to extend score animation and render more than + * just the standard single label. + * + * @param x the upper left coordinate of the animation. + * @param y the upper left coordinate of the animation. + */ + protected void paintLabels (Graphics2D gfx, int x, int y) + { + _label.render(gfx, x, y); + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + + buf.append(", x=").append(_x); + buf.append(", y=").append(_y); + buf.append(", alpha=").append(_alpha); + } + + /** The starting animation time. */ + protected long _start; + + /** The label we're animating. */ + protected Label _label; + + /** The starting coordinates of the score. */ + protected int _startX, _startY; + + /** The current coordinates of the score. */ + protected int _x, _y; + + /** The destination coordinates towards which the animation travels. */ + protected int _destx, _desty; + + /** The distance to be traveled by the score animation. */ + protected int _dx, _dy; + + /** The duration for which we float up the screen. */ + protected long _floatPeriod; + + /** The current alpha level used to render the score. */ + protected float _alpha; + + /** The composite used to render the score. */ + protected Composite _comp; + + /** The time in milliseconds during which the score is visible. */ + protected static final long DEFAULT_FLOAT_PERIOD = 1500L; + + /** The total vertical distance the score travels. */ + protected static final int DELTA_Y = 30; +} diff --git a/src/java/com/threerings/media/animation/GleamAnimation.java b/src/java/com/threerings/media/animation/GleamAnimation.java new file mode 100644 index 00000000..30531248 --- /dev/null +++ b/src/java/com/threerings/media/animation/GleamAnimation.java @@ -0,0 +1,150 @@ +// +// $Id: GleamAnimation.java 4191 2006-06-13 22:42:20Z ray $ + +package com.threerings.media.animation; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Transparency; + +import com.threerings.media.sprite.Sprite; +import com.threerings.media.sprite.SpriteManager; +import com.threerings.media.util.LinearTimeFunction; +import com.threerings.media.util.TimeFunction; + +/** + * Washes all non-transparent pixels in a sprite with a particular color + * (by compositing them with the solid color with progressively higher + * alpha values) and then back again. + */ +public class GleamAnimation extends Animation +{ + /** + * Creates a gleam animation with the supplied sprite. The sprite will + * be faded to the specified color and then back again. The sprite may + * be already added to the supplied sprite manager or not, but when + * the animation is complete, it will have been added. + * + * @param fadeIn if true, the sprite itself will be faded in as we + * fade up to the gleam color and the gleam color will fade out, + * leaving just the sprite imagery. + */ + public GleamAnimation (SpriteManager spmgr, Sprite sprite, Color color, + int upmillis, int downmillis, boolean fadeIn) + { + super(sprite.getBounds()); + _spmgr = spmgr; + _sprite = sprite; + _color = color; + _upfunc = new LinearTimeFunction(0, 750, upmillis); + _downfunc = new LinearTimeFunction(750, 0, downmillis); + _fadeIn = fadeIn; + } + + // documentation inherited + public void tick (long timestamp) + { + int alpha; + if (_upfunc != null) { + if ((alpha = _upfunc.getValue(timestamp)) == 750) { + _upfunc = null; + } + } else if (_downfunc != null) { + if ((alpha = _downfunc.getValue(timestamp)) == 0) { + _downfunc = null; + } + } else { + _finished = true; + _spmgr.addSprite(_sprite); + return; + } + + if (_alpha != alpha) { + _alpha = alpha; + invalidate(); + } + } + + // documentation inherited + public void fastForward (long timeDelta) + { + if (_upfunc != null) { + _upfunc.fastForward(timeDelta); + } else if (_downfunc != null) { + _downfunc.fastForward(timeDelta); + } + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + // TODO: recreate our off image if the sprite bounds changed; we + // also need to change the bounds of our animation which might + // require some jockeying (especially if we shrink) + if (_offimg == null) { + _offimg = gfx.getDeviceConfiguration().createCompatibleImage( + _bounds.width, _bounds.height, Transparency.TRANSLUCENT); + } + + // create a mask image with our sprite and the appropriate color + Graphics2D ogfx = (Graphics2D)_offimg.getGraphics(); + try { + ogfx.setColor(_color); + ogfx.fillRect(0, 0, _bounds.width, _bounds.height); + ogfx.setComposite(AlphaComposite.DstAtop); + ogfx.translate(-_sprite.getX(), -_sprite.getY()); + _sprite.paint(ogfx); + } finally { + ogfx.dispose(); + } + + Composite ocomp = null; + Composite ncomp = AlphaComposite.getInstance( + AlphaComposite.SRC_OVER, _alpha/1000f); + + // if we're fading the sprite in on the way up, set our alpha + // composite before we render the sprite + if (_fadeIn && _upfunc != null) { + ocomp = gfx.getComposite(); + gfx.setComposite(ncomp); + } + + // next render the sprite + _sprite.paint(gfx); + + // if we're not fading in, we still need to alpha the white bits + if (ocomp == null) { + ocomp = gfx.getComposite(); + gfx.setComposite(ncomp); + } + + // now alpha composite our mask atop the sprite + gfx.drawImage(_offimg, _sprite.getX(), _sprite.getY(), null); + gfx.setComposite(ocomp); + } + + // documentation inherited + protected void willStart (long tickStamp) + { + super.willStart(tickStamp); + + // remove the sprite we're fiddling with from the manager; we'll + // add it back when we're done + if (_spmgr.isManaged(_sprite)) { + _spmgr.removeSprite(_sprite); + } + } + + protected SpriteManager _spmgr; + protected Sprite _sprite; + protected Color _color; + protected Image _offimg; + protected boolean _fadeIn; + + protected TimeFunction _upfunc; + protected TimeFunction _downfunc; + protected int _alpha = -1; +} diff --git a/src/java/com/threerings/media/animation/MultiFrameAnimation.java b/src/java/com/threerings/media/animation/MultiFrameAnimation.java new file mode 100644 index 00000000..98a48dfd --- /dev/null +++ b/src/java/com/threerings/media/animation/MultiFrameAnimation.java @@ -0,0 +1,134 @@ +// +// $Id: MultiFrameAnimation.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.threerings.media.util.FrameSequencer; +import com.threerings.media.util.MultiFrameImage; + +/** + * Animates a sequence of image frames in place with a particular frame + * rate. + */ +public class MultiFrameAnimation extends Animation +{ + /** + * Creates a multi-frame animation with the specified source image + * frames and the specified target frame rate (in frames per second). + * + * @param frames the source frames of the animation. + * @param fps the target number of frames per second. + * @param loop whether the animation should loop indefinitely or + * finish after one shot. + */ + public MultiFrameAnimation ( + MultiFrameImage frames, double fps, boolean loop) + { + this(frames, new FrameSequencer.ConstantRate(fps, loop)); + } + + /** + * Creates a multi-frame animation with the specified source image + * frames and the specified target frame rate (in frames per second). + */ + public MultiFrameAnimation (MultiFrameImage frames, FrameSequencer seeker) + { + // we'll set up our bounds via setLocation() and in reset() + super(new Rectangle()); + + _frames = frames; + _seeker = seeker; + + // reset ourselves to start things off + reset(); + } + + // documentation inherited + public Rectangle getBounds () + { + // fill in the bounds with our current animation frame's bounds + return _bounds; + } + + /** + * If this animation has run to completion, it can be reset to prepare + * it for another go. + */ + public void reset () + { + super.reset(); + + // set the frame number to -1 so that we don't ignore the + // transition to frame zero on the first call to tick() + _fidx = -1; + + // reset our frame sequencer + _seeker.init(_frames); + if (_seeker instanceof AnimationFrameSequencer) { + ((AnimationFrameSequencer) _seeker).setAnimation(this); + } + } + + // documentation inherited + public void tick (long tickStamp) + { + int fidx = _seeker.tick(tickStamp); + if (fidx == -1) { + _finished = true; + + } else if (fidx != _fidx) { + // update our frame index and bounds + setFrameIndex(fidx); + + // and have ourselves repainted + invalidate(); + } + } + + /** + * Sets the frame index and updates our dimensions. + */ + protected void setFrameIndex (int fidx) + { + _fidx = fidx; + _bounds.width = _frames.getWidth(_fidx); + _bounds.height = _frames.getHeight(_fidx); + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + _frames.paintFrame(gfx, _fidx, _bounds.x, _bounds.y); + } + + // documentation inherited + public void fastForward (long timeDelta) + { + _seeker.fastForward(timeDelta); + } + + protected MultiFrameImage _frames; + protected FrameSequencer _seeker; + protected int _fidx; +} diff --git a/src/java/com/threerings/media/animation/RainAnimation.java b/src/java/com/threerings/media/animation/RainAnimation.java new file mode 100644 index 00000000..931600af --- /dev/null +++ b/src/java/com/threerings/media/animation/RainAnimation.java @@ -0,0 +1,134 @@ +// +// $Id: RainAnimation.java 4188 2006-06-13 18:03:48Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.samskivert.util.RandomUtil; + +/** + * An animation that displays raindrops spattering across an image. + */ +public class RainAnimation extends Animation +{ + /** + * Constructs a rain animation with reasonable defaults for the number + * of raindrops and their dimensions. + * + * @param bounds the bounding rectangle for the animation. + * @param duration the number of seconds the animation should last. + */ + public RainAnimation (Rectangle bounds, long duration) + { + super(bounds); + + init(duration, DEFAULT_COUNT, DEFAULT_WIDTH, DEFAULT_HEIGHT); + } + + /** + * Constructs a rain animation. + * + * @param bounds the bounding rectangle for the animation. + * @param duration the number of seconds the animation should last. + * @param count the number of raindrops to render in each frame. + * @param wid the width of a raindrop's bounding rectangle. + * @param hei the height of a raindrop's bounding rectangle. + */ + public RainAnimation ( + Rectangle bounds, long duration, int count, int wid, int hei) + { + super(bounds); + init(duration, count, wid, hei); + } + + protected void init (long duration, int count, int wid, int hei) + { + // save things off + _count = count; + _wid = wid; + _hei = hei; + + // create the raindrop array + _drops = new int[count]; + + // calculate ending time + _duration = duration; + } + + // documentation inherited + public void tick (long tickStamp) + { + // grab our ending time on our first tick + if (_end == 0L) { + _end = tickStamp + _duration; + } + + _finished = (tickStamp >= _end); + + // calculate the latest raindrop locations + for (int ii = 0; ii < _count; ii++) { + int x = RandomUtil.getInt(_bounds.width); + int y = RandomUtil.getInt(_bounds.height); + _drops[ii] = (x << 16 | y); + } + + invalidate(); + } + + // documentation inherited + public void fastForward (long timeDelta) + { + _end += timeDelta; + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + gfx.setColor(Color.white); + for (int ii = 0; ii < _count; ii++) { + int x = _drops[ii] >> 16; + int y = _drops[ii] & 0xFFFF; + gfx.drawLine(x, y, x + _wid, y + _hei); + } + } + + /** The number of raindrops. */ + protected static final int DEFAULT_COUNT = 300; + + /** The raindrop streak dimensions. */ + protected static final int DEFAULT_WIDTH = 13; + protected static final int DEFAULT_HEIGHT = 10; + + /** The number of raindrops. */ + protected int _count; + + /** The dimensions of each raindrop's bounding rectangle. */ + protected int _wid, _hei; + + /** The raindrop locations. */ + protected int[] _drops; + + /** Animation ending timing information. */ + protected long _duration, _end; +} diff --git a/src/java/com/threerings/media/animation/ScaleAnimation.java b/src/java/com/threerings/media/animation/ScaleAnimation.java new file mode 100644 index 00000000..bc612752 --- /dev/null +++ b/src/java/com/threerings/media/animation/ScaleAnimation.java @@ -0,0 +1,192 @@ +// +// $Id: ScaleAnimation.java 3123 2004-09-18 22:58:39Z mdb $ + +package com.threerings.media.animation; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Image; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Transparency; + +import java.awt.image.BufferedImage; + +import com.threerings.media.image.Mirage; +import com.threerings.media.sprite.Sprite; +import com.threerings.media.sprite.SpriteManager; +import com.threerings.media.util.LinearTimeFunction; +import com.threerings.media.util.TimeFunction; + +/** + * Animates an image changing size about its center point. + */ +public class ScaleAnimation extends Animation +{ + /** + * Creates a scale animation with the supplied image. If the image's + * size would ever be 0 or less, it is not drawn. + * + * @param image The image to paint. + * + * @param center The screen coordinates of the pixel upon which the + * image's center should always be rendered. + * + * @param startScale The amount to scale the image when it is rendered + * at time 0. + * + * @param endScale The amount to scale the image at the final frame + * of animation. + * + * @param duration The time in milliseconds the anim takes to complete. + */ + public ScaleAnimation (Mirage image, Point center, + float startScale, float endScale, int duration) + { + super(getBounds(Math.max(startScale, endScale), center, image)); + + // Save inputted variables + _image = image; + _bufferedImage = _image.getSnapshot(); + _center = new Point(center); + _startScale = startScale; + _endScale = endScale; + _duration = duration; + + // Hack the LinearTimeFunction to use fixed point rationals + // + // FIXME: This class doesn't seem to be saving me a lot of + // work, since I have to repackage the outputs into floats + // anyway. Find some way to make the LinearTimeFunction do + // more of this work for us, or write a new class that does. + // Maybe IntLinearTimeFunction and FloatLinearTimeFunction + // classes would be useful. + _scaleFunc = new LinearTimeFunction(0, 10000, duration); + } + + /** + * Java wants the first call in a constructor to be super() + * if it exists at all, so we have to trick it with this function. + * + * Oh, and this function computes how big the bounding box needs + * to be to bound the inputted image scaled to the inputted size + * centered around the inputted center poitn. + */ + public static Rectangle getBounds (float scale, Point center, Mirage image) + { + Point size = getSize(scale, image); + Point corner = getCorner(center, size); + return new Rectangle(corner.x, corner.y, size.x, size.y); + } + + /** + * Compute the bounds to use to render the animation's image + * right now. + */ + public Rectangle getBounds () + { + return getBounds(_scale, _center, _image); + } + + /** Computes the width and height to which an image should be scaled. */ + public static Point getSize (float scale, Mirage image) + { + int width = Math.max(0, Math.round(image.getWidth() * scale)); + int height = Math.max(0, Math.round(image.getHeight() * scale)); + return new Point(width, height); + } + + /** + * Computes the upper left corner where the image should be drawn, + * given the center and dimensions to which the image should be scaled. + */ + public static Point getCorner (Point center, Point size) + { + return new Point(center.x - size.x/2, center.y - size.y/2); + } + + // documentation inherited + public void tick (long tickStamp) + { + // Compute the new scaling value + float weight = _scaleFunc.getValue(tickStamp) / 10000.0f; + float scale = ((1.0f - weight) * _startScale) + + (( weight) * _endScale); + + // Update the animation if the scaling changes + if (_scale != scale) + { + _scale = scale; + invalidate(); + } + + // Check if the animation completed + if (weight >= 1.0f) { + _finished = true; + } + } + + // documentation inherited + public void fastForward (long timeDelta) + { + _scaleFunc.fastForward(timeDelta); + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + // Compute the bounding box to render this image + Rectangle bounds = getBounds(); + + // Paint nothing if the image was scaled to nothing + if (bounds.width <= 0 || bounds.height <= 0) { + return; + } + + // Smooth out the image scaling + // + // FIXME: Should this be turned off when the painting is done? + gfx.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + + // Paint the image scaled to this location + gfx.drawImage(_bufferedImage, + bounds.x, + bounds.y, + bounds.x + bounds.width, + bounds.y + bounds.height, + 0, + 0, + _bufferedImage.getWidth(), + _bufferedImage.getHeight(), + null); + } + + /** The image to scale. */ + protected Mirage _image; + + /** The image converted to a format Graphics2D likes, and cached. */ + protected BufferedImage _bufferedImage; + + /** The center pixel to render the image around. */ + protected Point _center; + + /** The amount of time the animation should last. */ + //XXX Is this needed? + protected long _duration; + + /** The amount to scale the image at the start of the animation. */ + protected float _startScale; + + /** The amount to scale the image at the end of the animation. */ + protected float _endScale; + + /** The current amount of scaling to render. */ + protected float _scale; + + /** Computes the image scaling to use at the specified time. */ + protected TimeFunction _scaleFunc; +} diff --git a/src/java/com/threerings/media/animation/SequencedAnimationObserver.java b/src/java/com/threerings/media/animation/SequencedAnimationObserver.java new file mode 100644 index 00000000..f0e38d84 --- /dev/null +++ b/src/java/com/threerings/media/animation/SequencedAnimationObserver.java @@ -0,0 +1,35 @@ +// +// $Id: SequencedAnimationObserver.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +/** + * Extends the animation observer interface with extra goodies. + */ +public interface SequencedAnimationObserver extends AnimationObserver +{ + /** + * Called when the observed animation -- previously configured with an + * {@link AnimationFrameSequencer} -- reached the specified frame. + */ + public void frameReached (Animation anim, long when, + int frameIdx, int frameSeq); +} diff --git a/src/java/com/threerings/media/animation/SparkAnimation.java b/src/java/com/threerings/media/animation/SparkAnimation.java new file mode 100644 index 00000000..9e6af1da --- /dev/null +++ b/src/java/com/threerings/media/animation/SparkAnimation.java @@ -0,0 +1,257 @@ +// +// $Id: SparkAnimation.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; + +import com.samskivert.util.RandomUtil; + +import com.threerings.media.animation.Animation; +import com.threerings.media.image.Mirage; + +import com.threerings.media.Log; + +/** + * Displays a set of spark images originating from a specified position + * and flying outward in random directions, fading out as they go, for a + * specified period of time. + */ +public class SparkAnimation extends Animation +{ + /** + * Constructs a spark animation with the supplied parameters. + * + * @param bounds the bounding rectangle for the animation. + * @param x the starting x-position for the sparks. + * @param y the starting y-position for the sparks. + * @param xjog the maximum X distance by which to "jog" the initial spark + * positions, or 0 if no jogging is desired. + * @param yjog the maximum Y distance by which to "jog" the initial spark + * positions, or 0 if no jogging is desired. + * @param minxvel the minimum starting x-velocity of the sparks. + * @param minyvel the minimum starting y-velocity of the sparks. + * @param maxxvel the maximum x-velocity of the sparks. + * @param maxyvel the maximum y-velocity of the sparks. + * @param xacc the x axis acceleration, or 0 if none is desired. + * @param yacc the y axis acceleration, or 0 if none is desired. + * @param images the spark images to be animated. + * @param delay the duration of the animation in milliseconds. + * @param fade do the fade thing + */ + public SparkAnimation (Rectangle bounds, int x, int y, int xjog, int yjog, + float minxvel, float minyvel, + float maxxvel, float maxyvel, + float xacc, float yacc, + Mirage[] images, long delay, boolean fade) + { + super(bounds); + + // save things off + _xacc = xacc; + _yacc = yacc; + _images = images; + _delay = delay; + _fade = fade; + + // initialize various things + _icount = images.length; + _ox = new int[_icount]; + _oy = new int[_icount]; + _xpos = new int[_icount]; + _ypos = new int[_icount]; + _sxvel = new float[_icount]; + _syvel = new float[_icount]; + + for (int ii = 0; ii < _icount; ii++) { + // initialize spark position + _ox[ii] = x + + ((xjog == 0) ? 0 : RandomUtil.getInt(xjog) * randomDirection()); + _oy[ii] = y + + ((yjog == 0) ? 0 : RandomUtil.getInt(yjog) * randomDirection()); + + // Choose random X and Y axis velocities between the inputted + // bounds + _sxvel[ii] = minxvel + RandomUtil.getFloat(1) * (maxxvel - minxvel); + _syvel[ii] = minyvel + RandomUtil.getFloat(1) * (maxyvel - minyvel); + + // If accelerationes were given, make the starting velocities + // move against that acceleration; otherwise pick directions + // at random + if (_xacc > 0) { + _sxvel[ii] = -_sxvel[ii]; + } else if (_xacc == 0) { + _sxvel[ii] *= randomDirection(); + } + if (_yacc > 0) { + _syvel[ii] = -_syvel[ii]; + } else if (_yacc == 0) { + _syvel[ii] *= randomDirection(); + } + } + + if (_fade) { + _alpha = 1.0f; + _comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha); + } + } + + /** + * Returns at random -1 for negative direction or +1 for positive. + */ + protected int randomDirection () + { + return (RandomUtil.getInt(2) == 0) ? -1 : 1; + } + + // documentation inherited + protected void willStart (long stamp) + { + super.willStart(stamp); + _start = stamp; + } + + // documentation inherited + public void fastForward (long timeDelta) + { + _start += timeDelta; + } + + // documentation inherited + public void tick (long timestamp) + { + // figure out the distance the chunks have travelled + long msecs = Math.max(timestamp - _start, 0); + long msecsSq = msecs * msecs; + + // calculate the alpha level with which to render the chunks + if (_fade) { + float pctdone = msecs / (float)_delay; + _alpha = Math.max(0.1f, Math.min(1.0f, 1.0f - pctdone)); + _comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha); + } + + // assume all sparks have moved outside the bounds + boolean allOutside = true; + + // move all sparks and check whether any remain to be animated + for (int ii = 0; ii < _icount; ii++) { + // determine the travel distance + int xtrav = (int) + ((_sxvel[ii] * msecs) + (0.5f * _xacc * msecsSq)); + int ytrav = (int) + ((_syvel[ii] * msecs) + (0.5f * _yacc * msecsSq)); + + // update the position + _xpos[ii] = _ox[ii] + xtrav; + _ypos[ii] = _oy[ii] + ytrav; + + // check to see if any are still in. Stop looking + // when we find one + if (allOutside && _bounds.intersects(_xpos[ii], _ypos[ii], + _images[ii].getWidth(), _images[ii].getHeight())) { + allOutside = false; + } + } + + // note whether we're finished + _finished = allOutside || (msecs >= _delay); + + // dirty ourselves + // TODO: only do this if at least one spark actually moved + invalidate(); + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + Shape oclip = gfx.getClip(); + gfx.clip(_bounds); + Composite ocomp = gfx.getComposite(); + + if (_fade) { + // set the alpha composite to reflect the current fade-out + gfx.setComposite(_comp); + } + + // draw all sparks + for (int ii = 0; ii < _icount; ii++) { + _images[ii].paint(gfx, _xpos[ii], _ypos[ii]); + } + + // restore the original gfx settings + gfx.setComposite(ocomp); + gfx.setClip(oclip); + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + + buf.append(", ox=").append(_ox); + buf.append(", oy=").append(_oy); + buf.append(", alpha=").append(_alpha); + } + + /** The spark images we're animating. */ + protected Mirage[] _images; + + /** The number of images we're animating. */ + protected int _icount; + + /** The x axis acceleration in pixels per millisecond. */ + protected float _xacc; + + /** The y axis acceleration in pixels per millisecond. */ + protected float _yacc; + + /** The starting x-axis velocity of each chunk. */ + protected float[] _sxvel; + + /** The starting y-axis velocity of each chunk. */ + protected float[] _syvel; + + /** The starting 'jog' positions for each spark. */ + protected int[] _ox, _oy; + + /** The current positions of each spark. */ + protected int[] _xpos, _ypos; + + /** The starting animation time. */ + protected long _start; + + /** Whether or not we should fade the sparks out. */ + protected boolean _fade; + + /** The percent alpha with which to render the images. */ + protected float _alpha; + + /** The alpha composite with which to render the images. */ + protected AlphaComposite _comp; + + /** The duration of the spark animation in milliseconds. */ + protected long _delay; +} diff --git a/src/java/com/threerings/media/animation/SpriteAnimation.java b/src/java/com/threerings/media/animation/SpriteAnimation.java new file mode 100644 index 00000000..9e9a5e54 --- /dev/null +++ b/src/java/com/threerings/media/animation/SpriteAnimation.java @@ -0,0 +1,102 @@ +// +// $Id: SpriteAnimation.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.animation; + +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.threerings.media.sprite.Sprite; +import com.threerings.media.sprite.SpriteManager; +import com.threerings.media.sprite.PathObserver; +import com.threerings.media.util.Path; + +public class SpriteAnimation extends Animation + implements PathObserver +{ + /** + * Constructs a sprite animation for the given sprite. The first time + * the animation is ticked, the sprite will be added to the given + * sprite manager and started along the supplied path. + */ + public SpriteAnimation (SpriteManager spritemgr, Sprite sprite, Path path) + { + super(new Rectangle()); + + // save things off + _spritemgr = spritemgr; + _sprite = sprite; + _path = path; + + // set up our sprite business + _sprite.addSpriteObserver(this); + } + + /** + * Derived classes can override tick if they want to end the animation + * in some other way than the sprite completing a path. + */ + public void tick (long timestamp) + { + // start the sprite moving on its path on our first tick + if (_path != null) { + _spritemgr.addSprite(_sprite); + _sprite.move(_path); + _path = null; + } + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + // nothing for now + } + + // documentation inherited from interface + public void pathCancelled (Sprite sprite, Path path) + { + _finished = true; + } + + // documentation inherited from interface + public void pathCompleted (Sprite sprite, Path path, long when) + { + _finished = true; + } + + // documentation inherited + protected void didFinish (long tickStamp) + { + super.didFinish(tickStamp); + + _spritemgr.removeSprite(_sprite); + _sprite = null; + } + + /** The sprite associated with this animation. */ + protected Sprite _sprite; + + /** The sprite manager managing our sprite. */ + protected SpriteManager _spritemgr; + + /** The path along which we'll move our sprite. */ + protected Path _path; +} diff --git a/src/java/com/threerings/media/effects/FadeEffect.java b/src/java/com/threerings/media/effects/FadeEffect.java new file mode 100644 index 00000000..e475b4ef --- /dev/null +++ b/src/java/com/threerings/media/effects/FadeEffect.java @@ -0,0 +1,125 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.effects; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.Graphics2D; + +/** + * Handles the math and timing of doing a fade effect in a sprite or animation. + */ +public class FadeEffect +{ + public FadeEffect (float alpha, float step, float target) + { + if ((step == 0f) || (alpha > target && step > 0f) || + (alpha < target && step < 0f)) { + throw new IllegalArgumentException("Step specified is illegal: " + + "Fade would never finish (start=" + alpha + ", step=" + step + + ", target=" + target + ")"); + } + + // save things off + _startAlpha = alpha; + _step = step; + _target = target; + + // create the initial composite + _alpha = Math.max(0f, Math.min(1f, _startAlpha)); + _comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha); + } + + public void init (long tickStamp) + { + _finished = false; + _initStamp = tickStamp; + } + + public boolean finished () + { + return _finished; + } + + public float getAlpha () + { + return _alpha; + } + + public boolean tick (long tickStamp) + { + // figure out the current alpha + long msecs = tickStamp - _initStamp; + float alpha = _startAlpha + (msecs * _step); + _finished = (_startAlpha < _target) ? (alpha >= _target) + : (alpha <= _target); + if (alpha < 0.0f) { + alpha = 0.0f; + } else if (alpha > 1.0f) { + alpha = 1.0f; + } + + if (_alpha != alpha) { + _alpha = alpha; + _comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha); + return true; + } + + // return false, unless we're finished + return _finished; + } + + public void beforePaint (Graphics2D gfx) + { + _ocomp = gfx.getComposite(); + gfx.setComposite(_comp); + } + + public void afterPaint (Graphics2D gfx) + { + gfx.setComposite(_ocomp); + } + + /** The composite used to render the image with the current alpha. */ + protected Composite _comp; + + /** The composite in effect outside our faded render. */ + protected Composite _ocomp; + + /** The current alpha of the image. */ + protected float _alpha; + + /** The target alpha. */ + protected float _target; + + /** The alpha step per millisecond. */ + protected float _step; + + /** The starting alpha. */ + protected float _startAlpha; + + /** Time zero. */ + protected long _initStamp; + + /** True when we're finished. */ + protected boolean _finished; +} diff --git a/src/java/com/threerings/media/image/AWTImageCreator.java b/src/java/com/threerings/media/image/AWTImageCreator.java new file mode 100644 index 00000000..92cbfeb6 --- /dev/null +++ b/src/java/com/threerings/media/image/AWTImageCreator.java @@ -0,0 +1,71 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Component; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.Transparency; +import java.awt.image.BufferedImage; + +/** + * If the user of the image manager services intends to create and display + * images using the AWT, they can use this image creator which will use the + * AWT to determine the optimal image format. + */ +public class AWTImageCreator + implements ImageManager.OptimalImageCreator +{ + /** + * Create an image creator that will rely on the AWT to determine the + * optimal image format. + * + * @param context if non-null, the graphics configuration will be obtained + * therefrom; otherwise {@link GraphicsDevice#getDefaultConfiguration} + * will be used. + */ + public AWTImageCreator (Component context) + { + // obtain our graphics configuration + if (context != null) { + _gc = context.getGraphicsConfiguration(); + } else { + _gc = ImageUtil.getDefGC(); + } + } + + // documentation inherited from interface ImageManager.OptimalImageCreator + public BufferedImage createImage (int width, int height, int trans) + { + // DEBUG: override transparency for the moment on all images + trans = Transparency.TRANSLUCENT; + if (_gc != null) { + return _gc.createCompatibleImage(width, height, trans); + } else { + // if we're running in headless mode, do everything in 24-bit + return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + } + + /** The graphics configuration for the default screen device. */ + protected GraphicsConfiguration _gc; +} diff --git a/src/java/com/threerings/media/image/BackedVolatileMirage.java b/src/java/com/threerings/media/image/BackedVolatileMirage.java new file mode 100644 index 00000000..233d3292 --- /dev/null +++ b/src/java/com/threerings/media/image/BackedVolatileMirage.java @@ -0,0 +1,82 @@ +// +// $Id: BackedVolatileMirage.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; + +import com.threerings.media.Log; + +/** + * Provides a volatile mirage that is backed by a buffered image that is + * not obtained from the image manager but is instead provided at + * construct time and completely circumvents the image manager's cache. As + * such, this should not be used unless you know what you're doing. + */ +public class BackedVolatileMirage extends VolatileMirage +{ + /** + * Creates a mirage with the supplied regeneration informoation and + * prepared image. + */ + public BackedVolatileMirage (ImageManager imgr, BufferedImage source) + { + super(imgr, new Rectangle(0, 0, source.getWidth(), source.getHeight())); + _source = source; + + // create our volatile image for the first time + createVolatileImage(); + } + + // documentation inherited + protected int getTransparency () + { + return _source.getColorModel().getTransparency(); + } + + // documentation inherited + protected void refreshVolatileImage () + { + Graphics gfx = null; + try { + gfx = _image.getGraphics(); + gfx.drawImage(_source, -_bounds.x, -_bounds.y, null); + + } catch (Exception e) { + Log.warning("Failure refreshing mirage " + this + "."); + Log.logStackTrace(e); + + } finally { + gfx.dispose(); + } + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", src=").append(_source); + } + + protected BufferedImage _source; +} diff --git a/src/java/com/threerings/media/image/BlankMirage.java b/src/java/com/threerings/media/image/BlankMirage.java new file mode 100644 index 00000000..a75d4ef3 --- /dev/null +++ b/src/java/com/threerings/media/image/BlankMirage.java @@ -0,0 +1,77 @@ +// +// $Id: BlankMirage.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +/** + * A mirage implementation that contains no image data. Generally only + * useful for testing. + */ +public class BlankMirage implements Mirage +{ + public BlankMirage (int width, int height) + { + _width = width; + _height = height; + } + + // documentation inherited from interface + public void paint (Graphics2D gfx, int x, int y) + { + // nothing doing + } + + // documentation inherited from interface + public int getWidth () + { + return _width; + } + + // documentation inherited from interface + public int getHeight () + { + return _height; + } + + // documentation inherited from interface + public boolean hitTest (int x, int y) + { + return false; + } + + // documentation inherited from interface + public long getEstimatedMemoryUsage () + { + return 0; + } + + // documentation inherited from interface + public BufferedImage getSnapshot () + { + return null; + } + + protected int _width; + protected int _height; +} diff --git a/src/java/com/threerings/media/image/BufferedMirage.java b/src/java/com/threerings/media/image/BufferedMirage.java new file mode 100644 index 00000000..0420f233 --- /dev/null +++ b/src/java/com/threerings/media/image/BufferedMirage.java @@ -0,0 +1,74 @@ +// +// $Id: BufferedMirage.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +/** + * A simple mirage implementation that uses a buffered image. + */ +public class BufferedMirage implements Mirage +{ + public BufferedMirage (BufferedImage image) + { + _image = image; + } + + // documentation inherited from interface + public void paint (Graphics2D gfx, int x, int y) + { + gfx.drawImage(_image, x, y, null); + } + + // documentation inherited from interface + public int getWidth () + { + return _image.getWidth(); + } + + // documentation inherited from interface + public int getHeight () + { + return _image.getHeight(); + } + + // documentation inherited from interface + public boolean hitTest (int x, int y) + { + return ImageUtil.hitTest(_image, x, y); + } + + // documentation inherited from interface + public long getEstimatedMemoryUsage () + { + return ImageUtil.getEstimatedMemoryUsage(_image.getRaster()); + } + + // documentation inherited from interface + public BufferedImage getSnapshot () + { + return _image; + } + + protected BufferedImage _image; +} diff --git a/src/java/com/threerings/media/image/CachedVolatileMirage.java b/src/java/com/threerings/media/image/CachedVolatileMirage.java new file mode 100644 index 00000000..7725080c --- /dev/null +++ b/src/java/com/threerings/media/image/CachedVolatileMirage.java @@ -0,0 +1,104 @@ +// +// $Id: CachedVolatileMirage.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.Transparency; +import java.awt.image.BufferedImage; + +import com.threerings.media.Log; + +/** + * A mirage implementation which allows the image to be maintained in + * video memory and refetched from the image manager in the event that our + * target screen resolution changes or we are flushed from video memory + * for some other reason. + * + *

These objects are never created directly, but always obtained from + * the {@link ImageManager}. + */ +public class CachedVolatileMirage extends VolatileMirage +{ + /** + * Creates a mirage with the supplied regeneration informoation and + * prepared image. + */ + protected CachedVolatileMirage ( + ImageManager imgr, ImageManager.ImageKey source, + Rectangle bounds, Colorization[] zations) + { + super(imgr, bounds); + + _source = source; + _zations = zations; + + // create our volatile image for the first time + createVolatileImage(); + } + + // documentation inherited + protected int getTransparency () + { + BufferedImage source = _imgr.getImage(_source, _zations); + return (source == null) ? Transparency.OPAQUE : + source.getColorModel().getTransparency(); + } + + // documentation inherited + protected void refreshVolatileImage () + { + Graphics gfx = null; + try { + BufferedImage source = _imgr.getImage(_source, _zations); + if (source != null) { + gfx = _image.getGraphics(); + gfx.drawImage(source, -_bounds.x, -_bounds.y, null); + } + + } catch (Exception e) { + Log.warning("Failure refreshing mirage " + this + "."); + Log.logStackTrace(e); + + } finally { + if (gfx != null) { + gfx.dispose(); + } + } + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", key=").append(_source); + buf.append(", zations=").append(_zations); + } + + /** The key that identifies the image data used to create our volatile + * image. */ + protected ImageManager.ImageKey _source; + + /** Optional colorizations that are applied to our source image when + * creating our mirage. */ + protected Colorization[] _zations; +} diff --git a/src/java/com/threerings/media/image/ColorPository.java b/src/java/com/threerings/media/image/ColorPository.java new file mode 100644 index 00000000..210401bb --- /dev/null +++ b/src/java/com/threerings/media/image/ColorPository.java @@ -0,0 +1,461 @@ +// +// $Id: ColorPository.java 4188 2006-06-13 18:03:48Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Iterator; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; + +import java.text.ParseException; + +import com.samskivert.util.HashIntMap; +import com.samskivert.util.RandomUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.media.Log; +import com.threerings.resource.ResourceManager; +import com.threerings.util.CompiledConfig; + +/** + * A repository of image recoloration information. It was called the + * recolor repository but the re-s cancelled one another out. + */ +public class ColorPository implements Serializable +{ + /** + * Used to store information on a class of colors. These are public to + * simplify the XML parsing process, so pay them no mind. + */ + public static class ClassRecord implements Serializable + { + /** An integer identifier for this class. */ + public int classId; + + /** The name of the color class. */ + public String name; + + /** The source color to use when recoloring colors in this class. */ + public Color source; + + /** Data identifying the range of colors around the source color + * that will be recolored when recoloring using this class. */ + public float[] range; + + /** The default starting legality value for this color class. See + * {@link ColorRecord#starter}. */ + public boolean starter; + + /** The default colorId to use for recoloration in this class, or + * 0 if there is no default defined. */ + public int defaultId; + + /** A table of target colors included in this class. */ + public HashIntMap colors = new HashIntMap(); + + /** Used when parsing the color definitions. */ + public void addColor (ColorRecord record) + { + // validate the color id + if (record.colorId > 255) { + Log.warning("Refusing to add color record; colorId > 255 " + + "[class=" + this + ", record=" + record + "]."); + } else { + record.cclass = this; + colors.put(record.colorId, record); + } + } + + /** + * Translants a color identified in string form into the id that + * should be used to look up its information. Throws an + * exception if no color could be found that associates with + * that name. + * + * FIXME: This lookup could be sped up a lot with some cached + * data tables if it looked like this function would get called + * in time critical situations. + */ + public int getColorId (String name) + throws ParseException + { + // Check if the string is itself a number + try { + int id = Integer.parseInt(name); + if (colors.containsKey(id)) { + return id; + } + } catch (NumberFormatException e) { + // Guess it must be something else + } + + // Look for name matches among all colors + Iterator iter = colors.values().iterator(); + while (iter.hasNext()) { + ColorRecord color = (ColorRecord)iter.next(); + if (color.name.equalsIgnoreCase(name)) { + return color.colorId; + } + } + + // That input wasn't a color + throw new ParseException("No color named '" + name + "'", 0); + } + + /** Returns a random starting id from the entries in this + * class. */ + public ColorRecord randomStartingColor () + { + // figure out our starter ids if we haven't already + if (_starters == null) { + ArrayList list = new ArrayList(); + Iterator iter = colors.values().iterator(); + while (iter.hasNext()) { + ColorRecord color = (ColorRecord)iter.next(); + if (color.starter) { + list.add(color); + } + } + _starters = (ColorRecord[]) + list.toArray(new ColorRecord[list.size()]); + } + + // sanity check + if (_starters.length < 1) { + Log.warning("Requested random starting color from " + + "colorless component class " + this + "]."); + return null; + } + + // return a random entry from the array + return _starters[RandomUtil.getInt(_starters.length)]; + } + + /** + * Get the default ColorRecord defined for this color class, or + * null if none. + */ + public ColorRecord getDefault () + { + return (ColorRecord) colors.get(defaultId); + } + + /** + * Returns a string representation of this instance. + */ + public String toString () + { + return "[id=" + classId + ", name=" + name + ", source=#" + + Integer.toString(source.getRGB() & 0xFFFFFF, 16) + + ", range=" + StringUtil.toString(range) + + ", starter=" + starter + ", colors=" + + StringUtil.toString(colors.values().iterator()) + "]"; + } + + protected transient ColorRecord[] _starters; + + /** Increase this value when object's serialized state is impacted + * by a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 2; + } + + /** + * Used to store information on a particular color. These are public + * to simplify the XML parsing process, so pay them no mind. + */ + public static class ColorRecord implements Serializable + { + /** The colorization class to which we belong. */ + public ClassRecord cclass; + + /** A unique colorization identifier (used in fingerprints). */ + public int colorId; + + /** The name of the target color. */ + public String name; + + /** Data indicating the offset (in HSV color space) from the + * source color to recolor to this color. */ + public float[] offsets; + + /** Tags this color as a legal starting color or not. This is a + * shameful copout, placing application-specific functionality + * into a general purpose library class. */ + public boolean starter; + + /** + * Returns a value that is the composite of our class id and color + * id which can be used to identify a colorization record. This + * value will always be a positive integer that fits into 16 bits. + */ + public int getColorPrint () + { + return ((cclass.classId << 8) | colorId); + } + + /** + * Returns the data in this record configured as a colorization + * instance. + */ + public Colorization getColorization () + { +// if (_zation == null) { +// _zation = new Colorization(getColorPrint(), cclass.source, +// cclass.range, offsets); +// } +// return _zation; + return new Colorization(getColorPrint(), cclass.source, + cclass.range, offsets); + } + + /** + * Returns a string representation of this instance. + */ + public String toString () + { + return "[id=" + colorId + ", name=" + name + + ", offsets=" + StringUtil.toString(offsets) + + ", starter=" + starter + "]"; + } + + /** Our data represented as a colorization. */ + protected transient Colorization _zation; + + /** Increase this value when object's serialized state is impacted + * by a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 2; + } + + /** + * Returns an iterator over all color classes in this pository. + */ + public Iterator enumerateClasses () + { + return _classes.values().iterator(); + } + + /** + * Returns an array containing the records for the colors in the + * specified class. + */ + public ColorRecord[] enumerateColors (String className) + { + // make sure the class exists + ClassRecord record = getClassRecord(className); + if (record == null) { + return null; + } + + // create the array + ColorRecord[] crecs = new ColorRecord[record.colors.size()]; + Iterator iter = record.colors.values().iterator(); + for (int i = 0; iter.hasNext(); i++) { + crecs[i] = ((ColorRecord)iter.next()); + } + return crecs; + } + + /** + * Returns an array containing the ids of the colors in the specified + * class. + */ + public int[] enumerateColorIds (String className) + { + // make sure the class exists + ClassRecord record = getClassRecord(className); + if (record == null) { + return null; + } + + int[] cids = new int[record.colors.size()]; + Iterator crecs = record.colors.values().iterator(); + for (int i = 0; crecs.hasNext(); i++) { + cids[i] = ((ColorRecord)crecs.next()).colorId; + } + return cids; + } + + /** + * Returns true if the specified color is legal for use at character + * creation time. false is always returned for non-existent colors or + * classes. + */ + public boolean isLegalStartColor (int classId, int colorId) + { + ColorRecord color = getColorRecord(classId, colorId); + return (color == null) ? false : color.starter; + } + + /** + * Returns a random starting color from the specified color class. + */ + public ColorRecord getRandomStartingColor (String className) + { + // make sure the class exists + ClassRecord record = getClassRecord(className); + return (record == null) ? null : record.randomStartingColor(); + } + + /** + * Looks up a colorization by id. + */ + public Colorization getColorization (int classId, int colorId) + { + ColorRecord color = getColorRecord(classId, colorId); + return (color == null) ? null : color.getColorization(); + } + + /** + * Looks up a colorization by color print. + */ + public Colorization getColorization (int colorPrint) + { + return getColorization(colorPrint >> 8, colorPrint & 0xFF); + } + + /** + * Looks up a colorization by name. + */ + public Colorization getColorization (String className, int colorId) + { + ClassRecord crec = getClassRecord(className); + if (crec != null) { + ColorRecord color = (ColorRecord)crec.colors.get(colorId); + if (color != null) { + return color.getColorization(); + } + } + return null; + } + + /** + * Loads up a colorization class by name and logs a warning if it + * doesn't exist. + */ + public ClassRecord getClassRecord (String className) + { + Iterator iter = _classes.values().iterator(); + while (iter.hasNext()) { + ClassRecord crec = (ClassRecord)iter.next(); + if (crec.name.equals(className)) { + return crec; + } + } + Log.warning("No such color class [class=" + className + "]."); + Thread.dumpStack(); + return null; + } + + /** + * Looks up the requested color record. + */ + public ColorRecord getColorRecord (int classId, int colorId) + { + ClassRecord record = (ClassRecord)_classes.get(classId); + if (record == null) { + // if they request color class zero, we assume they're just + // decoding a blank colorprint, otherwise we complain + if (classId != 0) { + Log.warning("Requested unknown color class " + + "[classId=" + classId + + ", colorId=" + colorId + "]."); + Thread.dumpStack(); + } + return null; + } + return (ColorRecord)record.colors.get(colorId); + } + + /** + * Adds a fully configured color class record to the pository. This is + * only called by the XML parsing code, so pay it no mind. + */ + public void addClass (ClassRecord record) + { + // validate the class id + if (record.classId > 255) { + Log.warning("Refusing to add class; classId > 255 " + record + "."); + } else { + _classes.put(record.classId, record); + } + } + + /** + * Loads up a serialized color pository from the supplied resource + * manager. + */ + public static ColorPository loadColorPository (ResourceManager rmgr) + { + try { + return loadColorPository(rmgr.getResource(CONFIG_PATH)); + } catch (IOException ioe) { + Log.warning("Failure loading color pository [path=" + CONFIG_PATH + + ", error=" + ioe + "]."); + return new ColorPository(); + } + } + + /** + * Loads up a serialized color pository from the supplied resource + * manager. + */ + public static ColorPository loadColorPository (InputStream source) + { + try { + return (ColorPository)CompiledConfig.loadConfig(source); + } catch (IOException ioe) { + Log.warning("Failure loading color pository: " + ioe + "."); + return new ColorPository(); + } + } + + /** + * Serializes and saves color pository to the supplied file. + */ + public static void saveColorPository (ColorPository posit, File root) + { + File path = new File(root, CONFIG_PATH); + try { + CompiledConfig.saveConfig(path, posit); + } catch (IOException ioe) { + Log.warning("Failure saving color pository " + + "[path=" + path + ", error=" + ioe + "]."); + } + } + + /** Our mapping from class names to class records. */ + protected HashIntMap _classes = new HashIntMap(); + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; + + /** + * The path (relative to the resource directory) at which the + * serialized recolorization repository should be loaded and stored. + */ + protected static final String CONFIG_PATH = "config/media/colordefs.dat"; +} diff --git a/src/java/com/threerings/media/image/ColorUtil.java b/src/java/com/threerings/media/image/ColorUtil.java new file mode 100644 index 00000000..76f58813 --- /dev/null +++ b/src/java/com/threerings/media/image/ColorUtil.java @@ -0,0 +1,57 @@ +// +// $Id: ColorUtil.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Color; + +/** + * Utilities to manipulate colors. + */ +public class ColorUtil +{ + /** + * Blends the two supplied colors. + * + * @return a color halfway between the two colors. + */ + public static final Color blend (Color c1, Color c2) + { + return new Color((c1.getRed() + c2.getRed()) >> 1, + (c1.getGreen() + c2.getGreen()) >> 1, + (c1.getBlue() + c2.getBlue()) >> 1); + } + + /** + * Blends the two supplied colors, using the supplied percentage + * as the amount of the first color to use. + * + * @param firstperc The percentage of the first color to use, from 0.0f + * to 1.0f inclusive. + */ + public static final Color blend (Color c1, Color c2, float firstperc) + { + float p2 = 1.0f - firstperc; + return new Color((int) (c1.getRed() * firstperc + c2.getRed() * p2), + (int) (c1.getGreen() * firstperc + c2.getGreen() * p2), + (int) (c1.getBlue() * firstperc + c2.getBlue() * p2)); + } +} diff --git a/src/java/com/threerings/media/image/Colorization.java b/src/java/com/threerings/media/image/Colorization.java new file mode 100644 index 00000000..afc28072 --- /dev/null +++ b/src/java/com/threerings/media/image/Colorization.java @@ -0,0 +1,194 @@ +// +// $Id: Colorization.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Color; + +import com.samskivert.util.StringUtil; + +/** + * Used to support recoloring images. + */ +public class Colorization +{ + /** Every colorization must have a unique id that can be used to + * compare a particular colorization record with another. */ + public int colorizationId; + + /** The root color for the colorization. */ + public Color rootColor; + + /** The range around the root color that will be colorized (in + * delta-hue, delta-saturation, delta-value. */ + public float[] range; + + /** The adjustments to make to hue, saturation and value. */ + public float[] offsets; + + /** + * Constructs a colorization record with the specified identifier. + */ + public Colorization (int colorizationId, Color rootColor, + float[] range, float[] offsets) + { + this.colorizationId = colorizationId; + this.rootColor = rootColor; + this.range = range; + this.offsets = offsets; + + // compute our HSV and fixed HSV + _hsv = Color.RGBtoHSB(rootColor.getRed(), rootColor.getGreen(), + rootColor.getBlue(), null); + _fhsv = toFixedHSV(_hsv, null); + } + + /** + * Returns the root color adjusted by the colorization. + */ + public Color getColorizedRoot () + { + return new Color(recolorColor(_hsv)); + } + + /** + * Adjusts the supplied color by the offests in this colorization, + * taking the appropriate measures for hue (wrapping it around) and + * saturation and value (clipping). + * + * @return the RGB value of the recolored color. + */ + public int recolorColor (float[] hsv) + { + // for hue, we wrap around + float hue = hsv[0] + offsets[0]; + if (hue > 1.0) { + hue -= 1.0; + } + + // otherwise we clip + float sat = Math.min(Math.max(hsv[1] + offsets[1], 0), 1); + float val = Math.min(Math.max(hsv[2] + offsets[2], 0), 1); + + // convert back to RGB space + return Color.HSBtoRGB(hue, sat, val); + } + + /** + * Returns true if this colorization matches the supplied color, false + * otherwise. + * + * @param hsv the HSV values for the color in question. + * @param fhsv the HSV values converted to fixed point via {@link + * #toFixedHSV} for the color in question. + */ + public boolean matches (float[] hsv, int[] fhsv) + { + // check to see that this color is sufficiently "close" to the + // root color based on the supplied distance parameters + if (distance(fhsv[0], _fhsv[0], Short.MAX_VALUE) >= + range[0] * Short.MAX_VALUE) { + return false; + } + + // saturation and value don't wrap around like hue + if (Math.abs(_hsv[1] - hsv[1]) >= range[1] || + Math.abs(_hsv[2] - hsv[2]) >= range[2]) { + return false; + } + + return true; + } + + // documentation inherited + public int hashCode () + { + return colorizationId ^ rootColor.hashCode(); + } + + /** + * Compares this colorization to another based on id. + */ + public boolean equals (Object other) + { + if (other instanceof Colorization) { + return ((Colorization)other).colorizationId == colorizationId; + } else { + return false; + } + } + + /** + * Returns a string representation of this colorization. + */ + public String toString () + { + return String.valueOf(colorizationId); + } + + /** + * Returns a long string representation of this colorization. + */ + public String toVerboseString () + { + return StringUtil.fieldsToString(this); + } + + /** + * Converts floating point HSV values to a fixed point integer + * representation. + * + * @param hsv the HSV values to be converted. + * @param fhsv the destination array into which the fixed values will + * be stored. If this is null, a new array will be created of the + * appropriate length. + * + * @return the fhsv parameter if it was non-null or the + * newly created target array. + */ + public static int[] toFixedHSV (float[] hsv, int[] fhsv) + { + if (fhsv == null) { + fhsv = new int[hsv.length]; + } + for (int i = 0; i < hsv.length; i++) { + // fhsv[i] = (int)(hsv[i]*Integer.MAX_VALUE); + fhsv[i] = (int)(hsv[i]*Short.MAX_VALUE); + } + return fhsv; + } + + /** + * Returns the distance between the supplied to numbers modulo N. + */ + public static int distance (int a, int b, int N) + { + return (a > b) ? Math.min(a-b, b+N-a) : Math.min(b-a, a+N-b); + } + + /** Fixed HSV values for our root color; used when calculating + * recolorizations using this colorization. */ + protected int[] _fhsv; + + /** HSV values for our root color; used when calculating + * recolorizations using this colorization. */ + protected float[] _hsv; +} diff --git a/src/java/com/threerings/media/image/CompositeMirage.java b/src/java/com/threerings/media/image/CompositeMirage.java new file mode 100644 index 00000000..e1a18a16 --- /dev/null +++ b/src/java/com/threerings/media/image/CompositeMirage.java @@ -0,0 +1,96 @@ +package com.threerings.media.image; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +public class CompositeMirage implements Mirage +{ + public CompositeMirage (Mirage[] mirages) + { + _mirages = mirages; + } + + public CompositeMirage (Mirage mirage1, Mirage mirage2) + { + _mirages = new Mirage[]{mirage1, mirage2}; + } + + // documentation inherited from interface Mirage + public long getEstimatedMemoryUsage () + { + // Return the total memory of our component mirages. + long mem = 0; + for (Mirage m : _mirages) { + mem += m.getEstimatedMemoryUsage(); + } + + return mem; + } + + // documentation inherited from interface Mirage + public int getHeight () + { + // Return the maximal height of our component mirages. + int height = 0; + for (Mirage m : _mirages) { + height = Math.max(height, m.getHeight()); + } + + return height; + } + + // documentation inherited from interface Mirage + public int getWidth () + { + // Return the maximal width of our component mirages. + int width = 0; + for (Mirage m : _mirages) { + width = Math.max(width, m.getWidth()); + } + + return width; + } + + // documentation inheritd from interface Mirage + public BufferedImage getSnapshot () + { + BufferedImage img = new BufferedImage(getWidth(), getHeight(), + BufferedImage.TYPE_INT_ARGB); + Graphics2D gfx = img.createGraphics(); + + try { + for (Mirage m : _mirages) { + m.paint(gfx, 0, 0); + } + } finally { + gfx.dispose(); + } + + return img; + } + + // documentation inheritd from interface Mirage + public boolean hitTest (int x, int y) + { + // If it hits any of our mirages, it hits us. + for (Mirage m : _mirages) { + if (m.hitTest(x, y)) { + return true; + } + } + + return false; + } + + // documentation inheritd from interface Mirage + public void paint (Graphics2D gfx, int x, int y) + { + // Paint everyone. + for (Mirage m : _mirages) { + m.paint(gfx, x, y); + } + } + + /** All the component mirages we're made up of. */ + protected Mirage[] _mirages; +} diff --git a/src/java/com/threerings/media/image/FastImageIO.java b/src/java/com/threerings/media/image/FastImageIO.java new file mode 100644 index 00000000..fba2020b --- /dev/null +++ b/src/java/com/threerings/media/image/FastImageIO.java @@ -0,0 +1,174 @@ +// +// $Id: FastImageIO.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Point; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.IndexColorModel; +import java.awt.image.PixelInterleavedSampleModel; +import java.awt.image.WritableRaster; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +import java.nio.IntBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +/** + * Provides routines for writing and reading uncompressed 8-bit color + * mapped images in a manner that is extremely fast and generates a + * minimal amount of garbage during the loading process. + */ +public class FastImageIO +{ + /** A suffix for use when storing raw images in bundles or on the file + * system. */ + public static final String FILE_SUFFIX = ".raw"; + + /** + * Returns true if the supplied image is of a format that is supported + * by the fast image I/O services, false if not. + */ + public static boolean canWrite (BufferedImage image) + { + return (image.getColorModel() instanceof IndexColorModel) && + (image.getRaster().getDataBuffer() instanceof DataBufferByte); + } + + /** + * Writes the supplied image to the supplied output stream. + * + * @exception IOException thrown if an error occurs writing to the + * output stream. + */ + public static void write (BufferedImage image, OutputStream out) + throws IOException + { + DataOutputStream dout = new DataOutputStream(out); + + // write the image dimensions + int width = image.getWidth(), height = image.getHeight(); + dout.writeInt(width); + dout.writeInt(height); + + // write the color model information + IndexColorModel cmodel = (IndexColorModel)image.getColorModel(); + int tpixel = cmodel.getTransparentPixel(); + dout.writeInt(tpixel); + int msize = cmodel.getMapSize(); + int[] map = new int[msize]; + cmodel.getRGBs(map); + dout.writeInt(msize); + for (int ii = 0; ii < map.length; ii++) { + dout.writeInt(map[ii]); + } + + // write the raster data + DataBufferByte dbuf = (DataBufferByte)image.getRaster().getDataBuffer(); + byte[] data = dbuf.getData(); + if (data.length != width * height) { + String errmsg = "Raster data not same size as image! [" + + width + "x" + height + " != " + data.length + "]"; + throw new IllegalStateException(errmsg); + } + dout.write(data); + dout.flush(); + } + + /** + * Reads an image from the supplied file (which must contain an image + * previously written via a call to {@link #write}). + * + * @exception IOException thrown if an error occurs reading from the + * file. + */ + public static BufferedImage read (File file) + throws IOException + { + RandomAccessFile raf = new RandomAccessFile(file, "r"); + FileChannel fchan = raf.getChannel(); + + try { + MappedByteBuffer mbuf = fchan.map( + FileChannel.MapMode.READ_ONLY, 0, file.length()); + + // read in our integer fields + IntBuffer ibuf = mbuf.asIntBuffer(); + int width = ibuf.get(); + int height = ibuf.get(); + int tpixel = ibuf.get(); + int msize = ibuf.get(); + + if (width > Short.MAX_VALUE || width < 0 || + height > Short.MAX_VALUE || height < 0) { + throw new IOException("Bogus image size " + + width + "x" + height); + } + + IndexColorModel cmodel; + synchronized (_origin) { // any old object will do + // make sure our colormap array is big enough + if (_cmap == null || _cmap.length < msize) { + _cmap = new int[msize]; + } + // read in the data and create our colormap + ibuf.get(_cmap, 0, msize); + cmodel = new IndexColorModel( + 8, msize, _cmap, 0, DataBuffer.TYPE_BYTE, null); + } + + // advance the byte buffer accordingly + mbuf.position(ibuf.position() * 4); + + // read in the image data itself + byte[] data = new byte[width*height]; + mbuf.get(data); + + // create the image from our component parts + DataBuffer dbuf = new DataBufferByte(data, data.length, 0); + int[] offsets = new int[] { 0 }; + PixelInterleavedSampleModel smodel = + new PixelInterleavedSampleModel( + DataBuffer.TYPE_BYTE, width, height, 1, width, offsets); + WritableRaster raster = WritableRaster.createWritableRaster( + smodel, dbuf, _origin); + return new BufferedImage(cmodel, raster, false, null); + + } finally { + fchan.close(); + raf.close(); + } + } + + /** Used when loading our color map. */ + protected static int[] _cmap; + + /** Used when creating our writable raster. */ + protected static Point _origin = new Point(0, 0); +} diff --git a/src/java/com/threerings/media/image/ImageDataProvider.java b/src/java/com/threerings/media/image/ImageDataProvider.java new file mode 100644 index 00000000..78abbc6d --- /dev/null +++ b/src/java/com/threerings/media/image/ImageDataProvider.java @@ -0,0 +1,46 @@ +// +// $Id: ImageDataProvider.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.io.IOException; +import java.awt.image.BufferedImage; + +/** + * Provides access to image data for the image with the specified + * path. Images loaded from different data providers (which are + * differentiated by reference equality) will be considered distinct + * images with respect to caching. + */ +public interface ImageDataProvider +{ + /** + * Returns a string identifier for this image data provider which wil + * be used to differentiate it from other providers and thus should be + * unique. + */ + public String getIdent (); + + /** + * Returns the image at the specified path. + */ + public BufferedImage loadImage (String path) throws IOException; +} diff --git a/src/java/com/threerings/media/image/ImageManager.java b/src/java/com/threerings/media/image/ImageManager.java new file mode 100644 index 00000000..d0f71250 --- /dev/null +++ b/src/java/com/threerings/media/image/ImageManager.java @@ -0,0 +1,615 @@ +// +// $Id: ImageManager.java 4007 2006-04-10 08:59:30Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Component; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Transparency; +import java.awt.image.BufferedImage; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; + +import com.samskivert.swing.RuntimeAdjust; +import com.samskivert.util.LRUHashMap; +import com.samskivert.util.StringUtil; +import com.samskivert.util.Throttle; +import com.samskivert.util.Tuple; + +import com.threerings.media.Log; +import com.threerings.media.MediaPrefs; +import com.threerings.resource.ResourceManager; + +/** + * Provides a single point of access for image retrieval and caching. + */ +public class ImageManager + implements ImageUtil.ImageCreator +{ + /** + * Used to identify an image for caching and reconstruction. + */ + public static class ImageKey + { + /** The data provider from which this image's data is loaded. */ + public ImageDataProvider daprov; + + /** The path used to identify the image to the data provider. */ + public String path; + + protected ImageKey (ImageDataProvider daprov, String path) + { + this.daprov = daprov; + this.path = path; + } + + public int hashCode () + { + return path.hashCode() ^ daprov.getIdent().hashCode(); + } + + public boolean equals (Object other) + { + if (other == null || !(other instanceof ImageKey)) { + return false; + } + + ImageKey okey = (ImageKey)other; + return ((okey.daprov.getIdent().equals(daprov.getIdent())) && + (okey.path.equals(path))); + } + + public String toString () + { + return daprov.getIdent() + ":" + path; + } + } + + /** + * This interface allows the image manager to create images that are in a + * format optimal for rendering to the screen. + */ + public interface OptimalImageCreator + { + /** + * Requests that a blank image be created that is in a format and of a + * depth that are optimal for rendering to the screen. + */ + public BufferedImage createImage (int width, int height, int trans); + } + + /** + * Construct an image manager with the specified {@link ResourceManager} + * from which it will obtain its data. + */ + public ImageManager (ResourceManager rmgr, OptimalImageCreator icreator) + { + _rmgr = rmgr; + _icreator = icreator; + + // create our image cache + int icsize = _cacheSize.getValue(); + Log.debug("Creating image cache [size=" + icsize + "k]."); + _ccache = new LRUHashMap(icsize * 1024, new LRUHashMap.ItemSizer() { + public int computeSize (Object value) { + return (int)((CacheRecord)value).getEstimatedMemoryUsage(); + } + }); + _ccache.setTracking(true); + } + + /** + * A convenience constructor that creates an {@link AWTImageCreator} for + * use by the image manager. + */ + public ImageManager (ResourceManager rmgr, Component context) + { + this(rmgr, new AWTImageCreator(context)); + } + + /** + * Creates a buffered image, optimized for display on our graphics + * device. + */ + public BufferedImage createImage (int width, int height, int transparency) + { + return _icreator.createImage(width, height, transparency); + } + + /** + * Loads (and caches) the specified image from the resource manager + * using the supplied path to identify the image. + */ + public BufferedImage getImage (String path) + { + return getImage(null, path, null); + } + + /** + * Like {@link #getImage(String)} but the specified colorizations are + * applied to the image before it is returned. + */ + public BufferedImage getImage (String path, Colorization[] zations) + { + return getImage(null, path, zations); + } + + /** + * Like {@link #getImage(String)} but the image is loaded from the + * specified resource set rathern than the default resource set. + */ + public BufferedImage getImage (String rset, String path) + { + return getImage(rset, path, null); + } + + /** + * Like {@link #getImage(String,String)} but the specified + * colorizations are applied to the image before it is returned. + */ + public BufferedImage getImage (String rset, String path, + Colorization[] zations) + { + if (StringUtil.isBlank(path)) { + String errmsg = "Invalid image path [rset=" + rset + + ", path=" + path + "]"; + throw new IllegalArgumentException(errmsg); + } + + return getImage(getImageKey(rset, path), zations); + } + + /** + * Loads (and caches) the specified image from the resource manager + * using the supplied path to identify the image. + * + *

Additionally the image is optimized for display in the current + * graphics configuration. Consider using {@link #getMirage(ImageKey)} + * instead of prepared images as they (some day) will automatically + * use volatile images to increase performance. + */ + public BufferedImage getPreparedImage (String path) + { + return getPreparedImage(null, path, null); + } + + /** + * Loads (and caches) the specified image from the resource manager, + * obtaining the image from the supplied resource set. + * + *

Additionally the image is optimized for display in the current + * graphics configuration. Consider using {@link #getMirage(ImageKey)} + * instead of prepared images as they (some day) will automatically + * use volatile images to increase performance. + */ + public BufferedImage getPreparedImage (String rset, String path) + { + return getPreparedImage(rset, path, null); + } + + /** + * Loads (and caches) the specified image from the resource manager, + * obtaining the image from the supplied resource set and applying the + * using the supplied path to identify the image. + * + *

Additionally the image is optimized for display in the current + * graphics configuration. Consider using {@link + * #getMirage(ImageKey,Colorization[])} instead of prepared images as + * they (some day) will automatically use volatile images to increase + * performance. + */ + public BufferedImage getPreparedImage ( + String rset, String path, Colorization[] zations) + { + BufferedImage image = getImage(rset, path, zations); + BufferedImage prepped = null; + if (image != null) { + prepped = createImage(image.getWidth(), image.getHeight(), + image.getColorModel().getTransparency()); + Graphics2D pg = prepped.createGraphics(); + pg.drawImage(image, 0, 0, null); + pg.dispose(); + } + return prepped; + } + + /** + * Returns an image key that can be used to fetch the image identified + * by the specified resource set and image path. + */ + public ImageKey getImageKey (String rset, String path) + { + return getImageKey(getDataProvider(rset), path); + } + + /** + * Returns an image key that can be used to fetch the image identified + * by the specified data provider and image path. + */ + public ImageKey getImageKey (ImageDataProvider daprov, String path) + { + return new ImageKey(daprov, path); + } + + /** + * Obtains the image identified by the specified key, caching if + * possible. The image will be recolored using the supplied + * colorizations if requested. + */ + public BufferedImage getImage (ImageKey key, Colorization[] zations) + { + CacheRecord crec = null; + synchronized (_ccache) { + crec = (CacheRecord)_ccache.get(key); + } + if (crec != null) { +// Log.info("Cache hit [key=" + key + ", crec=" + crec + "]."); + return crec.getImage(zations); + } +// Log.info("Cache miss [key=" + key + ", crec=" + crec + "]."); + + // load up the raw image + BufferedImage image = loadImage(key); + if (image == null) { + Log.warning("Failed to load image " + key + "."); + // create a blank image instead + image = new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_INDEXED); + } + +// Log.info("Loaded " + key.path + ", image=" + image + +// ", size=" + ImageUtil.getEstimatedMemoryUsage(image)); + + // create a cache record + crec = new CacheRecord(key, image); + synchronized (_ccache) { + _ccache.put(key, crec); + } + _keySet.add(key); + + // periodically report our image cache performance + reportCachePerformance(); + + return crec.getImage(zations); + } + + /** + * Returns the data provider configured to obtain image data from the + * specified resource set. + */ + protected ImageDataProvider getDataProvider (final String rset) + { + if (rset == null) { + return _defaultProvider; + } + + ImageDataProvider dprov = (ImageDataProvider)_providers.get(rset); + if (dprov == null) { + dprov = new ImageDataProvider() { + public BufferedImage loadImage (String path) + throws IOException { + // first attempt to load the image from the specified + // resource set + try { + return read(_rmgr.getImageResource(rset, path)); + } catch (FileNotFoundException fnfe) { + // fall back to trying the classpath + return read(_rmgr.getImageResource(path)); + } + } + + public String getIdent () { + return "rmgr:" + rset; + } + }; + _providers.put(rset, dprov); + } + + return dprov; + } + + /** + * Loads and returns the image with the specified key from the + * supplied data provider. + */ + protected BufferedImage loadImage (ImageKey key) + { +// if (EventQueue.isDispatchThread()) { +// Log.info("Loading image on AWT thread " + key + "."); +// } + + BufferedImage image = null; + try { + Log.debug("Loading image " + key + "."); + image = key.daprov.loadImage(key.path); + if (image == null) { + Log.warning("ImageIO.read(" + key + ") returned null."); + } + + } catch (Exception e) { + Log.warning("Unable to load image '" + key + "'."); + Log.logStackTrace(e); + + // create a blank image in its stead + image = createImage(1, 1, Transparency.OPAQUE); + } + + return image; + } + + /** + * Helper function for loading images. + */ + protected static BufferedImage read (ImageInputStream iis) + throws IOException + { + BufferedImage image = ImageIO.read(iis); + closeIIS(iis); + return image; + } + + /** + * Helper function for closing image input streams. + */ + protected static void closeIIS (ImageInputStream iis) + { + if (iis == null) { + return; + } + + try { + iis.close(); + } catch (IOException ioe) { + // jesus fucking hoppalong cassidy christ on a polyester pogo + // stick! ImageInputStreamImpl.close() throws a fucking + // IOException if it's already closed; there's no way to find + // out if it's already closed or not, so we have to check for + // their bullshit exception; as if we should just "trust" + // ImageIO.read() to close the fucking input stream when it's + // done, especially after the goddamned fiasco with + // PNGImageReader not closing it's fucking inflaters; for the + // love of humanity + if (!"closed".equals(ioe.getMessage())) { + Log.warning("Failure closing image input '" + iis + "'."); + Log.logStackTrace(ioe); + } + } + } + + /** + * Creates a mirage which is an image optimized for display on our + * current display device and which will be stored into video memory + * if possible. + */ + public Mirage getMirage (ImageKey key) + { + return getMirage(key, null, null); + } + + /** + * Like {@link #getMirage(ImageKey)} but that only the specified + * subimage of the source image is used to build the mirage. + */ + public Mirage getMirage (ImageKey key, Rectangle bounds) + { + return getMirage(key, bounds, null); + } + + /** + * Like {@link #getMirage(ImageKey)} but the supplied colorizations + * are applied to the source image before creating the mirage. + */ + public Mirage getMirage (ImageKey key, Colorization[] zations) + { + return getMirage(key, null, zations); + } + + /** + * Like {@link #getMirage(ImageKey,Colorization[])} except that the + * mirage is created using only the specified subset of the original + * image. + */ + public Mirage getMirage (ImageKey key, Rectangle bounds, + Colorization[] zations) + { + BufferedImage src = null; + + if (bounds == null) { + // if they specified no bounds, we need to load up the raw + // image and determine its bounds so that we can pass those + // along to the created mirage + src = getImage(key, zations); + bounds = new Rectangle(0, 0, src.getWidth(), src.getHeight()); + + } else if (!_prepareImages.getValue()) { + src = getImage(key, zations); + src = src.getSubimage(bounds.x, bounds.y, + bounds.width, bounds.height); + } + + if (_runBlank.getValue()) { + return new BlankMirage(bounds.width, bounds.height); + } else if (_prepareImages.getValue()) { + return new CachedVolatileMirage(this, key, bounds, zations); + } else { + return new BufferedMirage(src); + } + } + + /** + * Returns the image creator that can be used to create buffered images + * optimized for rendering to the screen. + */ + public OptimalImageCreator getImageCreator () + { + return _icreator; + } + + /** + * Reports statistics detailing the image manager cache performance + * and the current size of the cached images. + */ + protected void reportCachePerformance () + { + if (/* Log.getLevel() != Log.log.DEBUG || */ + _cacheStatThrottle.throttleOp()) { + return; + } + + // compute our estimated memory usage + long size = 0; + + int[] eff = null; + synchronized (_ccache) { + Iterator iter = _ccache.values().iterator(); + while (iter.hasNext()) { + size += ((CacheRecord)iter.next()).getEstimatedMemoryUsage(); + } + eff = _ccache.getTrackedEffectiveness(); + } + Log.info("ImageManager LRU [mem=" + (size / 1024) + "k" + + ", size=" + _ccache.size() + ", hits=" + eff[0] + + ", misses=" + eff[1] + ", totalKeys=" + _keySet.size() + "]."); + } + + /** Maintains a source image and a set of colorized versions in the + * image cache. */ + protected static class CacheRecord + { + public CacheRecord (ImageKey key, BufferedImage source) + { + _key = key; + _source = source; + } + + public BufferedImage getImage (Colorization[] zations) + { + if (zations == null) { + return _source; + } + + if (_colorized == null) { + _colorized = new ArrayList(); + } + + // we search linearly through our list of colorized copies + // because it is not likely to be very long + int csize = _colorized.size(); + for (int ii = 0; ii < csize; ii++) { + Tuple tup = (Tuple)_colorized.get(ii); + Colorization[] tzations = (Colorization[])tup.left; + if (Arrays.equals(zations, tzations)) { + return (BufferedImage)tup.right; + } + } + + try { + BufferedImage cimage = ImageUtil.recolorImage(_source, zations); + _colorized.add(new Tuple(zations, cimage)); + return cimage; + + } catch (Exception re) { + Log.warning("Failure recoloring image [source" + _key + + ", zations=" + StringUtil.toString(zations) + + ", error=" + re + "]."); + // return the uncolorized version + return _source; + } + } + + public long getEstimatedMemoryUsage () + { + return ImageUtil.getEstimatedMemoryUsage(_source); + } + + public String toString () + { + return "[key=" + _key + ", wid=" + _source.getWidth() + + ", hei=" + _source.getHeight() + + ", ccount=" + ((_colorized == null) ? 0 : _colorized.size()) + + "]"; + } + + protected ImageKey _key; + protected BufferedImage _source; + protected ArrayList _colorized; + } + + /** A reference to the resource manager via which we load image data + * by default. */ + protected ResourceManager _rmgr; + + /** We use this to create images optimized for rendering. */ + protected OptimalImageCreator _icreator; + + /** A cache of loaded images. */ + protected LRUHashMap _ccache; + + /** The set of all keys we've ever seen. */ + protected HashSet _keySet = new HashSet(); + + /** Throttle our cache status logging to once every 300 seconds. */ + protected Throttle _cacheStatThrottle = new Throttle(1, 300000L); + + /** Our default data provider. */ + protected ImageDataProvider _defaultProvider = new ImageDataProvider() { + public BufferedImage loadImage (String path) throws IOException { + return read(_rmgr.getImageResource(path)); + } + + public String getIdent () { + return "rmgr:default"; + } + }; + + /** Data providers for different resource sets. */ + protected HashMap _providers = new HashMap(); + + /** Register our image cache size with the runtime adjustments + * framework. */ + protected static RuntimeAdjust.IntAdjust _cacheSize = + new RuntimeAdjust.IntAdjust( + "Size (in kb of memory used) of the image manager LRU cache " + + "[requires restart]", "narya.media.image.cache_size", + MediaPrefs.config, 2048); + + /** Controls whether or not we prepare images or use raw versions. */ + protected static RuntimeAdjust.BooleanAdjust _prepareImages = + new RuntimeAdjust.BooleanAdjust( + "Cause image manager to optimize all images for display.", + "narya.media.image.prep_images", MediaPrefs.config, true); + + /** A debug toggle for running entirely without rendering images. */ + protected static RuntimeAdjust.BooleanAdjust _runBlank = + new RuntimeAdjust.BooleanAdjust( + "Cause image manager to return blank images.", + "narya.media.image.run_blank", MediaPrefs.config, false); +} diff --git a/src/java/com/threerings/media/image/ImageUtil.java b/src/java/com/threerings/media/image/ImageUtil.java new file mode 100644 index 00000000..fb6172e2 --- /dev/null +++ b/src/java/com/threerings/media/image/ImageUtil.java @@ -0,0 +1,647 @@ +// +// $Id: ImageUtil.java 3965 2006-03-21 02:32:27Z mthomas $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.HeadlessException; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.Transparency; + +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.IndexColorModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import java.util.Arrays; +import java.util.Iterator; + +import com.samskivert.swing.Label; + +/** + * Image related utility functions. + */ +public class ImageUtil +{ + public static interface ImageCreator + { + /** Used by routines that need to create new images to allow the + * caller to dictate the format (which may mean using + * createCompatibleImage). */ + public BufferedImage createImage ( + int width, int height, int transparency); + } + + /** + * Creates a new buffered image with the same sample model and color + * model as the source image but with the new width and height. + */ + public static BufferedImage createCompatibleImage ( + BufferedImage source, int width, int height) + { + WritableRaster raster = + source.getRaster().createCompatibleWritableRaster(width, height); + return new BufferedImage(source.getColorModel(), raster, false, null); + } + + /** + * Creates an image with the word "Error" written in it. + */ + public static BufferedImage createErrorImage (int width, int height) + { + BufferedImage img = new BufferedImage( + width, height, BufferedImage.TYPE_BYTE_INDEXED); + Graphics2D g = (Graphics2D)img.getGraphics(); + g.setColor(Color.red); + Label l = new Label("Error"); + l.layout(g); + Dimension d = l.getSize(); + // fill that sucker with errors + for (int yy = 0; yy < height; yy += d.height) { + for (int xx = 0; xx < width; xx += (d.width+5)) { + l.render(g, xx, yy); + } + } + g.dispose(); + return img; + } + + /** + * Used to recolor images by shifting bands of color (in HSV color + * space) to a new hue. The source images must be 8-bit color mapped + * images, as the recoloring process works by analysing the color map + * and modifying it. + */ + public static BufferedImage recolorImage ( + BufferedImage image, Color rootColor, float[] dists, float[] offsets) + { + return recolorImage(image, new Colorization[] { + new Colorization(-1, rootColor, dists, offsets) }); + } + + /** + * Recolors the supplied image as in {@link + * #recolorImage(BufferedImage,Color,float[],float[])} obtaining the + * recoloring parameters from the supplied {@link Colorization} + * instance. + */ + public static BufferedImage recolorImage ( + BufferedImage image, Colorization cz) + { + return recolorImage(image, new Colorization[] { cz }); + } + + /** + * Recolors the supplied image using the supplied colorizations. + */ + public static BufferedImage recolorImage ( + BufferedImage image, Colorization[] zations) + { + ColorModel cm = image.getColorModel(); + if (!(cm instanceof IndexColorModel)) { + String errmsg = "Unable to recolor images with non-index color " + + "model [cm=" + cm.getClass() + "]"; + throw new RuntimeException(errmsg); + } + + // now process the image + IndexColorModel icm = (IndexColorModel)cm; + int size = icm.getMapSize(); + int zcount = zations.length; + int[] rgbs = new int[size]; + + // fetch the color data + icm.getRGBs(rgbs); + + // convert the colors to HSV + float[] hsv = new float[3]; + int[] fhsv = new int[3]; + int tpixel = -1; + for (int i = 0; i < size; i++) { + int value = rgbs[i]; + + // don't fiddle with alpha pixels + if ((value & 0xFF000000) == 0) { + tpixel = i; + continue; + } + + // convert the color to HSV + int red = (value >> 16) & 0xFF; + int green = (value >> 8) & 0xFF; + int blue = (value >> 0) & 0xFF; + Color.RGBtoHSB(red, green, blue, hsv); + Colorization.toFixedHSV(hsv, fhsv); + + // see if this color matches and of our colorizations and + // recolor it if it does + for (int z = 0; z < zcount; z++) { + Colorization cz = zations[z]; + if (cz != null && cz.matches(hsv, fhsv)) { + // massage the HSV bands and update the RGBs array + rgbs[i] = cz.recolorColor(hsv); + break; + } + } + } + + // create a new image with the adjusted color palette + IndexColorModel nicm = new IndexColorModel( + icm.getPixelSize(), size, rgbs, 0, icm.hasAlpha(), + icm.getTransparentPixel(), icm.getTransferType()); + return new BufferedImage(nicm, image.getRaster(), false, null); + } + + /** + * Paints multiple copies of the supplied image using the supplied + * graphics context such that the requested area is filled with the + * image. + */ + public static void tileImage ( + Graphics2D gfx, Mirage image, int x, int y, int width, int height) + { + int iwidth = image.getWidth(), iheight = image.getHeight(); + int xnum = width / iwidth, xplus = width % iwidth; + int ynum = height / iheight, yplus = height % iheight; + Shape oclip = gfx.getClip(); + + for (int ii=0; ii < ynum; ii++) { + // draw the full copies of the image across + int xx = x; + for (int jj=0; jj < xnum; jj++) { + image.paint(gfx, xx, y); + xx += iwidth; + } + + if (xplus > 0) { + gfx.clipRect(xx, y, xplus, iheight); + image.paint(gfx, xx, y); + gfx.setClip(oclip); + } + + y += iheight; + } + + if (yplus > 0) { + int xx = x; + for (int jj=0; jj < xnum; jj++) { + gfx.clipRect(xx, y, iwidth, yplus); + image.paint(gfx, xx, y); + gfx.setClip(oclip); + xx += iwidth; + } + + if (xplus > 0) { + gfx.clipRect(xx, y, xplus, yplus); + image.paint(gfx, xx, y); + gfx.setClip(oclip); + } + } + } + + /** + * Paints multiple copies of the supplied image using the supplied + * graphics context such that the requested width is filled with the + * image. + */ + public static void tileImageAcross (Graphics2D gfx, Mirage image, + int x, int y, int width) + { + tileImage(gfx, image, x, y, width, image.getHeight()); + } + + /** + * Paints multiple copies of the supplied image using the supplied + * graphics context such that the requested height is filled with the + * image. + */ + public static void tileImageDown (Graphics2D gfx, Mirage image, + int x, int y, int height) + { + tileImage(gfx, image, x, y, image.getWidth(), height); + } + + // Not fully added because we're not using it anywhere, plus + // it's probably a little sketchy to create Area objects with all + // this pixely data. + // Also, the Area was getting zeroed out when it was translated. Something + // to look into someday if anyone wants to use this method. +// /** +// * Creates a mask that is opaque in the non-transparent areas of the +// * source image. +// */ +// public static Area createImageMask (BufferedImage src) +// { +// Raster srcdata = src.getData(); +// int wid = src.getWidth(), hei = src.getHeight(); +// Log.info("creating area of (" + wid + ", " + hei + ")"); +// Area a = new Area(new Rectangle(wid, hei)); +// Rectangle r = new Rectangle(1, 1); +// +// for (int yy=0; yy < hei; yy++) { +// for (int xx=0; xx < wid; xx++) { +// if (srcdata.getSample(xx, yy, 0) == 0) { +// r.setLocation(xx, yy); +// a.subtract(new Area(r)); +// } +// } +// } +// +// return a; +// } + + /** + * Creates and returns a new image consisting of the supplied image + * traced with the given color and thickness. + */ + public static BufferedImage createTracedImage ( + ImageCreator isrc, BufferedImage src, Color tcolor, int thickness) + { + return createTracedImage(isrc, src, tcolor, thickness, 1.0f, 1.0f); + } + + /** + * Creates and returns a new image consisting of the supplied image + * traced with the given color, thickness and alpha transparency. + */ + public static BufferedImage createTracedImage ( + ImageCreator isrc, BufferedImage src, Color tcolor, int thickness, + float startAlpha, float endAlpha) + { + // create the destination image + int wid = src.getWidth(), hei = src.getHeight(); + BufferedImage dest = isrc.createImage( + wid, hei, Transparency.TRANSLUCENT); + return createTracedImage( + src, dest, tcolor, thickness, startAlpha, endAlpha); + } + + /** + * Creates and returns a new image consisting of the supplied image + * traced with the given color, thickness and alpha transparency. + */ + public static BufferedImage createTracedImage ( + BufferedImage src, BufferedImage dest, Color tcolor, int thickness, + float startAlpha, float endAlpha) + { + // prepare various bits of working data + int wid = src.getWidth(), hei = src.getHeight(); + int spixel = (tcolor.getRGB() & RGB_MASK); + int salpha = (int)(startAlpha * 255); + int tpixel = (spixel | (salpha << 24)); + boolean[] traced = new boolean[wid * hei]; + int stepAlpha = (thickness <= 1) ? 0 : + (int)(((startAlpha - endAlpha) * 255) / (thickness - 1)); + + // TODO: this could be made more efficient, e.g., if we made four + // passes through the image in a vertical scan, horizontal scan, + // and opposing diagonal scans, making sure each non-transparent + // pixel found during each scan is traced on both sides of the + // respective scan direction. for now, we just naively check all + // eight pixels surrounding each pixel in the image and fill the + // center pixel with the tracing color if it's transparent but has + // a non-transparent pixel around it. + for (int tt = 0; tt < thickness; tt++) { + if (tt > 0) { + // clear out the array of pixels traced this go-around + Arrays.fill(traced, false); + // use the destination image as our new source + src = dest; + // decrement the trace pixel alpha-level + salpha -= Math.max(0, stepAlpha); + tpixel = (spixel | (salpha << 24)); + } + + for (int yy = 0; yy < hei; yy++) { + for (int xx = 0; xx < wid; xx++) { + // get the pixel we're checking + int argb = src.getRGB(xx, yy); + + if ((argb & TRANS_MASK) != 0) { + // copy any pixel that isn't transparent + dest.setRGB(xx, yy, argb); + + } else if (bordersNonTransparentPixel( + src, wid, hei, traced, xx, yy)) { + dest.setRGB(xx, yy, tpixel); + // note that we traced this pixel this pass so + // that it doesn't impact other-pixel borderedness + traced[(yy*wid)+xx] = true; + } + } + } + } + + return dest; + } + + /** + * Returns whether the given pixel is bordered by any non-transparent + * pixel. + */ + protected static boolean bordersNonTransparentPixel ( + BufferedImage data, int wid, int hei, boolean[] traced, int x, int y) + { + // check the three-pixel row above the pixel + if (y > 0) { + for (int rxx = x - 1; rxx <= x + 1; rxx++) { + if (rxx < 0 || rxx >= wid || traced[((y-1)*wid)+rxx]) { + continue; + } + + if ((data.getRGB(rxx, y - 1) & TRANS_MASK) != 0) { + return true; + } + } + } + + // check the pixel to the left + if (x > 0 && !traced[(y*wid)+(x-1)]) { + if ((data.getRGB(x - 1, y) & TRANS_MASK) != 0) { + return true; + } + } + + // check the pixel to the right + if (x < wid - 1 && !traced[(y*wid)+(x+1)]) { + if ((data.getRGB(x + 1, y) & TRANS_MASK) != 0) { + return true; + } + } + + // check the three-pixel row below the pixel + if (y < hei - 1) { + for (int rxx = x - 1; rxx <= x + 1; rxx++) { + if (rxx < 0 || rxx >= wid || traced[((y+1)*wid)+rxx]) { + continue; + } + + if ((data.getRGB(rxx, y + 1) & TRANS_MASK) != 0) { + return true; + } + } + } + + return false; + } + + /** + * Create an image using the alpha channel from the first and the RGB + * values from the second. + */ + public static BufferedImage composeMaskedImage ( + ImageCreator isrc, BufferedImage mask, BufferedImage base) + { + int wid = base.getWidth(); + int hei = base.getHeight(); + + Raster maskdata = mask.getData(); + Raster basedata = base.getData(); + + // create a new image using the rasters if possible + if (maskdata.getNumBands() == 4 && basedata.getNumBands() >= 3) { + WritableRaster target = + basedata.createCompatibleWritableRaster(wid, hei); + + // copy the alpha from the mask image + int[] adata = maskdata.getSamples(0, 0, wid, hei, 3, (int[]) null); + target.setSamples(0, 0, wid, hei, 3, adata); + + // copy the RGB from the base image + for (int ii=0; ii < 3; ii++) { + int[] cdata = basedata.getSamples( + 0, 0, wid, hei, ii, (int[]) null); + target.setSamples(0, 0, wid, hei, ii, cdata); + } + + return new BufferedImage(mask.getColorModel(), target, true, null); + + } else { + // otherwise composite them by rendering them with an alpha + // rule + BufferedImage target = isrc.createImage( + wid, hei, Transparency.TRANSLUCENT); + Graphics2D g2 = target.createGraphics(); + try { + g2.drawImage(mask, 0, 0, null); + g2.setComposite(AlphaComposite.SrcIn); + g2.drawImage(base, 0, 0, null); + } finally { + g2.dispose(); + } + return target; + } + } + + /** + * Create a new image using the supplied shape as a mask from which to + * cut out pixels from the supplied image. Pixels inside the shape + * will be added to the final image, pixels outside the shape will be + * clear. + */ + public static BufferedImage composeMaskedImage ( + ImageCreator isrc, Shape mask, BufferedImage base) + { + int wid = base.getWidth(); + int hei = base.getHeight(); + + // alternate method for composition: + // 1. create WriteableRaster with base data + // 2. test each pixel with mask.contains() and set the alpha + // channel to fully-alpha if false + // 3. create buffered image from raster + // (I didn't use this method because it depends on the colormodel + // of the source image, and was booching when the souce image was + // a cut-up from a tileset, and it seems like it would take + // longer than the method we are using. + // But it's something to consider) + + // composite them by rendering them with an alpha rule + BufferedImage target = isrc.createImage( + wid, hei, Transparency.TRANSLUCENT); + Graphics2D g2 = target.createGraphics(); + try { + g2.setColor(Color.BLACK); // whatever, really + g2.fill(mask); + g2.setComposite(AlphaComposite.SrcIn); + g2.drawImage(base, 0, 0, null); + } finally { + g2.dispose(); + } + return target; + } + + /** + * Returns true if the supplied image contains a non-transparent pixel + * at the specified coordinates, false otherwise. + */ + public static boolean hitTest (BufferedImage image, int x, int y) + { + // it's only a hit if the pixel is non-transparent + int argb = image.getRGB(x, y); + return (argb >> 24) != 0; + } + + /** + * Computes the bounds of the smallest rectangle that contains all + * non-transparent pixels of this image. This isn't extremely + * efficient, so you shouldn't be doing this anywhere exciting. + */ + public static void computeTrimmedBounds ( + BufferedImage image, Rectangle tbounds) + { + // this could be more efficient, but it's run as a batch process + // and doesn't really take that long anyway + int width = image.getWidth(), height = image.getHeight(); + + int firstrow = -1, lastrow = -1, minx = width, maxx = 0; + for (int yy = 0; yy < height; yy++) { + + int firstidx = -1, lastidx = -1; + for (int xx = 0; xx < width; xx++) { + // if this pixel is transparent, do nothing + int argb = image.getRGB(xx, yy); + if ((argb >> 24) == 0) { + continue; + } + + // otherwise, if we've not seen a non-transparent pixel, + // make a note that this is the first non-transparent + // pixel in the row + if (firstidx == -1) { + firstidx = xx; + } + // keep track of the last non-transparent pixel we saw + lastidx = xx; + } + + // if we saw no pixels on this row, we can bail now + if (firstidx == -1) { + continue; + } + + // update our min and maxx + minx = Math.min(firstidx, minx); + maxx = Math.max(lastidx, maxx); + + // otherwise keep track of the first row on which we see + // pixels and the last row on which we see pixels + if (firstrow == -1) { + firstrow = yy; + } + lastrow = yy; + } + + // fill in the dimensions + if (firstrow != -1) { + tbounds.x = minx; + tbounds.y = firstrow; + tbounds.width = maxx - minx + 1; + tbounds.height = lastrow - firstrow + 1; + } else { + // Entirely blank image. Return 1x1 blank image. + tbounds.x = 0; + tbounds.y = 0; + tbounds.width = 1; + tbounds.height = 1; + } + } + + /** + * Returns the estimated memory usage in bytes for the specified + * image. + */ + public static long getEstimatedMemoryUsage (BufferedImage image) + { + if (image != null) { + return getEstimatedMemoryUsage(image.getRaster()); + } else { + return 0; + } + } + + /** + * Returns the estimated memory usage in bytes for the specified + * raster. + */ + public static long getEstimatedMemoryUsage (Raster raster) + { + // we assume that the data buffer stores each element in a + // byte-rounded memory element; maybe the buffer is smarter about + // things than this, but we're better to err on the safe side + DataBuffer db = raster.getDataBuffer(); + int bpe = (int)Math.ceil( + DataBuffer.getDataTypeSize(db.getDataType()) / 8f); + return bpe * db.getSize(); + } + + /** + * Returns the estimated memory usage in bytes for all buffered images + * in the supplied iterator. + */ + public static long getEstimatedMemoryUsage (Iterator iter) + { + long size = 0; + while (iter.hasNext()) { + BufferedImage image = (BufferedImage)iter.next(); + size += getEstimatedMemoryUsage(image); + } + return size; + } + + /** + * Obtains the default graphics configuration for this VM. If the JVM + * is in headless mode, this method will return null. + */ + protected static GraphicsConfiguration getDefGC () + { + if (_gc == null) { + // obtain information on our graphics environment + try { + GraphicsEnvironment env = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice gd = env.getDefaultScreenDevice(); + _gc = gd.getDefaultConfiguration(); + } catch (HeadlessException e) { + // no problem, just return null + } + } + return _gc; + } + + /** The graphics configuration for the default screen device. */ + protected static GraphicsConfiguration _gc; + + /** Used when seeking fully transparent pixels for outlining. */ + protected static final int TRANS_MASK = (0xFF << 24); + + /** Used when outlining. */ + protected static final int RGB_MASK = 0x00FFFFFF; +} diff --git a/src/java/com/threerings/media/image/Mirage.java b/src/java/com/threerings/media/image/Mirage.java new file mode 100644 index 00000000..3e05da42 --- /dev/null +++ b/src/java/com/threerings/media/image/Mirage.java @@ -0,0 +1,67 @@ +// +// $Id: Mirage.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +/** + * Provides an interface via which images can be accessed in a way that + * allows them to optionally be located in video memory where that affords + * performance improvements. + */ +public interface Mirage +{ + /** + * Renders this mirage at the specified position in the supplied + * graphics context. + */ + public void paint (Graphics2D gfx, int x, int y); + + /** + * Returns the width of this mirage. + */ + public int getWidth (); + + /** + * Returns the height of this mirage. + */ + public int getHeight (); + + /** + * Returns true if this mirage contains a non-transparent pixel at the + * specified coordinate. + */ + public boolean hitTest (int x, int y); + + /** + * Returns a snapshot of this mirage as a buffered image. The snapshot + * should not be modified by the caller. + */ + public BufferedImage getSnapshot (); + + /** + * Returns an estimate of the memory consumed by this mirage's image + * raster data. + */ + public long getEstimatedMemoryUsage (); +} diff --git a/src/java/com/threerings/media/image/MirageIcon.java b/src/java/com/threerings/media/image/MirageIcon.java new file mode 100644 index 00000000..d26be90c --- /dev/null +++ b/src/java/com/threerings/media/image/MirageIcon.java @@ -0,0 +1,60 @@ +// +// $Id: MirageIcon.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Component; +import java.awt.Graphics2D; +import java.awt.Graphics; + +import javax.swing.Icon; + +/** + * Implements the Swing {@link Icon} interface with a mirage providing the + * image information. + */ +public class MirageIcon implements Icon +{ + public MirageIcon (Mirage mirage) + { + _mirage = mirage; + } + + // documentation inherited from interface + public void paintIcon (Component c, Graphics g, int x, int y) + { + _mirage.paint((Graphics2D)g, x, y); + } + + // documentation inherited from interface + public int getIconWidth() + { + return _mirage.getWidth(); + } + + // documentation inherited from interface + public int getIconHeight() + { + return _mirage.getHeight(); + } + + protected Mirage _mirage; +} diff --git a/src/java/com/threerings/media/image/Quantize.java b/src/java/com/threerings/media/image/Quantize.java new file mode 100644 index 00000000..8a6cfecc --- /dev/null +++ b/src/java/com/threerings/media/image/Quantize.java @@ -0,0 +1,776 @@ +// +// $Id: Quantize.java 4031 2006-04-18 20:35:28Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +package com.threerings.media.image; + +/* + * @(#)Quantize.java 0.90 9/19/00 Adam Doppelt + */ + +/** + * Calculates a reduced color + * + * Three Rings note: Code taken from + * Adam Doppelt, who + * adapted it from other code. Feel the love.

+ * + * RenderingHints is supposed to provide a way to block dithering, but I have + * not been able to get that to work. It always dithers, so we use this + * class instead. + *

+ * + * The following modifications were added to the original code: + * - Made it work with image data with transparent pixels. + * - Clarified documentation of the main method. + * - Changed the 'QUICK' constant to false for better quantization. + * - Fixed an integer overflow that caused a bug quantizing large images. + * + *

+ * + * Original headers follow: + * + * + * + * + * An efficient color quantization algorithm, adapted from the C++ + * implementation quantize.c in ImageMagick. The pixels for + * an image are placed into an oct tree. The oct tree is reduced in + * size, and the pixels from the original image are reassigned to the + * nodes in the reduced tree.

+ * + * Here is the copyright notice from ImageMagick: + * + *

+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%  Permission is hereby granted, free of charge, to any person obtaining a    %
+%  copy of this software and associated documentation files ("ImageMagick"),  %
+%  to deal in ImageMagick without restriction, including without limitation   %
+%  the rights to use, copy, modify, merge, publish, distribute, sublicense,   %
+%  and/or sell copies of ImageMagick, and to permit persons to whom the       %
+%  ImageMagick is furnished to do so, subject to the following conditions:    %
+%                                                                             %
+%  The above copyright notice and this permission notice shall be included in %
+%  all copies or substantial portions of ImageMagick.                         %
+%                                                                             %
+%  The software is provided "as is", without warranty of any kind, express or %
+%  implied, including but not limited to the warranties of merchantability,   %
+%  fitness for a particular purpose and noninfringement.  In no event shall   %
+%  E. I. du Pont de Nemours and Company be liable for any claim, damages or   %
+%  other liability, whether in an action of contract, tort or otherwise,      %
+%  arising from, out of or in connection with ImageMagick or the use or other %
+%  dealings in ImageMagick.                                                   %
+%                                                                             %
+%  Except as contained in this notice, the name of the E. I. du Pont de       %
+%  Nemours and Company shall not be used in advertising or otherwise to       %
+%  promote the sale, use or other dealings in ImageMagick without prior       %
+%  written authorization from the E. I. du Pont de Nemours and Company.       %
+%                                                                             %
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+ * + * + * @version 0.90 19 Sep 2000 + * @author Adam Doppelt + */ +public class Quantize { + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% QQQ U U AAA N N TTTTT IIIII ZZZZZ EEEEE % +% Q Q U U A A NN N T I ZZ E % +% Q Q U U AAAAA N N N T I ZZZ EEEEE % +% Q QQ U U A A N NN T I ZZ E % +% QQQQ UUU A A N N T IIIII ZZZZZ EEEEE % +% % +% % +% Reduce the Number of Unique Colors in an Image % +% % +% % +% Software Design % +% John Cristy % +% July 1992 % +% % +% % +% Copyright 1998 E. I. du Pont de Nemours and Company % +% % +% Permission is hereby granted, free of charge, to any person obtaining a % +% copy of this software and associated documentation files ("ImageMagick"), % +% to deal in ImageMagick without restriction, including without limitation % +% the rights to use, copy, modify, merge, publish, distribute, sublicense, % +% and/or sell copies of ImageMagick, and to permit persons to whom the % +% ImageMagick is furnished to do so, subject to the following conditions: % +% % +% The above copyright notice and this permission notice shall be included in % +% all copies or substantial portions of ImageMagick. % +% % +% The software is provided "as is", without warranty of any kind, express or % +% implied, including but not limited to the warranties of merchantability, % +% fitness for a particular purpose and noninfringement. In no event shall % +% E. I. du Pont de Nemours and Company be liable for any claim, damages or % +% other liability, whether in an action of contract, tort or otherwise, % +% arising from, out of or in connection with ImageMagick or the use or other % +% dealings in ImageMagick. % +% % +% Except as contained in this notice, the name of the E. I. du Pont de % +% Nemours and Company shall not be used in advertising or otherwise to % +% promote the sale, use or other dealings in ImageMagick without prior % +% written authorization from the E. I. du Pont de Nemours and Company. % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Realism in computer graphics typically requires using 24 bits/pixel to +% generate an image. Yet many graphic display devices do not contain +% the amount of memory necessary to match the spatial and color +% resolution of the human eye. The QUANTIZE program takes a 24 bit +% image and reduces the number of colors so it can be displayed on +% raster device with less bits per pixel. In most instances, the +% quantized image closely resembles the original reference image. +% +% A reduction of colors in an image is also desirable for image +% transmission and real-time animation. +% +% Function Quantize takes a standard RGB or monochrome images and quantizes +% them down to some fixed number of colors. +% +% For purposes of color allocation, an image is a set of n pixels, where +% each pixel is a point in RGB space. RGB space is a 3-dimensional +% vector space, and each pixel, pi, is defined by an ordered triple of +% red, green, and blue coordinates, (ri, gi, bi). +% +% Each primary color component (red, green, or blue) represents an +% intensity which varies linearly from 0 to a maximum value, cmax, which +% corresponds to full saturation of that color. Color allocation is +% defined over a domain consisting of the cube in RGB space with +% opposite vertices at (0,0,0) and (cmax,cmax,cmax). QUANTIZE requires +% cmax = 255. +% +% The algorithm maps this domain onto a tree in which each node +% represents a cube within that domain. In the following discussion +% these cubes are defined by the coordinate of two opposite vertices: +% The vertex nearest the origin in RGB space and the vertex farthest +% from the origin. +% +% The tree's root node represents the the entire domain, (0,0,0) through +% (cmax,cmax,cmax). Each lower level in the tree is generated by +% subdividing one node's cube into eight smaller cubes of equal size. +% This corresponds to bisecting the parent cube with planes passing +% through the midpoints of each edge. +% +% The basic algorithm operates in three phases: Classification, +% Reduction, and Assignment. Classification builds a color +% description tree for the image. Reduction collapses the tree until +% the number it represents, at most, the number of colors desired in the +% output image. Assignment defines the output image's color map and +% sets each pixel's color by reclassification in the reduced tree. +% Our goal is to minimize the numerical discrepancies between the original +% colors and quantized colors (quantization error). +% +% Classification begins by initializing a color description tree of +% sufficient depth to represent each possible input color in a leaf. +% However, it is impractical to generate a fully-formed color +% description tree in the classification phase for realistic values of +% cmax. If colors components in the input image are quantized to k-bit +% precision, so that cmax= 2k-1, the tree would need k levels below the +% root node to allow representing each possible input color in a leaf. +% This becomes prohibitive because the tree's total number of nodes is +% 1 + sum(i=1,k,8k). +% +% A complete tree would require 19,173,961 nodes for k = 8, cmax = 255. +% Therefore, to avoid building a fully populated tree, QUANTIZE: (1) +% Initializes data structures for nodes only as they are needed; (2) +% Chooses a maximum depth for the tree as a function of the desired +% number of colors in the output image (currently log2(colormap size)). +% +% For each pixel in the input image, classification scans downward from +% the root of the color description tree. At each level of the tree it +% identifies the single node which represents a cube in RGB space +% containing the pixel's color. It updates the following data for each +% such node: +% +% n1: Number of pixels whose color is contained in the RGB cube +% which this node represents; +% +% n2: Number of pixels whose color is not represented in a node at +% lower depth in the tree; initially, n2 = 0 for all nodes except +% leaves of the tree. +% +% Sr, Sg, Sb: Sums of the red, green, and blue component values for +% all pixels not classified at a lower depth. The combination of +% these sums and n2 will ultimately characterize the mean color of a +% set of pixels represented by this node. +% +% E: The distance squared in RGB space between each pixel contained +% within a node and the nodes' center. This represents the quantization +% error for a node. +% +% Reduction repeatedly prunes the tree until the number of nodes with +% n2 > 0 is less than or equal to the maximum number of colors allowed +% in the output image. On any given iteration over the tree, it selects +% those nodes whose E count is minimal for pruning and merges their +% color statistics upward. It uses a pruning threshold, Ep, to govern +% node selection as follows: +% +% Ep = 0 +% while number of nodes with (n2 > 0) > required maximum number of colors +% prune all nodes such that E <= Ep +% Set Ep to minimum E in remaining nodes +% +% This has the effect of minimizing any quantization error when merging +% two nodes together. +% +% When a node to be pruned has offspring, the pruning procedure invokes +% itself recursively in order to prune the tree from the leaves upward. +% n2, Sr, Sg, and Sb in a node being pruned are always added to the +% corresponding data in that node's parent. This retains the pruned +% node's color characteristics for later averaging. +% +% For each node, n2 pixels exist for which that node represents the +% smallest volume in RGB space containing those pixel's colors. When n2 +% > 0 the node will uniquely define a color in the output image. At the +% beginning of reduction, n2 = 0 for all nodes except a the leaves of +% the tree which represent colors present in the input image. +% +% The other pixel count, n1, indicates the total number of colors +% within the cubic volume which the node represents. This includes n1 - +% n2 pixels whose colors should be defined by nodes at a lower level in +% the tree. +% +% Assignment generates the output image from the pruned tree. The +% output image consists of two parts: (1) A color map, which is an +% array of color descriptions (RGB triples) for each color present in +% the output image; (2) A pixel array, which represents each pixel as +% an index into the color map array. +% +% First, the assignment phase makes one pass over the pruned color +% description tree to establish the image's color map. For each node +% with n2 > 0, it divides Sr, Sg, and Sb by n2 . This produces the +% mean color of all pixels that classify no lower than this node. Each +% of these colors becomes an entry in the color map. +% +% Finally, the assignment phase reclassifies each pixel in the pruned +% tree to identify the deepest node containing the pixel's color. The +% pixel's value in the pixel array becomes the index of this node's mean +% color in the color map. +% +% With the permission of USC Information Sciences Institute, 4676 Admiralty +% Way, Marina del Rey, California 90292, this code was adapted from module +% ALCOLS written by Paul Raveling. +% +% The names of ISI and USC are not used in advertising or publicity +% pertaining to distribution of the software without prior specific +% written permission from ISI. +% +*/ + + final static boolean QUICK = false; + + final static int MAX_RGB = 255; + final static int MAX_NODES = 266817; + final static int MAX_TREE_DEPTH = 8; + + // these are precomputed in advance + static int SQUARES[]; + static int SHIFT[]; + + static { + SQUARES = new int[MAX_RGB + MAX_RGB + 1]; + for (int i= -MAX_RGB; i <= MAX_RGB; i++) { + SQUARES[i + MAX_RGB] = i * i; + } + + SHIFT = new int[MAX_TREE_DEPTH + 1]; + for (int i = 0; i < MAX_TREE_DEPTH + 1; ++i) { + SHIFT[i] = 1 << (15 - i); + } + } + + /** + * Reduce the image to the given number of colors. + * + * @param pixels an in/out parameter that should initially contain + * [A]RGB values but that will contain color palette indicies upon return. + * + * @return The new color palette. + */ + public static int[] quantizeImage(int pixels[][], int max_colors) { + Cube cube = new Cube(pixels, max_colors); + cube.classification(); + cube.reduction(); + cube.assignment(); + return cube.colormap; + } + + static class Cube { + int pixels[][]; + int max_colors; + int colormap[]; + + // do we have transparent pixels? + boolean hasTrans = false; + + Node root; + int depth; + + // counter for the number of colors in the cube. this gets + // recalculated often. + int colors; + + // counter for the number of nodes in the tree + int nodes; + + Cube(int pixels[][], int max_colors) { + this.pixels = pixels; + this.max_colors = max_colors; + + int i = max_colors; + // tree_depth = log max_colors + // 4 + for (depth = 1; i != 0; depth++) { + i /= 4; + } + if (depth > 1) { + --depth; + } + if (depth > MAX_TREE_DEPTH) { + depth = MAX_TREE_DEPTH; + } else if (depth < 2) { + depth = 2; + } + + root = new Node(this); + } + + /* + * Procedure Classification begins by initializing a color + * description tree of sufficient depth to represent each + * possible input color in a leaf. However, it is impractical + * to generate a fully-formed color description tree in the + * classification phase for realistic values of cmax. If + * colors components in the input image are quantized to k-bit + * precision, so that cmax= 2k-1, the tree would need k levels + * below the root node to allow representing each possible + * input color in a leaf. This becomes prohibitive because the + * tree's total number of nodes is 1 + sum(i=1,k,8k). + * + * A complete tree would require 19,173,961 nodes for k = 8, + * cmax = 255. Therefore, to avoid building a fully populated + * tree, QUANTIZE: (1) Initializes data structures for nodes + * only as they are needed; (2) Chooses a maximum depth for + * the tree as a function of the desired number of colors in + * the output image (currently log2(colormap size)). + * + * For each pixel in the input image, classification scans + * downward from the root of the color description tree. At + * each level of the tree it identifies the single node which + * represents a cube in RGB space containing It updates the + * following data for each such node: + * + * number_pixels : Number of pixels whose color is contained + * in the RGB cube which this node represents; + * + * unique : Number of pixels whose color is not represented + * in a node at lower depth in the tree; initially, n2 = 0 + * for all nodes except leaves of the tree. + * + * total_red/green/blue : Sums of the red, green, and blue + * component values for all pixels not classified at a lower + * depth. The combination of these sums and n2 will + * ultimately characterize the mean color of a set of pixels + * represented by this node. + */ + void classification() { + int pixels[][] = this.pixels; + + int width = pixels.length; + int height = pixels[0].length; + + // convert to indexed color + for (int x = width; x-- > 0; ) { + for (int y = height; y-- > 0; ) { + int pixel = pixels[x][y]; + int alpha = (pixel >> 24) & 0xFF; + if (alpha != 255) { + hasTrans = true; + continue; // don't add transparent pixels to the cube + } + int red = (pixel >> 16) & 0xFF; + int green = (pixel >> 8) & 0xFF; + int blue = (pixel >> 0) & 0xFF; + + // a hard limit on the number of nodes in the tree + if (nodes > MAX_NODES) { + System.out.println("pruning"); + root.pruneLevel(); + --depth; + } + + // walk the tree to depth, increasing the + // number_pixels count for each node + Node node = root; + for (int level = 1; level <= depth; ++level) { + int id = (((red > node.mid_red ? 1 : 0) << 0) | + ((green > node.mid_green ? 1 : 0) << 1) | + ((blue > node.mid_blue ? 1 : 0) << 2)); + if (node.child[id] == null) { + new Node(node, id, level); + } + node = node.child[id]; + node.number_pixels += SHIFT[level]; + } + + ++node.unique; + node.total_red += red; + node.total_green += green; + node.total_blue += blue; + } + } + + // if we have transparent pixels, that cuts into the number + // of other colors we can use. + if (hasTrans) { + this.max_colors--; + } + } + + /* + * reduction repeatedly prunes the tree until the number of + * nodes with unique > 0 is less than or equal to the maximum + * number of colors allowed in the output image. + * + * When a node to be pruned has offspring, the pruning + * procedure invokes itself recursively in order to prune the + * tree from the leaves upward. The statistics of the node + * being pruned are always added to the corresponding data in + * that node's parent. This retains the pruned node's color + * characteristics for later averaging. + */ + void reduction() { + long threshold = 1; + while (colors > max_colors) { + colors = 0; + threshold = root.reduce(threshold, Long.MAX_VALUE); + } + } + + /** + * The result of a closest color search. + */ + static class Search { + int distance; + int color_number; + } + + /* + * Procedure assignment generates the output image from the + * pruned tree. The output image consists of two parts: (1) A + * color map, which is an array of color descriptions (RGB + * triples) for each color present in the output image; (2) A + * pixel array, which represents each pixel as an index into + * the color map array. + * + * First, the assignment phase makes one pass over the pruned + * color description tree to establish the image's color map. + * For each node with n2 > 0, it divides Sr, Sg, and Sb by n2. + * This produces the mean color of all pixels that classify no + * lower than this node. Each of these colors becomes an entry + * in the color map. + * + * Finally, the assignment phase reclassifies each pixel in + * the pruned tree to identify the deepest node containing the + * pixel's color. The pixel's value in the pixel array becomes + * the index of this node's mean color in the color map. + */ + void assignment() { + colormap = new int[colors]; + colors = 0; + root.colormap(); + + int pixels[][] = this.pixels; + + int width = pixels.length; + int height = pixels[0].length; + + Search search = new Search(); + + int transPad = hasTrans ? 1 : 0; + + // convert to indexed color + for (int x = width; x-- > 0; ) { + for (int y = height; y-- > 0; ) { + int pixel = pixels[x][y]; + int alpha = (pixel >> 24) & 0xFF; + if (alpha != 255) { + pixels[x][y] = 0; // transparent + continue; + } + int red = (pixel >> 16) & 0xFF; + int green = (pixel >> 8) & 0xFF; + int blue = (pixel >> 0) & 0xFF; + + // walk the tree to find the cube containing that color + Node node = root; + for ( ; ; ) { + int id = (((red > node.mid_red ? 1 : 0) << 0) | + ((green > node.mid_green ? 1 : 0) << 1) | + ((blue > node.mid_blue ? 1 : 0) << 2) ); + if (node.child[id] == null) { + break; + } + node = node.child[id]; + } + + if (QUICK) { + // if QUICK is set, just use that + // node. Strictly speaking, this isn't + // necessarily best match. + pixels[x][y] = node.color_number + transPad; + } else { + // Find the closest color. + search.distance = Integer.MAX_VALUE; + node.parent.closestColor(red, green, blue, search); + pixels[x][y] = search.color_number + transPad; + } + } + } + + // expand the colormap by one to account for the transparent + if (hasTrans) { + int[] newcmap = new int[colormap.length + 1]; + System.arraycopy(colormap, 0, newcmap, 1, colormap.length); + colormap = newcmap; + } + } + + /** + * A single Node in the tree. + */ + static class Node { + Cube cube; + + // parent node + Node parent; + + // child nodes + Node child[]; + int nchild; + + // our index within our parent + int id; + // our level within the tree + int level; + // our color midpoint + int mid_red; + int mid_green; + int mid_blue; + + // the pixel count for this node and all children + long number_pixels; + + // the pixel count for this node + int unique; + // the sum of all pixels contained in this node + int total_red; + int total_green; + int total_blue; + + // used to build the colormap + int color_number; + + Node(Cube cube) { + this.cube = cube; + this.parent = this; + this.child = new Node[8]; + this.id = 0; + this.level = 0; + + this.number_pixels = Long.MAX_VALUE; + + this.mid_red = (MAX_RGB + 1) >> 1; + this.mid_green = (MAX_RGB + 1) >> 1; + this.mid_blue = (MAX_RGB + 1) >> 1; + } + + Node(Node parent, int id, int level) { + this.cube = parent.cube; + this.parent = parent; + this.child = new Node[8]; + this.id = id; + this.level = level; + + // add to the cube + ++cube.nodes; + if (level == cube.depth) { + ++cube.colors; + } + + // add to the parent + ++parent.nchild; + parent.child[id] = this; + + // figure out our midpoint + int bi = (1 << (MAX_TREE_DEPTH - level)) >> 1; + mid_red = parent.mid_red + ((id & 1) > 0 ? bi : -bi); + mid_green = parent.mid_green + ((id & 2) > 0 ? bi : -bi); + mid_blue = parent.mid_blue + ((id & 4) > 0 ? bi : -bi); + } + + /** + * Remove this child node, and make sure our parent + * absorbs our pixel statistics. + */ + void pruneChild() { + --parent.nchild; + parent.unique += unique; + parent.total_red += total_red; + parent.total_green += total_green; + parent.total_blue += total_blue; + parent.child[id] = null; + --cube.nodes; + cube = null; + parent = null; + } + + /** + * Prune the lowest layer of the tree. + */ + void pruneLevel() { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + child[id].pruneLevel(); + } + } + } + if (level == cube.depth) { + pruneChild(); + } + } + + /** + * Remove any nodes that have fewer than threshold + * pixels. Also, as long as we're walking the tree: + * + * - figure out the color with the fewest pixels + * - recalculate the total number of colors in the tree + */ + long reduce(long threshold, long next_threshold) { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + next_threshold = child[id].reduce(threshold, next_threshold); + } + } + } + if (number_pixels <= threshold) { + pruneChild(); + } else { + if (unique != 0) { + cube.colors++; + } + if (number_pixels < next_threshold) { + next_threshold = number_pixels; + } + } + return next_threshold; + } + + /* + * colormap traverses the color cube tree and notes each + * colormap entry. A colormap entry is any node in the + * color cube tree where the number of unique colors is + * not zero. + */ + void colormap() { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + child[id].colormap(); + } + } + } + if (unique != 0) { + int r = ((total_red + (unique >> 1)) / unique); + int g = ((total_green + (unique >> 1)) / unique); + int b = ((total_blue + (unique >> 1)) / unique); + cube.colormap[cube.colors] = ((( 0xFF) << 24) | + ((r & 0xFF) << 16) | + ((g & 0xFF) << 8) | + ((b & 0xFF) << 0)); + color_number = cube.colors++; + } + } + + /* ClosestColor traverses the color cube tree at a + * particular node and determines which colormap entry + * best represents the input color. + */ + void closestColor(int red, int green, int blue, Search search) { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + child[id].closestColor(red, green, blue, search); + } + } + } + + if (unique != 0) { + int color = cube.colormap[color_number]; + int distance = distance(color, red, green, blue); + if (distance < search.distance) { + search.distance = distance; + search.color_number = color_number; + } + } + } + + /** + * Figure out the distance between this node and som color. + */ + final static int distance(int color, int r, int g, int b) { + return (SQUARES[((color >> 16) & 0xFF) - r + MAX_RGB] + + SQUARES[((color >> 8) & 0xFF) - g + MAX_RGB] + + SQUARES[((color >> 0) & 0xFF) - b + MAX_RGB]); + } + + public String toString() { + StringBuilder buf = new StringBuilder(); + if (parent == this) { + buf.append("root"); + } else { + buf.append("node"); + } + buf.append(' '); + buf.append(level); + buf.append(" ["); + buf.append(mid_red); + buf.append(','); + buf.append(mid_green); + buf.append(','); + buf.append(mid_blue); + buf.append(']'); + return new String(buf); + } + } + } +} diff --git a/src/java/com/threerings/media/image/TransformedMirage.java b/src/java/com/threerings/media/image/TransformedMirage.java new file mode 100644 index 00000000..87e06fa2 --- /dev/null +++ b/src/java/com/threerings/media/image/TransformedMirage.java @@ -0,0 +1,145 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.image.BufferedImage; + +import com.threerings.media.Log; + +/** + * Draws a mirage combined with an arbitrary AffineTransform. + */ +public class TransformedMirage + implements Mirage +{ + /** + * Constructor. + */ + public TransformedMirage (Mirage base, AffineTransform transform) + { + _base = base; + + // clone the transform so that it doesn't get changed on us. + _transform = (AffineTransform) transform.clone(); + computeTransformedBounds(); + _transform.preConcatenate( + AffineTransform.getTranslateInstance(-_bounds.x, -_bounds.y)); + } + + // documentation inherited from interface Mirage + public void paint (Graphics2D gfx, int x, int y) + { + AffineTransform otrans = gfx.getTransform(); + gfx.translate(x, y); + gfx.transform(_transform); + _base.paint(gfx, 0, 0); + gfx.setTransform(otrans); + } + + // documentation inherited from interface Mirage + public int getWidth () + { + return _bounds.width; + } + + // documentation inherited from interface Mirage + public int getHeight () + { + return _bounds.height; + } + + // documentation inherited from interface Mirage + public boolean hitTest (int x, int y) + { + Point p = new Point(x, y); + try { + _transform.createInverse().transform(p, p); + return _base.hitTest(p.x, p.y); + + } catch (NoninvertibleTransformException nte) { + // grumble, grumble + // TODO: log something? + return ImageUtil.hitTest(getSnapshot(), x, y); + } + } + + // documentation inherited from interface Mirage + public BufferedImage getSnapshot () + { + BufferedImage baseSnap = _base.getSnapshot(); + BufferedImage img = new BufferedImage(_bounds.width, _bounds.height, + baseSnap.getType()); + Graphics2D gfx = (Graphics2D) img.getGraphics(); + try { + gfx.transform(_transform); + gfx.drawImage(baseSnap, 0, 0, null); + } finally { + gfx.dispose(); + } + return img; + } + + // documentation inherited from interface Mirage + public long getEstimatedMemoryUsage () + { + return _base.getEstimatedMemoryUsage(); + } + + /** + * Compute the bounds of the base Mirage after it has been + * transformed. + */ + protected void computeTransformedBounds () + { + int w = _base.getWidth(); + int h = _base.getHeight(); + Point[] points = new Point[] { + new Point(0, 0), new Point(w, 0), new Point(0, h), new Point(w, h) }; + _transform.transform(points, 0, points, 0, 4); + int minX, minY, maxX, maxY; + minX = minY = Integer.MAX_VALUE; + maxX = maxY = Integer.MIN_VALUE; + for (int ii=0; ii < 4; ii++) { + minX = Math.min(minX, points[ii].x); + maxX = Math.max(maxX, points[ii].x); + minY = Math.min(minY, points[ii].y); + maxY = Math.max(maxY, points[ii].y); + } + + _bounds = new Rectangle(minX, minY, maxX - minX, maxY - minY); + } + + /** The base mirage. */ + protected Mirage _base; + + /** Our transformed bounds. */ + protected Rectangle _bounds; + + /** The transform we apply when painting the base mirage. */ + protected AffineTransform _transform; +} diff --git a/src/java/com/threerings/media/image/VolatileMirage.java b/src/java/com/threerings/media/image/VolatileMirage.java new file mode 100644 index 00000000..8717f7fd --- /dev/null +++ b/src/java/com/threerings/media/image/VolatileMirage.java @@ -0,0 +1,202 @@ +// +// $Id: VolatileMirage.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; + +import com.samskivert.util.StringUtil; + +/** + * A mirage implementation which allows the image to be maintained in + * video memory and rebuilt from some source image or images in the event + * that our target screen resolution changes or we are flushed from video + * memory for some other reason. + */ +public abstract class VolatileMirage implements Mirage +{ + /** + * Informs the base class of its image manager and image bounds. + */ + protected VolatileMirage (ImageManager imgr, Rectangle bounds) + { + _imgr = imgr; + _bounds = bounds; + } + + // documentation inherited from interface + public void paint (Graphics2D gfx, int x, int y) + { + // create our volatile image for the first time if necessary + if (_image == null) { + createVolatileImage(); + } + +// int renders = 0; +// do { +// // validate that our image is compatible with the target GC +// switch (_image.validate(_imgr.getGraphicsConfiguration())) { +// case VolatileImage.IMAGE_RESTORED: +// refreshVolatileImage(); // need to rerender it +// break; +// case VolatileImage.IMAGE_INCOMPATIBLE: +// createVolatileImage(); // need to recreate it +// break; +// } + +// // now we can render it +// gfx.drawImage(_image, x, y, null); +// renders++; + +// // don't try forever +// } while (_image.contentsLost() && (renders < 10)); + + if (IMAGE_DEBUG) { + gfx.setColor(new Color(_image.getRGB(_bounds.width/2, + _bounds.height/2))); + gfx.fillRect(x, y, _bounds.width, _bounds.height); + } else { + gfx.drawImage(_image, x, y, null); + } + + // TODO: note number of attempted renders for performance + } + + /** + * Returns the x offset into our source image, which is generally zero but + * may be non-zero for a mirage that obtains its data from a region of its + * source image. + */ + public int getX () + { + return _bounds.x; + } + + /** + * Returns the y offset into our source image, which is generally zero but + * may be non-zero for a mirage that obtains its data from a region of its + * source image. + */ + public int getY () + { + return _bounds.y; + } + + // documentation inherited from interface + public int getWidth () + { + return _bounds.width; + } + + // documentation inherited from interface + public int getHeight () + { + return _bounds.height; + } + + // documentation inherited from interface + public boolean hitTest (int x, int y) + { +// return ImageUtil.hitTest(_image.getSnapshot(), x, y); + return ImageUtil.hitTest(_image, x, y); + } + + // documentation inherited from interface + public long getEstimatedMemoryUsage () + { + return ImageUtil.getEstimatedMemoryUsage(_image.getRaster()); + } + + // documentation inherited from interface + public BufferedImage getSnapshot () + { +// return _image.getSnapshot(); + return _image; + } + + /** + * Creates our volatile image from the information in our source + * image. + */ + protected void createVolatileImage () + { + // release any previous volatile image we might hold + if (_image != null) { + _image.flush(); + } + + // create a new, compatible, volatile image +// _image = _imgr.createVolatileImage( +// _bounds.width, _bounds.height, getTransparency()); + _image = _imgr.createImage( + _bounds.width, _bounds.height, getTransparency()); + + // render our source image into the volatile image + refreshVolatileImage(); + } + + /** + * Returns the transparency that should be used when creating our + * volatile image. + */ + protected abstract int getTransparency (); + + /** + * Rerenders our volatile image from the its source image data. + */ + protected abstract void refreshVolatileImage (); + + /** + * Generates a string representation of this instance. + */ + public String toString () + { + StringBuilder buf = new StringBuilder("["); + toString(buf); + return buf.append("]").toString(); + } + + /** + * Generates a string representation of this instance. + */ + protected void toString (StringBuilder buf) + { + buf.append("bounds=").append(StringUtil.toString(_bounds)); + } + + /** The image manager with whom we interoperate. */ + protected ImageManager _imgr; + + /** The bounds of the region of our source image which we desire for + * this mirage (possibly the whole thing). */ + protected Rectangle _bounds; + + /** Our volatile image which lives in video memory and can go away at + * any time. */ +// protected VolatileImage _image; + protected BufferedImage _image; + + /** Turns off image rendering for testing. */ + protected static final boolean IMAGE_DEBUG = false; +} diff --git a/src/java/com/threerings/media/image/tools/DumpColorPository.java b/src/java/com/threerings/media/image/tools/DumpColorPository.java new file mode 100644 index 00000000..83b35317 --- /dev/null +++ b/src/java/com/threerings/media/image/tools/DumpColorPository.java @@ -0,0 +1,54 @@ +// +// $Id: DumpColorPository.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image.tools; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Iterator; + +import com.threerings.media.image.ColorPository; + +/** + * Simple tool for dumping a serialized color pository. + */ +public class DumpColorPository +{ + public static void main (String[] args) + { + if (args.length == 0) { + System.err.println("Usage: DumpColorPository colorpos.dat"); + System.exit(-1); + } + + try { + ColorPository pos = ColorPository.loadColorPository( + new FileInputStream(args[0])); + Iterator iter = pos.enumerateClasses(); + while (iter.hasNext()) { + System.out.println(iter.next()); + } + + } catch (IOException ioe) { + ioe.printStackTrace(System.err); + } + } +} diff --git a/src/java/com/threerings/media/image/tools/xml/ColorPositoryParser.java b/src/java/com/threerings/media/image/tools/xml/ColorPositoryParser.java new file mode 100644 index 00000000..e3add99b --- /dev/null +++ b/src/java/com/threerings/media/image/tools/xml/ColorPositoryParser.java @@ -0,0 +1,78 @@ +// +// $Id: ColorPositoryParser.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.image.tools.xml; + +import java.io.Serializable; + +import org.xml.sax.Attributes; +import org.apache.commons.digester.Digester; +import org.apache.commons.digester.Rule; +import com.samskivert.xml.SetPropertyFieldsRule; + +import com.threerings.tools.xml.CompiledConfigParser; + +import com.threerings.media.image.ColorPository.ClassRecord; +import com.threerings.media.image.ColorPository.ColorRecord; +import com.threerings.media.image.ColorPository; + +/** + * Parses the XML color repository definition and creates a {@link + * ColorPository} instance that reflects its contents. + */ +public class ColorPositoryParser extends CompiledConfigParser +{ + // documentation inherited + protected Serializable createConfigObject () + { + return new ColorPository(); + } + + // documentation inherited + protected void addRules (Digester digest) + { + // create and configure class record instances + String prefix = "colors/class"; + digest.addObjectCreate(prefix, ClassRecord.class.getName()); + digest.addRule(prefix, new SetPropertyFieldsRule()); + digest.addSetNext(prefix, "addClass", ClassRecord.class.getName()); + + // create and configure color record instances + prefix += "/color"; + digest.addRule(prefix, new Rule() { + public void begin (String namespace, String name, + Attributes attributes) throws Exception { + // we want to inherit settings from the color class when + // creating the record, so we do some custom stuff + ColorRecord record = new ColorRecord(); + ClassRecord clrec = (ClassRecord)digester.peek(); + record.starter = clrec.starter; + digester.push(record); + } + + public void end (String namespace, String name) throws Exception { + digester.pop(); + } + }); + digest.addRule(prefix, new SetPropertyFieldsRule()); + digest.addSetNext(prefix, "addColor", ColorRecord.class.getName()); + } +} diff --git a/src/java/com/threerings/media/sound/MidiPlayer.java b/src/java/com/threerings/media/sound/MidiPlayer.java new file mode 100644 index 00000000..2642be7a --- /dev/null +++ b/src/java/com/threerings/media/sound/MidiPlayer.java @@ -0,0 +1,178 @@ +// +// $Id: MidiPlayer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sound; + +import java.io.BufferedInputStream; +import java.io.InputStream; + +import javax.sound.midi.MetaEventListener; +import javax.sound.midi.MetaMessage; +import javax.sound.midi.MidiChannel; +import javax.sound.midi.MidiSystem; +import javax.sound.midi.Sequencer; +import javax.sound.midi.Synthesizer; + +/** + * Plays midi/rmf sounds using Java's sequencer, which is susceptible + * to the accuracy of System.currentTimeMillis() and so currently sounds + * like "ass" under Windows. + */ +public class MidiPlayer extends MusicPlayer + implements MetaEventListener +{ + // documentation inherited + public void init () + throws Exception + { + _sequencer = MidiSystem.getSequencer(); + _sequencer.open(); + if (_sequencer instanceof Synthesizer) { + _channels = ((Synthesizer) _sequencer).getChannels(); + } + } + + // documentation inherited + public void shutdown () + { + _sequencer.close(); + } + + // documentation inherited + public void start (InputStream stream) + throws Exception + { + _sequencer.setSequence(new BufferedInputStream(stream)); + _sequencer.start(); + _sequencer.addMetaEventListener(this); + } + + // documentation inherited + public void stop () + { + _sequencer.removeMetaEventListener(this); + _sequencer.stop(); + } + + // documentation inherited + public void setVolume (float volume) + { + if (_channels != null) { + int setting = (int) (volume * 127.0); + for (int ii=0; ii < _channels.length; ii++) { + _channels[ii].controlChange(VOLUME_CONTROL, setting); + } + } + } + + // documentation inherited from interface MetaEventListener + public void meta (MetaMessage msg) + { + if (msg.getType() == END_OF_TRACK) { + _musicListener.musicStopped(); + } + } + +// STUFF FROM ANOTHER TIME +// /** +// * Get a list of alternate midi devices. +// */ +// public MidiDevice.Info[] getAlternateMidiDevices () +// { +// ArrayList infos = new ArrayList(); +// CollectionUtil.addAll(infos, MidiSystem.getMidiDeviceInfo()); +// +// // remove the synth/seqs, leaving only hardware midi thingies +// for (Iterator iter=infos.iterator(); iter.hasNext(); ) { +// try { +// MidiDevice dev = MidiSystem.getMidiDevice( +// (MidiDevice.Info) iter.next()); +// if ((dev instanceof Sequencer) || +// (dev instanceof Synthesizer)) { +// iter.remove(); +// } +// } catch (MidiUnavailableException mue) { +// iter.remove(); +// } +// } +// +// return (MidiDevice.Info[]) infos.toArray( +// new MidiDevice.Info[infos.size()]); +// } +// +// /** +// * Attempt to use the alternate midi device for output. +// * Return true if we're using it. +// */ +// public boolean useAlternateDevice (MidiDevice.Info devinfo) +// { +// Log.info("Trying alternate device: " + devinfo); +// try { +// MidiDevice dev = MidiSystem.getMidiDevice(devinfo); +// Receiver rec = dev.getReceiver(); +// if (rec == null) { +// Log.info("Got no device!"); +// return false; +// } +// _stoppingSong = true; +// _sequencer.stop(); +// _sequencer.close(); +// +// Receiver old = _sequencer.getTransmitter().getReceiver(); +// Log.info("Old receiver: " + old); +// if (old != null) { +// old.close(); +// } +// _sequencer.open(); +// +// // THIS DOESN'T WORK. +// // See bug #4347135, specifically notes on the bottom. +// _sequencer.getTransmitter().setReceiver(rec); +// playTopSong(); +// +// // possibly shut down an old receiver +// if (_receiver != null) { +// _receiver.close(); +// } +// // set the new receiver +// _receiver = rec; +// +// return true; +// +// } catch (MidiUnavailableException mue) { +// Log.warning("Use of alternate device failed [e=" + mue + +// ", device=" + devinfo + "]."); +// return false; +// } +// } + + /** This is apparently the midi code for end of track. Wack. */ + protected static final int END_OF_TRACK = 47; + + /** The midi control for volume is 7. Ooooooo. */ + protected static final int VOLUME_CONTROL = 7; + + /** The sequencer. */ + protected Sequencer _sequencer; + + /** The channels in the sequencer, which we'll use to fuxor volumes. */ + protected MidiChannel[] _channels; +} diff --git a/src/java/com/threerings/media/sound/ModPlayer.java b/src/java/com/threerings/media/sound/ModPlayer.java new file mode 100644 index 00000000..af06330f --- /dev/null +++ b/src/java/com/threerings/media/sound/ModPlayer.java @@ -0,0 +1,135 @@ +// +// $Id: ModPlayer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sound; + +import java.io.DataInputStream; +import java.io.InputStream; + +import micromod.MicroMod; +import micromod.Module; +import micromod.ModuleLoader; +import micromod.output.JavaSoundOutputDevice; +import micromod.output.OutputDeviceException; +import micromod.output.converters.SS16LEAudioFormatConverter; +import micromod.resamplers.LinearResampler; + +/** + * A player that plays .mod format music. + */ +public class ModPlayer extends MusicPlayer +{ + // documentation inherited + public void init () + throws Exception + { + _device = new NaryaSoundDevice(); + _device.start(); + } + + // documentation inherited + public void shutdown () + { + _device.stop(); + } + + // documentation inherited + public void start (InputStream stream) + throws Exception + { + Module module = ModuleLoader.read(new DataInputStream(stream)); + + final MicroMod mod = new MicroMod( + module, _device, new LinearResampler()); + + _player = new Thread("narya mod player") { + public void run () { + + while (mod.getSequenceLoopCount() == 0) { + + mod.doRealTimePlayback(); + try { + Thread.sleep(20); + } catch (InterruptedException ie) { + // WFCares + } + + if (_player != Thread.currentThread()) { + // we were stopped! + return; + } + } + _device.drain(); + _musicListener.musicStopped(); + } + }; + + _player.setDaemon(true); + _player.start(); + } + + // documentation inherited + public void stop () + { + _player = null; + } + + // documentation inherited + public void setVolume (float vol) + { + _device.setVolume(vol); + } + + /** + * A class that allows us to access the dataline so we can adjust + * the volume. + */ + protected static class NaryaSoundDevice extends JavaSoundOutputDevice + { + public NaryaSoundDevice () + throws OutputDeviceException + { + super(new SS16LEAudioFormatConverter(), 44100, 1000); + } + + /** + * Adjust the volume of the line that we're sending our mod data to. + */ + public void setVolume (float vol) + { + SoundManager.adjustVolume(sourceDataLine, vol); + } + + /** + * Access the drain method of the line. + */ + public void drain () + { + sourceDataLine.drain(); + } + } + + /** The thread that does the work. */ + protected Thread _player; + + /** The sound output device. */ + protected NaryaSoundDevice _device; +} diff --git a/src/java/com/threerings/media/sound/Mp3Player.java b/src/java/com/threerings/media/sound/Mp3Player.java new file mode 100644 index 00000000..8765faa5 --- /dev/null +++ b/src/java/com/threerings/media/sound/Mp3Player.java @@ -0,0 +1,148 @@ +// +// $Id: Mp3Player.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sound; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; + +import com.threerings.media.Log; + +/** + * Plays mp3 files. Depends on three external jar files that aren't even + * imported here: + * tritonus_share.jar + * tritonus_mp3.jar + * javalayer.jar + */ +public class Mp3Player extends MusicPlayer +{ + // documentation inherited + public void init () + { + // TODO: some stuff needs to move here, like setting up the line + // but we don't yet know the audio format, so I need to figure that + // out (the format might always be known..). + } + + // documentation inherited + public void shutdown () + { + } + + // documentation inherited + public void start (final InputStream stream) + throws Exception + { + // TODO: some stuff needs to come out of here and into init/shutdown + // but we'll deal with all that later, d00d. + _player = new Thread("narya mp3 relay") { + public void run () { + AudioInputStream inStream = null; + try { + inStream = AudioSystem.getAudioInputStream( + new BufferedInputStream(stream, BUFFER_SIZE)); + } catch (Exception e) { + Log.warning("MP3 fuckola. [e=" + e + "]."); + return; + } + + AudioFormat sourceFormat = inStream.getFormat(); + AudioFormat.Encoding targetEnc = + AudioFormat.Encoding.PCM_SIGNED; + + inStream = AudioSystem.getAudioInputStream( + targetEnc, inStream); + AudioFormat format = inStream.getFormat(); + + DataLine.Info info = new DataLine.Info( + SourceDataLine.class, format); + + try { + _line = (SourceDataLine) AudioSystem.getLine(info); + _line.open(format); + } catch (LineUnavailableException lue) { + Log.warning("MP3 line unavailable: " + lue); + return; + } + + _line.start(); + + byte[] data = new byte[BUFFER_SIZE]; + int count = 0; + while (count >= 0) { + try { + count = inStream.read(data, 0, data.length); + } catch (IOException ioe) { + Log.warning("Error reading MP3: " + ioe); + break; + } + if (count >= 0) { + _line.write(data, 0, count); + } + if (_player != Thread.currentThread()) { + return; + } + } + + _line.drain(); + _line.close(); + _musicListener.musicStopped(); + } + }; + + _player.setDaemon(true); + //_player.setPriority(_player.getPriority() + 1); + _player.start(); + } + + // documentation inherited + public void stop () + { + _player = null; + } + + // documentation inherited + public void setVolume (float volume) + { + // TODO : line won't be null when we initialize it in the right place + if (_line != null) { + SoundManager.adjustVolume(_line, volume); + } + } + + /** The thread that transfers data to the line. */ + protected Thread _player; + + /** The line that we play through. */ + protected SourceDataLine _line; + + /** The size of our buffer. */ + protected static final int BUFFER_SIZE = 8192; +} diff --git a/src/java/com/threerings/media/sound/MusicManager.java b/src/java/com/threerings/media/sound/MusicManager.java new file mode 100644 index 00000000..caa03ff9 --- /dev/null +++ b/src/java/com/threerings/media/sound/MusicManager.java @@ -0,0 +1,353 @@ +// +// $Id: SoundManager.java 3290 2004-12-29 21:56:58Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sound; + +import java.util.Iterator; +import java.util.LinkedList; + +import com.samskivert.util.Config; +import com.samskivert.util.RandomUtil; +import com.samskivert.util.RunQueue; + +import com.threerings.media.Log; + +/** + * Manages the playing of audio files. + */ +// TODO: +// - fade music out when stopped? +// - be able to pause music +public class MusicManager +{ + /** + * Constructs a music manager. + * + * @param smgr The soundManager we work with. + * @param runQueue the client event run queue. + * + */ + public MusicManager (SoundManager smgr, RunQueue runQueue) + { + _smgr = smgr; + _runQueue = runQueue; + } + + /** + * Shut the damn thing off. + */ + public void shutdown () + { + _musicStack.clear(); + stopMusicPlayer(); + } + + /** + * Returns a string summarizing our volume settings and disabled sound + * types. + */ + public String summarizeState () + { + StringBuilder buf = new StringBuilder(); + buf.append("musicVol=").append(_musicVol); + return buf.append("]").toString(); + } + + /** + * Sets the volume for music. + * + * @param vol a volume parameter between 0f and 1f, inclusive. + */ + public void setMusicVolume (float vol) + { + float oldvol = _musicVol; + _musicVol = Math.max(0f, Math.min(1f, vol)); + if (_musicPlayer != null) { + _musicPlayer.setVolume(_musicVol); + } + + if ((oldvol == 0f) && (_musicVol != 0f)) { + playTopMusic(); + } else if ((oldvol != 0f) && (_musicVol == 0f)) { + stopMusicPlayer(); + } + } + + /** + * Get the music volume. + */ + public float getMusicVolume () + { + return _musicVol; + } + + /** + * Start playing the specified music repeatedly. + */ + public void pushMusic (String pkgPath, String key) + { + pushMusic(pkgPath, key, -1); + } + + /** + * Start playing music for the specified number of loops. If no other + * music is pushed, the specified music will play for the number of loops + * specified, and will then be popped off the stack. + */ + public void pushMusic (String pkgPath, String key, int numloops) + { + MusicKey mkey = new MusicKey(pkgPath, key, numloops); + + // stop any existing playing music + if (_musicPlayer != null) { + _musicPlayer.stop(); + handleMusicStopped(); + } + + // add the new song + _musicStack.addFirst(mkey); + + // and play it + playTopMusic(); + } + + /** + * Remove the specified music from the playlist. If it is currently + * playing, it will be stopped and the previous song will be started. + */ + public void removeMusic (String pkgPath, String key) + { + MusicKey mkey = new MusicKey(pkgPath, key, -1); + + if (!_musicStack.isEmpty()) { + MusicKey current = (MusicKey) _musicStack.getFirst(); + + // if we're currently playing this song.. + if (mkey.equals(current)) { + // stop it + if (_musicPlayer != null) { + _musicPlayer.stop(); + } + + // remove it from the stack + _musicStack.removeFirst(); + // start playing the next.. + playTopMusic(); + return; + + } else { + // we aren't currently playing this song. Simply remove. + for (Iterator iter=_musicStack.iterator(); iter.hasNext(); ) { + if (key.equals(iter.next())) { + iter.remove(); + return; + } + } + + } + } + + Log.debug("Sequence stopped that wasn't in the stack anymore " + + "[key=" + mkey + "]."); + } + + /** + * Start the specified sequence. + */ + protected void playTopMusic () + { + if (_musicStack.isEmpty()) { + return; + } + + // if the volume is off, we don't actually want to play anything + // but we want to at least decrement any loopers by one + // and keep them on the top of the queue + if (_musicVol == 0f) { + handleMusicStopped(); + return; + } + + MusicKey info = (MusicKey) _musicStack.getFirst(); + + Config c = _smgr.getConfig(info); + String[] names = c.getValue(info.key, (String[])null); + if ((names == null) || (names.length == 0)) { + Log.warning("No such music [key=" + info + "]."); + _musicStack.removeFirst(); + playTopMusic(); + return; + } + String music = names[RandomUtil.getInt(names.length)]; + + Class playerClass = getMusicPlayerClass(music); + + // if we don't have a player for this song, play the next song + if (playerClass == null) { + _musicStack.removeFirst(); + playTopMusic(); + return; + } + + // shutdown the old player if we're switching music types + if (! playerClass.isInstance(_musicPlayer)) { + if (_musicPlayer != null) { + _musicPlayer.shutdown(); + } + + // set up the new player + try { + _musicPlayer = (MusicPlayer) playerClass.newInstance(); + _musicPlayer.init(_playerListener); + + } catch (Exception e) { + Log.warning("Unable to instantiate music player [class=" + + playerClass + ", e=" + e + "]."); + + // scrap it, try again with the next song + _musicPlayer = null; + _musicStack.removeFirst(); + playTopMusic(); + return; + } + + _musicPlayer.setVolume(_musicVol); + } + + // play! + String bundle = c.getValue("bundle", (String)null); + try { + // TODO: buffer for the music player? + _musicPlayer.start(_smgr._rmgr.getResource(bundle, music)); + } catch (Exception e) { + Log.warning("Error playing music, skipping [e=" + e + + ", bundle=" + bundle + ", music=" + music + "]."); + _musicStack.removeFirst(); + playTopMusic(); + return; + } + } + + /** + * Get the appropriate music player for the specified music file. + */ + protected static Class getMusicPlayerClass (String path) + { + path = path.toLowerCase(); + +// if (path.endsWith(".mid") || path.endsWith(".rmf")) { +// return MidiPlayer.class; + +// } else if (path.endsWith(".mod")) { +// return ModPlayer.class; + +// } else if (path.endsWith(".mp3")) { +// return Mp3Player.class; + +// } else if (path.endsWith(".ogg")) { +// return OggPlayer.class; + +// } else { + return null; +// } + } + + /** + * Stop whatever song is currently playing and deal with the + * MusicKey associated with it. + */ + protected void handleMusicStopped () + { + if (_musicStack.isEmpty()) { + return; + } + + // see what was playing + MusicKey current = (MusicKey) _musicStack.getFirst(); + + // see how many times the song was to loop and act accordingly + switch (current.loops) { + default: + current.loops--; + break; + + case 1: + // sorry charlie + _musicStack.removeFirst(); + break; + } + } + + /** + * Stop the current music player. + */ + protected void stopMusicPlayer () + { + if (_musicPlayer != null) { + _musicPlayer.stop(); + _musicPlayer.shutdown(); + _musicPlayer = null; + } + } + + /** + * A class that tracks the information about our playing music files. + */ + protected static class MusicKey extends SoundManager.SoundKey + { + /** How many times to loop, or -1 for forever. */ + public int loops; + + public MusicKey (String set, String path, int loops) + { + super((byte) -1, set, path); + this.loops = loops; + } + } + + /** The sound manager we work with. */ + protected SoundManager _smgr; + + /** The client event run queue. */ + protected RunQueue _runQueue; + + /** Volume level for music. */ + protected float _musicVol = 1f; + + /** The stack of songs that we're playing. */ + protected LinkedList _musicStack = new LinkedList(); + + /** The current music player, if any. */ + protected MusicPlayer _musicPlayer; + + /** Event listener for receiving information about a song ending. */ + protected MusicPlayer.MusicEventListener _playerListener = + new MusicPlayer.MusicEventListener() { + public void musicStopped () { + _runQueue.postRunnable(new Runnable() { + public void run() { + handleMusicStopped(); + playTopMusic(); + } + }); + } + }; +} diff --git a/src/java/com/threerings/media/sound/MusicPlayer.java b/src/java/com/threerings/media/sound/MusicPlayer.java new file mode 100644 index 00000000..b42c2a80 --- /dev/null +++ b/src/java/com/threerings/media/sound/MusicPlayer.java @@ -0,0 +1,89 @@ +// +// $Id: MusicPlayer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sound; + +import java.io.InputStream; + +/** + * Abstract music player. + */ +public abstract class MusicPlayer +{ + /** + * A watcher interested in music events. + */ + public interface MusicEventListener + { + /** + * A callback that all players should use when the song they are + * playing has finished playing (completely). + */ + public void musicStopped(); + } + + /** + * Initialize the music player. + */ + public final void init (MusicEventListener musicListener) + throws Exception + { + _musicListener = musicListener; + + init(); + } + + /** + * Do your init here. + */ + public void init () + throws Exception + { + } + + /** + * Shutdown and free all resources. + */ + public void shutdown () + { + } + + /** + * Start playing song data from the specified stream. + */ + public abstract void start (InputStream stream) + throws Exception; + + /** + * Stop playing the specified song. + */ + public abstract void stop (); + + /** + * Set the volume. + * + * @param volume 0f - 1f, inclusive. + */ + public abstract void setVolume (float volume); + + /** Tell this guy about it when a song stops. */ + protected MusicEventListener _musicListener; +} diff --git a/src/java/com/threerings/media/sound/OggPlayer.java b/src/java/com/threerings/media/sound/OggPlayer.java new file mode 100644 index 00000000..15b33bd8 --- /dev/null +++ b/src/java/com/threerings/media/sound/OggPlayer.java @@ -0,0 +1,401 @@ +// +// $Id: OggPlayer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sound; + +import java.io.InputStream; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; + +import com.jcraft.jorbis.*; +import com.jcraft.jogg.*; + +/** + * Plays Ogg Vorbis streams. + * + * Hacked together from NASTY code from JOrbis. + */ +// TODO- this would need to be greatly cleaned up if we were serious about +// using it. +public class OggPlayer extends MusicPlayer +{ + // documentation inherited + public void start (final InputStream stream) + { + _player = new Thread("narya ogg player") { + public void run () { + playStream(stream); + } + }; + _player.setDaemon(true); + _player.start(); + } + + // documentation inherited + public void stop () + { + _player = null; + } + + static final int BUFSIZE=4096*2; + static int convsize=BUFSIZE*2; + static byte[] convbuffer=new byte[convsize]; + + SyncState oy; + StreamState os; + Page og; + Packet op; + Info vi; + Comment vc; + DspState vd; + Block vb; + + byte[] buffer=null; + int bytes=0; + + int format; + int rate=0; + int channels=0; + SourceDataLine outputLine=null; + + int frameSizeInBytes; + int bufferLengthInBytes; + + void init_jorbis(){ + oy=new SyncState(); + os=new StreamState(); + og=new Page(); + op=new Packet(); + + vi=new Info(); + vc=new Comment(); + vd=new DspState(); + vb=new Block(vd); + + buffer=null; + bytes=0; + + oy.init(); + } + + SourceDataLine getOutputLine(int channels, int rate){ + if(outputLine!=null || this.rate!=rate || this.channels!=channels){ + if(outputLine!=null){ + outputLine.drain(); + outputLine.stop(); + outputLine.close(); + } + init_audio(channels, rate); + outputLine.start(); + } + return outputLine; + } + + void init_audio(int channels, int rate){ + try { + //ClassLoader originalClassLoader=null; + //try{ + // originalClassLoader=Thread.currentThread().getContextClassLoader(); + // Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader()); + //} + //catch(Exception ee){ + // System.out.println(ee); + //} + AudioFormat audioFormat = + new AudioFormat((float)rate, + 16, + channels, + true, // PCM_Signed + false // littleEndian + ); + DataLine.Info info = + new DataLine.Info(SourceDataLine.class, + audioFormat, + AudioSystem.NOT_SPECIFIED); + if (!AudioSystem.isLineSupported(info)) { + //System.out.println("Line " + info + " not supported."); + return; + } + + try{ + outputLine = (SourceDataLine) AudioSystem.getLine(info); + //outputLine.addLineListener(this); + outputLine.open(audioFormat); + } + catch (LineUnavailableException ex) { + System.out.println("Unable to open the sourceDataLine: " + ex); + return; + } + catch (IllegalArgumentException ex) { + System.out.println("Illegal Argument: " + ex); + return; + } + + frameSizeInBytes = audioFormat.getFrameSize(); + int bufferLengthInFrames = outputLine.getBufferSize()/frameSizeInBytes/2; + bufferLengthInBytes = bufferLengthInFrames * frameSizeInBytes; + + //buffer = new byte[bufferLengthInBytes]; + //if(originalClassLoader!=null) + // Thread.currentThread().setContextClassLoader(originalClassLoader); + + this.rate=rate; + this.channels=channels; + } + catch(Exception ee){ + System.out.println(ee); + } + } + + protected void playStream (InputStream stream) + { + init_jorbis(); + + loop: + while (true) { + int eos = 0; + + int index = oy.buffer(BUFSIZE); + buffer = oy.data; + try { + bytes = stream.read(buffer, index, BUFSIZE); + } catch (Exception e) { + System.err.println(e); + return; + } + oy.wrote(bytes); + + if (oy.pageout(og) != 1) { + if (bytes < BUFSIZE) { + break; + } + System.err.println("Input does not appear to be an Ogg bitstream."); + return; + } + + os.init(og.serialno()); + os.reset(); + + vi.init(); + vc.init(); + + if (os.pagein(og) < 0) { + // error; stream version mismatch perhaps + System.err.println("Error reading first page of Ogg bitstream data."); + return; + } + + if (os.packetout(op) != 1) { + // no page? must not be vorbis + System.err.println("Error reading initial header packet."); + break; + // return; + } + + if (vi.synthesis_headerin(vc, op) < 0) { + // error case; not a vorbis header + System.err.println("This Ogg bitstream does not contain Vorbis audio data."); + return; + } + + int i=0; + + while (i < 2) { + while (i < 2) { + int result = oy.pageout(og); + if (result==0) { + break; // Need more data + } + if (result==1) { + os.pagein(og); + while (i < 2) { + result = os.packetout(op); + if (result == 0) { + break; + } + if (result == -1) { + System.err.println("Corrupt secondary header. Exiting."); + //return; + break loop; + } + vi.synthesis_headerin(vc, op); + i++; + } + } + } + + index = oy.buffer(BUFSIZE); + buffer = oy.data; + try { + bytes = stream.read(buffer, index, BUFSIZE); + } + catch(Exception e){ + System.err.println(e); + return; + } + + if (bytes == 0 && i < 2) { + System.err.println("End of file before finding all Vorbis headers!"); + return; + } + oy.wrote(bytes); + } + + convsize=BUFSIZE/vi.channels; + + vd.synthesis_init(vi); + vb.init(vd); + + double[][][] _pcm=new double[1][][]; + float[][][] _pcmf=new float[1][][]; + int[] _index=new int[vi.channels]; + + getOutputLine(vi.channels, vi.rate); + + while (eos == 0) { + while (eos == 0) { + + if (_player != Thread.currentThread()) { + //System.err.println("bye."); + try { + //outputLine.drain(); + //outputLine.stop(); + //outputLine.close(); + stream.close(); + } catch(Exception ee) { + } + return; + } + + int result = oy.pageout(og); + if (result == 0) { + break; // need more data + } + if (result == -1) { // missing or corrupt data at this page position + // System.err.println("Corrupt or missing data in bitstream; continuing..."); + + } else { + os.pagein(og); + while (true) { + result = os.packetout(op); + if (result == 0) break; // need more data + if (result == -1) { // missing or corrupt data at this page position + // no reason to complain; already complained above + } else { + // we have a packet. Decode it + int samples; + if (vb.synthesis(op) == 0) { // test for success! + vd.synthesis_blockin(vb); + } + while ((samples = + vd.synthesis_pcmout(_pcmf, _index)) > 0) { + + double[][] pcm = _pcm[0]; + float[][] pcmf = _pcmf[0]; + boolean clipflag = false; + int bout = Math.min(samples, convsize); + + // convert doubles to 16 bit signed ints (host order) and + // interleave + for (i = 0; i < vi.channels; i++) { + int ptr = i*2; + //int ptr=i; + int mono = _index[i]; + for (int j = 0; j < bout; j++) { + int val = (int) + (pcmf[i][mono+j] * 32767.); + if (val > 32767){ + val = 32767; + clipflag = true; + + } else if (val < -32768) { + val = -32768; + clipflag = true; + } + if (val < 0) { + val = val | 0x8000; + } + convbuffer[ptr] = (byte)(val); + convbuffer[ptr+1] = (byte)(val>>>8); + ptr += 2 * (vi.channels); + } + } + outputLine.write(convbuffer, 0, + 2 * vi.channels * bout); + vd.synthesis_read(bout); + } + } + } + if (og.eos() != 0) { + eos=1; + } + } + } + + if (eos==0) { + index = oy.buffer(BUFSIZE); + buffer = oy.data; + try { + bytes = stream.read(buffer,index,BUFSIZE); + + } catch (Exception e) { + System.err.println(e); + return; + } + if (bytes == -1) { + break; + } + oy.wrote(bytes); + if (bytes==0) { + eos=1; + } + } + } + + os.clear(); + vb.clear(); + vd.clear(); + vi.clear(); + } + + oy.clear(); + + //System.err.println("Done."); + + try { + if (stream != null) { + stream.close(); + } + } catch (Exception e) { + } + } + + public void setVolume (float volume) + { + // TODO + } + + protected Thread _player; +} diff --git a/src/java/com/threerings/media/sound/SoundCodes.java b/src/java/com/threerings/media/sound/SoundCodes.java new file mode 100644 index 00000000..ef88ca05 --- /dev/null +++ b/src/java/com/threerings/media/sound/SoundCodes.java @@ -0,0 +1,57 @@ +// +// $Id: SoundCodes.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sound; + +import com.threerings.media.sound.SoundManager.SoundType; + +/** + * A basic set of sound types. + */ +public interface SoundCodes +{ + /** + * Alert sounds are the type of sounds a player would hear when + * getting a puzzle challenge. + */ + public static final SoundType ALERT = new SoundType("alert"); + + /** + * Feedback sounds are the type of sounds a player would here when + * clicking on buttons or performing an action. + */ + public static final SoundType FEEDBACK = new SoundType("feedback"); + + /** + * Ambient sounds are birds chirping, waves lapping, boats creaking. + */ + public static final SoundType AMBIENT = new SoundType("ambient"); + + /** + * Game alert sounds are used to indicate that it's a player's turn. + */ + public static final SoundType GAME_ALERT = new SoundType("game_alert"); + + /** + * General game sound effects. + */ + public static final SoundType GAME_FX =new SoundType("game_fx"); +} diff --git a/src/java/com/threerings/media/sound/SoundManager.java b/src/java/com/threerings/media/sound/SoundManager.java new file mode 100644 index 00000000..43fc626d --- /dev/null +++ b/src/java/com/threerings/media/sound/SoundManager.java @@ -0,0 +1,1044 @@ +// +// $Id: SoundManager.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sound; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Properties; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.FloatControl; +import javax.sound.sampled.Line; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; +import javax.sound.sampled.UnsupportedAudioFileException; + +import org.apache.commons.io.IOUtils; + +import com.samskivert.swing.RuntimeAdjust; +import com.samskivert.util.Config; +import com.samskivert.util.ConfigUtil; +import com.samskivert.util.Interval; +import com.samskivert.util.LRUHashMap; +import com.samskivert.util.Queue; +import com.samskivert.util.RandomUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.resource.ResourceManager; + +import com.threerings.media.Log; +import com.threerings.media.MediaPrefs; + +/** + * Manages the playing of audio files. + */ +public class SoundManager +{ + /** A pan value indicating that a sound should play from the left only. */ + public static final float PAN_LEFT = -1f; + + /** A pan value indicating that a sound should play from the right only. */ + public static final float PAN_RIGHT = 1f; + + /** A pan value indicating that a sound should play from center. */ + public static final float PAN_CENTER = 0f; + + /** + * Create instances of this for your application to differentiate + * between different types of sounds. + */ + public static class SoundType + { + /** + * Construct a new SoundType. + * Which should be a static variable stashed somewhere for the + * entire application to share. + * + * @param strname a short string identifier, preferably without spaces. + */ + public SoundType (String strname) + { + _strname = strname; + } + + public String toString () + { + return _strname; + } + + protected String _strname; + } + + /** + * A control for sounds. + */ + public static interface Frob + { + /** + * Stop playing or looping the sound. + * At present, the granularity of this command is limited to the + * buffer size of the line spooler, or about 8k of data. Thus, + * if playing an 11khz sample, it could take 8/11ths of a second + * for the sound to actually stop playing. + */ + public void stop (); + + /** + * Set the volume of the sound. + */ + public void setVolume (float vol); + + /** + * Get the volume of this sound. + */ + public float getVolume (); + + /** + * Set the pan value for the sound. Valid values are + * -1 for left-only, 0 is centered, all the way to +1 for right-only. + */ + public void setPan (float pan); + + /** + * Get the pan value of this sound. + */ + public float getPan (); + } + + /** The default sound type. */ + public static final SoundType DEFAULT = new SoundType("default"); + + /** + * Constructs a sound manager. + */ + public SoundManager (ResourceManager rmgr) + { + this(rmgr, null, null); + } + + /** + * Constructs a sound manager. + * + * @param defaultClipBundle + * @param defaultClipPath The pathname of a sound clip to use as a + * fallback if another sound clip cannot be located. + */ + public SoundManager ( + ResourceManager rmgr, String defaultClipBundle, String defaultClipPath) + { + // save things off + _rmgr = rmgr; + _defaultClipBundle = defaultClipBundle; + _defaultClipPath = defaultClipPath; + } + + /** + * Shut the damn thing off. + */ + public void shutdown () + { + // TODO: we need to stop any looping sounds + synchronized (_queue) { + _queue.clear(); + if (_spoolerCount > 0) { + _queue.append(new SoundKey(DIE)); // signal death + } + } + synchronized (_clipCache) { + _lockedClips.clear(); + _configs.clear(); + } + } + + /** + * Returns a string summarizing our volume settings and disabled sound + * types. + */ + public String summarizeState () + { + StringBuilder buf = new StringBuilder(); + buf.append("clipVol=").append(_clipVol); + buf.append(", disabled=["); + int ii = 0; + for (Iterator iter = _disabledTypes.iterator(); iter.hasNext(); ) { + if (ii++ > 0) { + buf.append(", "); + } + buf.append(iter.next()); + } + return buf.append("]").toString(); + } + + /** + * Is the specified soundtype enabled? + */ + public boolean isEnabled (SoundType type) + { + // by default, types are enabled.. + return (!_disabledTypes.contains(type)); + } + + /** + * Turns on or off the specified sound type. + */ + public void setEnabled (SoundType type, boolean enabled) + { + if (enabled) { + _disabledTypes.remove(type); + } else { + _disabledTypes.add(type); + } + } + + /** + * Sets the volume for all sound clips. + * + * @param vol a volume parameter between 0f and 1f, inclusive. + */ + public void setClipVolume (float vol) + { + _clipVol = Math.max(0f, Math.min(1f, vol)); + } + + /** + * Get the volume for all sound clips. + */ + public float getClipVolume () + { + return _clipVol; + } + + /** + * Optionally lock the sound data prior to playing, to guarantee + * that it will be quickly available for playing. + */ + public void lock (String pkgPath, String key) + { + enqueue(new SoundKey(LOCK, pkgPath, key), true); + } + + /** + * Unlock the specified sound so that its resources can be freed. + */ + public void unlock (String pkgPath, String key) + { + enqueue(new SoundKey(UNLOCK, pkgPath, key), true); + } + + /** + * Batch lock a list of sounds. + */ + public void lock (String pkgPath, String[] keys) + { + for (int ii=0; ii < keys.length; ii++) { + enqueue(new SoundKey(LOCK, pkgPath, keys[ii]), (ii == 0)); + } + } + + /** + * Batch unlock a list of sounds. + */ + public void unlock (String pkgPath, String[] keys) + { + for (int ii=0; ii < keys.length; ii++) { + enqueue(new SoundKey(UNLOCK, pkgPath, keys[ii]), (ii == 0)); + } + } + + /** + * Play the specified sound as the specified type of sound, immediately. + * Note that a sound need not be locked prior to playing. + */ + public void play (SoundType type, String pkgPath, String key) + { + play(type, pkgPath, key, 0, PAN_CENTER); + } + + /** + * Play the specified sound as the specified type of sound, immediately, + * with the specified pan value. + * Note that a sound need not be locked prior to playing. + * + * @param pan a value from -1f (all left) to +1f (all right). + */ + public void play (SoundType type, String pkgPath, String key, float pan) + { + play(type, pkgPath, key, 0, pan); + } + + /** + * Play the specified sound after the specified delay. + * @param delay the delay in milliseconds. + */ + public void play (SoundType type, String pkgPath, String key, int delay) + { + play(type, pkgPath, key, delay, PAN_CENTER); + } + + /** + * Play the specified sound after the specified delay. + * @param delay the delay in milliseconds. + * @param pan a value from -1f (all left) to +1f (all right). + */ + public void play ( + SoundType type, String pkgPath, String key, int delay, float pan) + { + if (type == null) { + type = DEFAULT; // let the lazy kids play too + } + + if ((_clipVol != 0f) && isEnabled(type)) { + final SoundKey skey = new SoundKey(PLAY, pkgPath, key, delay, + _clipVol, pan); + if (delay > 0) { + new Interval() { + public void expired () { + addToPlayQueue(skey); + } + }.schedule(delay); + } else { + addToPlayQueue(skey); + } + } + } + + /** + * Loop the specified sound. + */ + public Frob loop (SoundType type, String pkgPath, String key) + { + return loop(type, pkgPath, key, PAN_CENTER); + } + + /** + * Loop the specified sound. + */ + public Frob loop (SoundType type, String pkgPath, String key, float pan) + { + if (type == null) { + type = DEFAULT; + } + + if (!isEnabled(type)) { + return null; + } + SoundKey skey = new SoundKey(LOOP, pkgPath, key, 0, _clipVol, pan); + addToPlayQueue(skey); + return skey; // it is a frob + } + + // ==== End of public methods ==== + + /** + * Add the sound clip key to the queue to be played. + */ + protected void addToPlayQueue (SoundKey skey) + { + boolean queued = enqueue(skey, true); + if (queued) { + if (_verbose.getValue()) { + Log.info("Sound request [key=" + skey.key + "]."); + } + + } else /* if (_verbose.getValue()) */ { + Log.warning("SoundManager not playing sound because " + + "too many sounds in queue [key=" + skey + "]."); + } + } + + /** + * Enqueue a new SoundKey. + */ + protected boolean enqueue (SoundKey key, boolean okToStartNew) + { + boolean add; + boolean queued; + synchronized (_queue) { + if (key.cmd == PLAY && _queue.size() > MAX_QUEUE_SIZE) { + queued = add = false; + } else { + _queue.appendLoud(key); + queued = true; + add = okToStartNew && (_freeSpoolers == 0) && + (_spoolerCount < MAX_SPOOLERS); + if (add) { + _spoolerCount++; + } + } + } + + // and if we need a new thread, add it + if (add) { + Thread spooler = new Thread("narya SoundManager line spooler") { + public void run () { + spoolerRun(); + } + }; + spooler.setDaemon(true); + spooler.start(); + } + + return queued; + } + + /** + * This is the primary run method of the sound-playing threads. + */ + protected void spoolerRun () + { + while (true) { + try { + SoundKey key; + synchronized (_queue) { + _freeSpoolers++; + key = (SoundKey) _queue.get(MAX_WAIT_TIME); + _freeSpoolers--; + + if (key == null || key.cmd == DIE) { + _spoolerCount--; + // if dieing and there are others to kill, do so + if (key != null && _spoolerCount > 0) { + _queue.appendLoud(key); + } + return; + } + } + + // process the command + processKey(key); + + } catch (Exception e) { + Log.logStackTrace(e); + } + } + } + + /** + * Process the requested command in the specified SoundKey. + */ + protected void processKey (SoundKey key) + throws Exception + { + switch (key.cmd) { + case PLAY: + case LOOP: + playSound(key); + break; + + case LOCK: + if (!isTesting()) { + synchronized (_clipCache) { + try { + getClipData(key); // preload + // copy cached to lock map + _lockedClips.put(key, _clipCache.get(key)); + } catch (Exception e) { + // don't whine about LOCK failures unless + // we are verbosely logging + if (_verbose.getValue()) { + throw e; + } + } + } + } + break; + + case UNLOCK: + synchronized (_clipCache) { + _lockedClips.remove(key); + } + break; + } + } + + /** + * On a spooling thread, + */ + protected void playSound (SoundKey key) + { + if (!key.running) { + return; + } + key.thread = Thread.currentThread(); + SourceDataLine line = null; + try { + // get the sound data from our LRU cache + byte[] data = getClipData(key); + if (data == null) { + return; // borked! + + } else if (key.isExpired()) { + if (_verbose.getValue()) { + Log.info("Sound expired [key=" + key.key + "]."); + } + return; + + } + + AudioInputStream stream = AudioSystem.getAudioInputStream( + new ByteArrayInputStream(data)); + if (key.cmd == LOOP) { + stream.mark(data.length); + } + + // open the sound line + AudioFormat format = stream.getFormat(); + line = (SourceDataLine) AudioSystem.getLine( + new DataLine.Info(SourceDataLine.class, format)); + line.open(format, LINEBUF_SIZE); + float setVolume = 1; + float setPan = PAN_CENTER; + line.start(); + _soundSeemsToWork = true; + + byte[] buffer = new byte[LINEBUF_SIZE]; + do { + // play the sound + int count = 0; + while (key.running && count != -1) { + float vol = key.volume; + if (vol != setVolume) { + adjustVolume(line, vol); + setVolume = vol; + } + float pan = key.pan; + if (pan != setPan) { + adjustPan(line, pan); + setPan = pan; + } + try { + count = stream.read(buffer, 0, buffer.length); + } catch (IOException e) { + // this shouldn't ever ever happen because the stream + // we're given is from a reliable source + Log.warning("Error reading clip data! [e=" + e + "]."); + return; + } + + if (count >= 0) { + line.write(buffer, 0, count); + } + } + + // if we're going to loop, reset the stream to the beginning + if (key.cmd == LOOP) { + stream.reset(); + } + } while (key.cmd == LOOP && key.running); + + // sleep the drain time. We never trust line.drain() because + // it is buggy and locks up on natively multithreaded systems + // (linux, winXP with HT). + float sampleRate = format.getSampleRate(); + if (sampleRate == AudioSystem.NOT_SPECIFIED) { + sampleRate = 11025; // most of our sounds are + } + int sampleSize = format.getSampleSizeInBits(); + if (sampleSize == AudioSystem.NOT_SPECIFIED) { + sampleSize = 16; + } + int drainTime = (int) Math.ceil( + (LINEBUF_SIZE * 8 * 1000) / (sampleRate * sampleSize)); + + // add in a fudge factor of half a second + drainTime += 500; + + try { + Thread.sleep(drainTime); + } catch (InterruptedException ie) { + } + + } catch (IOException ioe) { + Log.warning("Error loading sound file [key=" + key + + ", e=" + ioe + "]."); + + } catch (UnsupportedAudioFileException uafe) { + Log.warning("Unsupported sound format [key=" + key + + ", e=" + uafe + "]."); + + } catch (LineUnavailableException lue) { + String err = "Line not available to play sound [key=" + key.key + + ", e=" + lue + "]."; + if (_soundSeemsToWork) { + Log.warning(err); + } else { + // this error comes every goddamned time we play a sound on + // someone with a misconfigured sound card, so let's just keep + // it to ourselves + Log.debug(err); + } + + } finally { + if (line != null) { + line.close(); + } + key.thread = null; + } + } + + /** + * @return true if we're using a test sound directory. + */ + protected boolean isTesting () + { + return !StringUtil.isBlank(_testDir.getValue()); + } + + /** + * Called by spooling threads, loads clip data from the resource + * manager or the cache. + */ + protected byte[] getClipData (SoundKey key) + throws IOException, UnsupportedAudioFileException + { + byte[][] data; + boolean verbose = _verbose.getValue(); + synchronized (_clipCache) { + // if we're testing, clear all non-locked sounds every time + if (isTesting()) { + _clipCache.clear(); + } + + data = (byte[][]) _clipCache.get(key); + + // see if it's in the locked cache (we first look in the regular + // clip cache so that locked clips that are still cached continue + // to be moved to the head of the LRU queue) + if (data == null) { + data = (byte[][]) _lockedClips.get(key); + } + + if (data == null) { + // if there is a test sound, JUST use the test sound. + InputStream stream = getTestClip(key); + if (stream != null) { + data = new byte[1][]; + data[0] = IOUtils.toByteArray(stream); + + } else { + // otherwise, randomize between all available sounds + Config c = getConfig(key); + String[] names = c.getValue(key.key, (String[])null); + if (names == null) { + Log.warning("No such sound [key=" + key + "]."); + return null; + } + + data = new byte[names.length][]; + String bundle = c.getValue("bundle", (String)null); + for (int ii=0; ii < names.length; ii++) { + data[ii] = loadClipData(bundle, names[ii]); + } + } + + _clipCache.put(key, data); + } + } + + return (data.length > 0) ? data[RandomUtil.getInt(data.length)] : null; + } + + protected InputStream getTestClip (SoundKey key) + { + String testDirectory = _testDir.getValue(); + if (StringUtil.isBlank(testDirectory)) { + return null; + } + + final String namePrefix = key.key; + File f = new File(testDirectory); + File[] list = f.listFiles(new FilenameFilter() { + public boolean accept (File f, String name) + { + if (name.startsWith(namePrefix)) { + String backhalf = name.substring(namePrefix.length()); + int dot = backhalf.indexOf('.'); + if (dot == -1) { + dot = backhalf.length(); + } + + // allow the file if the portion of the name + // after the prefix but before the extension is blank + // or a parsable integer + String extra = backhalf.substring(0, dot); + if ("".equals(extra)) { + return true; + } else { + try { + Integer.parseInt(extra); + // success! + return true; + } catch (NumberFormatException nfe) { + // not a number, we fall through... + } + } + // else fall through + } + return false; + } + }); + int size = (list == null) ? 0 : list.length; + if (size > 0) { + File pick = list[RandomUtil.getInt(size)]; + try { + return new FileInputStream(pick); + } catch (Exception e) { + Log.warning("Error reading test sound [e=" + e + ", file=" + + pick + "]."); + } + } + return null; + } + + /** + * Read the data from the resource manager. + */ + protected byte[] loadClipData (String bundle, String path) + throws IOException + { + InputStream clipin = null; + try { + clipin = _rmgr.getResource(bundle, path); + } catch (FileNotFoundException fnfe) { + // try from the classpath + try { + clipin = _rmgr.getResource(path); + } catch (FileNotFoundException fnfe2) { + // only play the default sound if we have verbose sound + // debuggin turned on. + if (_verbose.getValue()) { + Log.warning("Could not locate sound data [bundle=" + + bundle + ", path=" + path + "]."); + if (_defaultClipPath != null) { + try { + clipin = _rmgr.getResource( + _defaultClipBundle, _defaultClipPath); + } catch (FileNotFoundException fnfe3) { + try { + clipin = _rmgr.getResource(_defaultClipPath); + } catch (FileNotFoundException fnfe4) { + Log.warning("Additionally, the default " + + "fallback sound could not be located " + + "[bundle=" + _defaultClipBundle + + ", path=" + _defaultClipPath + "]."); + } + } + } else { + Log.warning("No fallback default sound specified!"); + } + } + // if we couldn't load the default, rethrow + if (clipin == null) { + throw fnfe2; + } + } + } + + return IOUtils.toByteArray(clipin); + } + + /** + * Get the cached Config. + */ + protected Config getConfig (SoundKey key) + { + Config c = (Config) _configs.get(key.pkgPath); + if (c == null) { + String propPath = key.pkgPath + Sounds.PROP_NAME; + Properties props = new Properties(); + try { + props = ConfigUtil.loadInheritedProperties( + propPath + ".properties", _rmgr.getClassLoader()); + } catch (IOException ioe) { + Log.warning("Failed to load sound properties " + + "[path=" + propPath + ", error=" + ioe + "]."); + } + c = new Config(propPath, props); + _configs.put(key.pkgPath, c); + } + return c; + } + +// /** +// * Adjust the volume of this clip. +// */ +// protected static void adjustVolumeIdeally (Line line, float volume) +// { +// if (line.isControlSupported(FloatControl.Type.VOLUME)) { +// FloatControl vol = (FloatControl) +// line.getControl(FloatControl.Type.VOLUME); +// +// float min = vol.getMinimum(); +// float max = vol.getMaximum(); +// +// float ourval = (volume * (max - min)) + min; +// Log.debug("adjust vol: [min=" + min + ", ourval=" + ourval + +// ", max=" + max + "]."); +// vol.setValue(ourval); +// +// } else { +// // fall back +// adjustVolume(line, volume); +// } +// } + + /** + * Use the gain control to implement volume. + */ + protected static void adjustVolume (Line line, float vol) + { + FloatControl control = (FloatControl) + line.getControl(FloatControl.Type.MASTER_GAIN); + + // the only problem is that gain is specified in decibals, + // which is a logarithmic scale. + // Since we want max volume to leave the sample unchanged, our + // maximum volume translates into a 0db gain. + float gain; + if (vol == 0f) { + gain = control.getMinimum(); + } else { + gain = (float) ((Math.log(vol) / Math.log(10.0)) * 20.0); + } + + control.setValue(gain); + //Log.info("Set gain: " + gain); + } + + /** + * Set the pan value for the specified line. + */ + protected static void adjustPan (Line line, float pan) + { + try { + FloatControl control = + (FloatControl) line.getControl(FloatControl.Type.PAN); + control.setValue(pan); + } catch (Exception e) { + Log.debug("Cannot set pan on line: " + e); + } + } + + /** + * A key for tracking sounds. + */ + protected static class SoundKey + implements Frob + { + public byte cmd; + + public String pkgPath; + + public String key; + + public long stamp; + + /** Should we still be running? */ + public volatile boolean running = true; + + public volatile float volume; + + /** The pan, or 0 to center the sound. */ + public volatile float pan; + + /** The player thread, if it's playing us. */ + public Thread thread; + + /** + * Create a SoundKey that just contains the specified command. + * DIE. + */ + public SoundKey (byte cmd) + { + this.cmd = cmd; + } + + /** + * Quicky constructor for music keys and lock operations. + */ + public SoundKey (byte cmd, String pkgPath, String key) + { + this(cmd); + this.pkgPath = pkgPath; + this.key = key; + } + + /** + * Constructor for a sound effect soundkey. + */ + public SoundKey (byte cmd, String pkgPath, String key, int delay, + float volume, float pan) + { + this(cmd, pkgPath, key); + + stamp = System.currentTimeMillis() + delay; + setVolume(volume); + setPan(pan); + } + + // documentation inherited from interface Frob + public void stop () + { + running = false; + Thread t = thread; + if (t != null) { + // doesn't actually ever seem to do much + t.interrupt(); + } + } + + // documentation inherited from interface Frob + public void setVolume (float vol) + { + volume = Math.max(0f, Math.min(1f, vol)); + } + + // documentation inherited from interface Frob + public float getVolume () + { + return volume; + } + + // documentation inherited from interface Frob + public void setPan (float newPan) + { + pan = Math.max(PAN_LEFT, Math.min(PAN_RIGHT, newPan)); + } + + // documentation inherited from interface Frob + public float getPan () + { + return pan; + } + + /** + * Has this sound key expired. + */ + public boolean isExpired () + { + return (stamp + MAX_SOUND_DELAY < System.currentTimeMillis()); + } + + // documentation inherited + public String toString () + { + return "SoundKey{cmd=" + cmd + ", pkgPath=" + pkgPath + + ", key=" + key + "}"; + } + + // documentation inherited + public int hashCode () + { + return pkgPath.hashCode() ^ key.hashCode(); + } + + // documentation inherited + public boolean equals (Object o) + { + if (o instanceof SoundKey) { + SoundKey that = (SoundKey) o; + return this.pkgPath.equals(that.pkgPath) && + this.key.equals(that.key); + } + return false; + } + } + + /** The path of the default sound to use for missing sounds. */ + protected String _defaultClipBundle, _defaultClipPath; + + /** The resource manager from which we obtain audio files. */ + protected ResourceManager _rmgr; + + /** The queue of sound clips to be played. */ + protected Queue _queue = new Queue(); + + /** The number of currently active LineSpoolers. */ + protected int _spoolerCount, _freeSpoolers; + + /** If we every play a sound successfully, this is set to true. */ + protected boolean _soundSeemsToWork = false; + + /** Volume level for sound clips. */ + protected float _clipVol = 1f; + + /** The cache of recent audio clips . */ + protected LRUHashMap _clipCache = new LRUHashMap(10); + + /** The set of locked audio clips; this is separate from the LRU so + * that locking clips doesn't booch up an otherwise normal caching + * agenda. */ + protected HashMap _lockedClips = new HashMap(); + + /** A set of soundTypes for which sound is enabled. */ + protected HashSet _disabledTypes = new HashSet(); + + /** A cache of config objects we've created. */ + protected LRUHashMap _configs = new LRUHashMap(5); + + /** Soundkey command constants. */ + protected static final byte PLAY = 0; + protected static final byte LOCK = 1; + protected static final byte UNLOCK = 2; + protected static final byte DIE = 3; + protected static final byte LOOP = 4; + + /** A pref that specifies a directory for us to get test sounds from. */ + protected static RuntimeAdjust.FileAdjust _testDir = + new RuntimeAdjust.FileAdjust( + "Test sound directory", "narya.media.sound.test_dir", + MediaPrefs.config, true, ""); + + protected static RuntimeAdjust.BooleanAdjust _verbose = + new RuntimeAdjust.BooleanAdjust( + "Verbose sound event logging", "narya.media.sound.verbose", + MediaPrefs.config, false); + + /** The queue size at which we start to ignore requests to play sounds. */ + protected static final int MAX_QUEUE_SIZE = 25; + + /** The maximum time after which we throw away a sound rather + * than play it. */ + protected static final long MAX_SOUND_DELAY = 400L; + + /** The size of the line's buffer. */ + protected static final int LINEBUF_SIZE = 8 * 1024; + + /** The maximum time a spooler will wait for a stream before + * deciding to shut down. */ + protected static final long MAX_WAIT_TIME = 30000L; + + /** The maximum number of spoolers we'll allow. This is a lot. */ + protected static final int MAX_SPOOLERS = 12; +} diff --git a/src/java/com/threerings/media/sound/Sounds.java b/src/java/com/threerings/media/sound/Sounds.java new file mode 100644 index 00000000..ecca07a7 --- /dev/null +++ b/src/java/com/threerings/media/sound/Sounds.java @@ -0,0 +1,49 @@ +// +// $Id: Sounds.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sound; + +/** + * A base class for sound repository classes. These would extend this + * class and define keys for the various sounds that are mapped in the + * properties file associated with that sound repository. + */ +public class Sounds +{ + /** The name of the sound repository configuration file. */ + public static final String PROP_NAME = "sounds"; + + /** + * Return the package path prefix of the supplied class. + * + * Generates the key for the sound repository configuration file in + * the package associated with the class. For example, if a the class + * com.threerings.happy.fun.GameSounds were supplied to + * this method, it would return + * com/threerings/happy/fun/sounds/ which would reference + * a sounds.properties file in the + * com.threerings.happy.fun package. + */ + protected static String getPackagePath (Class clazz) + { + return clazz.getPackage().getName().replace('.', '/') + "/"; + } +} diff --git a/src/java/com/threerings/media/sprite/ButtonSprite.java b/src/java/com/threerings/media/sprite/ButtonSprite.java new file mode 100644 index 00000000..e79e3c6b --- /dev/null +++ b/src/java/com/threerings/media/sprite/ButtonSprite.java @@ -0,0 +1,384 @@ +// +// $Id: ButtonSprite.java 3509 2005-04-20 17:15:36Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sprite; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Shape; + +import com.samskivert.swing.Label; +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.media.sprite.action.ArmingSprite; +import com.threerings.media.sprite.action.CommandSprite; +import com.threerings.media.sprite.action.DisableableSprite; + +/** + * A sprite that acts as a button. + */ +public class ButtonSprite extends Sprite + implements CommandSprite, ArmingSprite, DisableableSprite +{ + /** The normal, square button style. */ + public static final int NORMAL = 0; + + /** The rounded button style. */ + public static final int ROUNDED = 1; + + /** + * Constructs a button sprite. + * + * @param label the label to render on the button + * @param style the style of button to render (NORMAL or ROUNDED) + * @param backgroundColor the background color of the button + * @param alternateColor the alternate (outline) color + * @param actionCommand the button's command + * @param commandArgument the button's command argument + */ + public ButtonSprite (Label label, int style, Color backgroundColor, + Color alternateColor, String actionCommand, Object commandArgument) + { + _label = label; + _style = style; + _backgroundColor = backgroundColor; + _alternateColor = alternateColor; + _actionCommand = actionCommand; + _commandArgument = commandArgument; + } + + /** + * Constructs a button sprite. + * + * @param label the label to render on the button + * @param style the style of button to render (NORMAL or ROUNDED) + * @param arcWidth the width of the corner arcs for rounded buttons + * @param arcHeight the height of the corner arcs for rounded buttons + * @param backgroundColor the background color of the button + * @param alternateColor the alternate (outline) color + * @param actionCommand the button's command + * @param commandArgument the button's command argument + */ + public ButtonSprite (Label label, int style, int arcWidth, int arcHeight, + Color backgroundColor, Color alternateColor, String actionCommand, + Object commandArgument) + { + _label = label; + _style = style; + _arcWidth = arcWidth; + _arcHeight = arcHeight; + _backgroundColor = backgroundColor; + _alternateColor = alternateColor; + _actionCommand = actionCommand; + _commandArgument = commandArgument; + } + + /** + * Returns a reference to the label displayed by this sprite. + */ + public Label getLabel () + { + return _label; + } + + /** + * Updates this sprite's bounds after a change to the label. + */ + public void updateBounds () + { + // invalidate the old... + invalidate(); + + // size the bounds to fit our label + Dimension size = _label.getSize(); + _bounds.width = size.width + PADDING*2 + + (_style == ROUNDED ? _arcWidth : 0); + _bounds.height = size.height + PADDING*2; + + // ...and the new + invalidate(); + } + + /** + * Sets the style of this button. + */ + public void setStyle (int style) + { + _style = style; + updateBounds(); + } + + /** + * Returns the style of this button. + */ + public int getStyle () + { + return _style; + } + + /** + * Sets the arc width for rounded buttons. + */ + public void setArcWidth (int arcWidth) + { + _arcWidth = arcWidth; + updateBounds(); + } + + /** + * Returns the arc width for rounded buttons. + */ + public int getArcWidth () + { + return _arcWidth; + } + + /** + * Sets the arc height for rounded buttons. + */ + public void setArcHeight (int arcHeight) + { + _arcHeight = arcHeight; + updateBounds(); + } + + /** + * Returns the arc height for rounded buttons. + */ + public int getArcHeight () + { + return _arcHeight; + } + + /** + * Sets the background color of this button. + */ + public void setBackgroundColor (Color backgroundColor) + { + _backgroundColor = backgroundColor; + } + + /** + * Returns the background color of this button. + */ + public Color getBackgroundColor () + { + return _backgroundColor; + } + + /** + * Sets the action command generated by this button. + */ + public void setActionCommand (String actionCommand) + { + _actionCommand = actionCommand; + } + + // documentation inherited from interface CommandSprite + public String getActionCommand () + { + return _actionCommand; + } + + /** + * Sets the command argument generated by this button. + */ + public void setCommandArgument (Object commandArgument) + { + _commandArgument = commandArgument; + } + + // documentation inherited from interface CommandSprite + public Object getCommandArgument () + { + return _commandArgument; + } + + /** + * Sets whether or not this button is enabled. + */ + public void setEnabled (boolean enabled) + { + if (_enabled != enabled) { + _enabled = enabled; + invalidate(); + } + } + + // documentation inherited from interface DisableableSprite + public boolean isEnabled () + { + return _enabled; + } + + // documentation inherited from interface ArmingSprite + public void setArmed (boolean pressed) + { + if (_pressed != pressed) { + _pressed = pressed; + invalidate(); + } + } + + /** + * Checks whether or not this button appears pressed. + */ + public boolean isArmed () + { + return _pressed; + } + + // documentation inherited + protected void init () + { + super.init(); + + // lay out the label if not already + if (!_label.isLaidOut()) { + _label.layout(_mgr.getMediaPanel()); + } + + updateBounds(); + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + Color baseTextColor = _label.getTextColor(), + baseAlternateColor = _label.getAlternateColor(); + + if (!_enabled) { + _label.setTextColor(baseTextColor.darker()); + _label.setAlternateColor(baseAlternateColor.darker()); + } + + switch (_style) { + case NORMAL: + gfx.setColor(_enabled ? _backgroundColor : + _backgroundColor.darker()); + gfx.fill3DRect(_bounds.x, _bounds.y, _bounds.width, + _bounds.height, !_pressed); + _label.render(gfx, _bounds.x + (_pressed ? PADDING : + PADDING - 1), _bounds.y + (_pressed ? PADDING : + PADDING - 1)); + break; + case ROUNDED: + Object aaState = SwingUtil.activateAntiAliasing(gfx); + // draw outline + gfx.setColor(_alternateColor); + gfx.fillRoundRect(_bounds.x, _bounds.y, _bounds.width, + _bounds.height, _arcWidth, _arcHeight); + // draw foreground + gfx.setColor(_enabled ? _backgroundColor : + _backgroundColor.darker()); + int innerBoundsX = _bounds.x+1, innerBoundsY = _bounds.y+1, + innerBoundsWidth = _bounds.width-2, + innerBoundsHeight = _bounds.height-2, + innerBoundsArcWidth = _arcWidth-2, + innerBoundsArcHeight = _arcHeight-2; + gfx.fillRoundRect(innerBoundsX, innerBoundsY, + innerBoundsWidth, innerBoundsHeight, + innerBoundsArcWidth, innerBoundsArcHeight); + Color brighter = _enabled ? _backgroundColor.brighter() : + _backgroundColor, darker = _enabled ? + _backgroundColor.darker() : + _backgroundColor.darker().darker(); + // draw the upper left/lower right corners (always dark) + gfx.setColor(darker); + gfx.drawArc(innerBoundsX, innerBoundsY, innerBoundsArcWidth, + innerBoundsArcHeight, 90, 90); + gfx.drawArc(innerBoundsX + innerBoundsWidth - + innerBoundsArcWidth - 1, + innerBoundsY + innerBoundsHeight - + innerBoundsArcHeight - 1, + innerBoundsArcWidth, innerBoundsArcHeight, 270, 90); + // draw the upper right (dark when pressed) + gfx.setColor(_pressed ? darker : brighter); + gfx.drawLine(innerBoundsX + innerBoundsArcWidth/2, + innerBoundsY, innerBoundsX + innerBoundsWidth - + innerBoundsArcWidth/2, innerBoundsY); + gfx.drawArc(innerBoundsX + innerBoundsWidth - + innerBoundsArcWidth - 1, innerBoundsY, + innerBoundsArcWidth, innerBoundsArcHeight, 0, 90); + gfx.drawLine(innerBoundsX + innerBoundsWidth - 1, + innerBoundsY + innerBoundsArcHeight/2, + innerBoundsX + innerBoundsWidth - 1, innerBoundsY + + innerBoundsHeight - innerBoundsArcHeight/2); + // draw the lower left (light when pressed) + gfx.setColor(_pressed ? brighter : darker); + gfx.drawLine(innerBoundsX, innerBoundsY + + innerBoundsArcHeight/2, innerBoundsX, + innerBoundsY + innerBoundsHeight - + innerBoundsArcHeight/2); + gfx.drawArc(innerBoundsX, innerBoundsY + innerBoundsHeight - + innerBoundsArcHeight - 1, + innerBoundsArcWidth, innerBoundsArcHeight, 180, 90); + gfx.drawLine(innerBoundsX + innerBoundsArcWidth/2, + innerBoundsY + innerBoundsHeight - 1, + innerBoundsX + innerBoundsWidth - innerBoundsArcWidth/2, + innerBoundsY + innerBoundsHeight - 1); + SwingUtil.restoreAntiAliasing(gfx, aaState); + _label.render(gfx, _bounds.x + PADDING + _arcWidth/2 - + (_pressed ? 2 : 1), + _bounds.y + PADDING + (_pressed ? 1 : 0)); + break; + } + + if (!_enabled) { + _label.setTextColor(baseTextColor); + _label.setAlternateColor(baseAlternateColor); + } + } + + /** The number of pixels to add between the text and the border. */ + protected static final int PADDING = 2; + + /** The label associated with this sprite. */ + protected Label _label; + + /** The button style. */ + protected int _style; + + /** The width of the corner arcs for rounded rectangle buttons. */ + protected int _arcWidth; + + /** The height of the corner arcs for rounded rectangle buttons. */ + protected int _arcHeight; + + /** The action command generated by this button. */ + protected String _actionCommand; + + /** The command argument generated by this button. */ + protected Object _commandArgument; + + /** The background color of this sprite. */ + protected Color _backgroundColor; + + /** The alternate (outline) color of this sprite. */ + protected Color _alternateColor; + + /** Whether or not the button is currently enabled. */ + protected boolean _enabled = true; + + /** Whether or not the button is currently pressed. */ + protected boolean _pressed; +} diff --git a/src/java/com/threerings/media/sprite/FadableImageSprite.java b/src/java/com/threerings/media/sprite/FadableImageSprite.java new file mode 100644 index 00000000..6d9a5be0 --- /dev/null +++ b/src/java/com/threerings/media/sprite/FadableImageSprite.java @@ -0,0 +1,203 @@ +package com.threerings.media.sprite; + +import java.awt.AlphaComposite; +import java.awt.Graphics2D; +import java.awt.Composite; + +import com.threerings.media.util.Path; + +public class FadableImageSprite extends OrientableImageSprite +{ + /** + * Fades this sprite in over the specified duration after + * waiting for the specified delay. + */ + public void fadeIn (long delay, long duration) + { + setAlpha(0.0f); + + _fadeStamp = 0; + _fadeDelay = delay; + _fadeInDuration = duration; + } + + /** + * Puts this sprite on the specified path and fades it in over + * the specified duration. + * + * @param path the path to move along + * @param fadePortion the portion of time to spend fading in, from 0.0f + * (no time) to 1.0f (the entire time) + */ + public void moveAndFadeIn (Path path, long pathDuration, float fadePortion) + { + move(path); + + setAlpha(0.0f); + + _fadeInDuration = (long)(pathDuration*fadePortion); + } + + /** + * Puts this sprite on the specified path and fades it out over + * the specified duration. + * + * @param path the path to move along + * @param pathDuration the duration of the path + * @param fadePortion the portion of time to spend fading out, from 0.0f + * (no time) to 1.0f (the entire time) + */ + public void moveAndFadeOut (Path path, long pathDuration, float fadePortion) + { + move(path); + + setAlpha(1.0f); + + _pathDuration = pathDuration; + _fadeOutDuration = (long)(pathDuration*fadePortion); + } + + /** + * Puts this sprite on the specified path, fading it in over the specified + * duration at the beginning and fading it out at the end. + * + * @param path the path to move along + * @param pathDuration the duration of the path + * @param fadePortion the portion of time to spend fading in/out, from + * 0.0f (no time) to 1.0f (the entire time) + */ + public void moveAndFadeInAndOut (Path path, long pathDuration, + float fadePortion) + { + move(path); + + setAlpha(0.0f); + + _pathDuration = pathDuration; + _fadeInDuration = _fadeOutDuration = (long)(pathDuration*fadePortion); + } + + // Documentation inherited. + public void tick (long tickStamp) + { + super.tick(tickStamp); + + if (_fadeInDuration != -1) { + if (_path != null && (tickStamp-_pathStamp) <= _fadeInDuration) { + // fading in while moving + float alpha = (float)(tickStamp-_pathStamp)/_fadeInDuration; + if (alpha >= 1.0f) { + // fade-in complete + setAlpha(1.0f); + _fadeInDuration = -1; + + } else { + setAlpha(alpha); + } + + } else { + // fading in while stationary + if (_fadeStamp == 0) { + // store the time at which fade started + _fadeStamp = tickStamp; + } + if (tickStamp > _fadeStamp + _fadeDelay) { + // initial delay has passed + float alpha = (float)(tickStamp-_fadeStamp-_fadeDelay)/ + _fadeInDuration; + if (alpha >= 1.0f) { + // fade-in complete + setAlpha(1.0f); + _fadeInDuration = -1; + + } else { + setAlpha(alpha); + } + } + } + + } else if (_fadeOutDuration != -1 && _pathStamp+_pathDuration-tickStamp + <= _fadeOutDuration) { + // fading out while moving + float alpha = (float)(_pathStamp+_pathDuration-tickStamp)/ + _fadeOutDuration; + setAlpha(alpha); + } + } + + // Documentation inherited. + public void pathCompleted (long timestamp) + { + super.pathCompleted(timestamp); + + if (_fadeInDuration != -1) { + setAlpha(1.0f); + _fadeInDuration = -1; + + } else if (_fadeOutDuration != -1) { + setAlpha(0.0f); + _fadeOutDuration = -1; + } + } + + // Documentation inherited. + public void paint (Graphics2D gfx) + { + if (_alphaComposite.getAlpha() < 1.0f) { + Composite ocomp = gfx.getComposite(); + gfx.setComposite(_alphaComposite); + super.paint(gfx); + gfx.setComposite(ocomp); + + } else { + super.paint(gfx); + } + } + + /** + * Sets the alpha value of this sprite. + */ + public void setAlpha (float alpha) + { + if (alpha < 0.0f) { + alpha = 0.0f; + + } else if (alpha > 1.0f) { + alpha = 1.0f; + } + if (alpha != _alphaComposite.getAlpha()) { + _alphaComposite = + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha); + if (_mgr != null) { + _mgr.getRegionManager().invalidateRegion(_bounds); + } + } + } + + /** + * Returns the alpha value of this sprite. + */ + public float getAlpha () + { + return _alphaComposite.getAlpha(); + } + + /** The alpha composite. */ + protected AlphaComposite _alphaComposite = + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f); + + /** If fading in, the fade-in duration (otherwise -1). */ + protected long _fadeInDuration = -1; + + /** If fading in without moving, the fade-in delay. */ + protected long _fadeDelay; + + /** The time at which fading started. */ + protected long _fadeStamp = -1; + + /** If fading out, the fade-out duration (otherwise -1). */ + protected long _fadeOutDuration = -1; + + /** If fading out, the path duration. */ + protected long _pathDuration; +} diff --git a/src/java/com/threerings/media/sprite/ImageSprite.java b/src/java/com/threerings/media/sprite/ImageSprite.java new file mode 100644 index 00000000..a590db6c --- /dev/null +++ b/src/java/com/threerings/media/sprite/ImageSprite.java @@ -0,0 +1,331 @@ +// +// $Id: ImageSprite.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sprite; + +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.threerings.media.image.Mirage; +import com.threerings.media.util.MultiFrameImage; +import com.threerings.media.util.SingleFrameImageImpl; + +/** + * Extends the sprite class to support rendering the sprite with one or + * more frames of image animation. Overrides various methods to provide + * correspondingly desirable functionality, e.g., {@link #hitTest} only + * reports a hit if the specified point is within a non-transparent pixel + * for the sprite's current image frame. + */ +public class ImageSprite extends Sprite +{ + /** Default frame rate. */ + public static final int DEFAULT_FRAME_RATE = 15; + + /** Animation mode indicating no animation. */ + public static final int NO_ANIMATION = 0; + + /** Animation mode indicating movement cued animation. */ + public static final int MOVEMENT_CUED = 1; + + /** Animation mode indicating time based animation. */ + public static final int TIME_BASED = 2; + + /** Animation mode indicating sequential progressive animation. + * Frame 0 is guaranteed to be shown first for the full duration, and + * so on. */ + public static final int TIME_SEQUENTIAL = 3; + + /** + * Constructs an image sprite without any associated frames and with + * an invalid default initial location. The sprite should be populated + * with a set of frames used to display it via a subsequent call to + * {@link #setFrames}, and its location updated with {@link + * #setLocation}. + */ + public ImageSprite () + { + this((MultiFrameImage)null); + } + + /** + * Constructs an image sprite. + * + * @param frames the multi-frame image used to display the sprite. + */ + public ImageSprite (MultiFrameImage frames) + { + // initialize frame animation member data + _frames = frames; + _frameIdx = 0; + _animMode = NO_ANIMATION; + _frameDelay = 1000L/DEFAULT_FRAME_RATE; + } + + /** + * Constructs an image sprite that will display the supplied single + * image when rendering itself. + */ + public ImageSprite (Mirage image) + { + this(new SingleFrameImageImpl(image)); + } + + // documentation inherited + protected void init () + { + super.init(); + + // now that we have our sprite manager, we can lay ourselves out + // and initialize our frames + layout(); + } + + /** + * Returns true if the sprite's bounds contain the specified point, + * and if there is a non-transparent pixel in the sprite's image at + * the specified point, false if not. + */ + public boolean hitTest (int x, int y) + { + // first check to see that we're in the sprite's bounds and that + // we've got a frame image (if we've got no image, there's nothing + // to be hit) + if (!super.hitTest(x, y) || _frames == null) { + return false; + } + + return _frames.hitTest(_frameIdx, x - _bounds.x, y - _bounds.y); + } + + /** + * Sets the animation mode for this sprite. The available modes are: + * + *
    + *
  • TIME_BASED: cues the animation based on a target + * frame rate (specified via {@link #setFrameRate}). + *
  • MOVEMENT_CUED: ticks the animation to the next + * frame every time the sprite is moved along its path. + *
  • NO_ANIMATION: disables animation. + *
+ * + * @param mode the desired animation mode. + */ + public void setAnimationMode (int mode) + { + _animMode = mode; + } + + /** + * Sets the number of frames per second desired for the sprite + * animation. This is only used when the animation mode is + * TIME_BASED. + * + * @param fps the desired frames per second. + */ + public void setFrameRate (float fps) + { + _frameDelay = (long)(1000/fps); + } + + /** + * Set the image to be used for this sprite. + */ + public void setMirage (Mirage mirage) + { + setFrames(new SingleFrameImageImpl(mirage)); + } + + /** + * Set the image array used to render the sprite. + * + * @param frames the sprite images. + */ + public void setFrames (MultiFrameImage frames) + { + if (frames == null) { + // Log.warning("Someone set up us the null frames! " + + // "[sprite=" + this + "]."); + return; + } + + // if these are the same frames we already had, no need to do a + // bunch of pointless business + if (frames == _frames) { + return; + } + + // set and init our frames + _frames = frames; + _frameIdx = 0; + layout(); + } + + /** + * Instructs this sprite to lay out its current frame and any + * accoutrements. + */ + public void layout () + { + if (_frames != null) { + setFrameIndex(_frameIdx, true); + } + } + + /** + * Instructs the sprite to display the specified frame index. + */ + protected void setFrameIndex (int frameIdx, boolean forceUpdate) + { + // make sure we're displaying a valid frame + frameIdx = (frameIdx % _frames.getFrameCount()); + + // if this is the same frame we're already displaying and we're + // not being forced to update, we can stop now + if (frameIdx == _frameIdx && !forceUpdate) { + return; + } else { + _frameIdx = frameIdx; + } + + // start with our old bounds + Rectangle dirty = new Rectangle(_bounds); + + // determine our drawing offsets and rendered rectangle size + accomodateFrame(_frameIdx, _frames.getWidth(_frameIdx), + _frames.getHeight(_frameIdx)); + + // add our new bounds + dirty.add(_bounds); + + // give the dirty rectangle to the region manager + if (_mgr != null) { + _mgr.getRegionManager().addDirtyRegion(dirty); + } + } + + /** + * Must adjust the bounds to accomodate the our new frame. This + * includes changing the width and height to reflect the size of the + * new frame and also updating the render origin (if necessary) and + * calling {@link #updateRenderOrigin} to reflect those changes in the + * sprite's bounds. + * + * @param frameIdx the index of our new frame. + * @param width the width of the new frame. + * @param height the height of the new frame. + */ + protected void accomodateFrame (int frameIdx, int width, int height) + { + _bounds.width = width; + _bounds.height = height; + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + if (_frames != null) { +// // DEBUG: fill our background with an alpha'd rectangle +// Composite ocomp = gfx.getComposite(); +// gfx.setComposite(ALPHA_BOUNDS); +// gfx.setColor(Color.blue); +// gfx.fill(_bounds); +// gfx.setComposite(ocomp); + + // render our frame + _frames.paintFrame(gfx, _frameIdx, _bounds.x, _bounds.y); + + } else { + super.paint(gfx); + } + } + + // documentation inherited + public void tick (long timestamp) + { + // if we have no frames, we're hosulated (to use a Greenwell term) + if (_frames == null) { + return; + } + + int fcount = _frames.getFrameCount(); + boolean moved = false; + + // move the sprite along toward its destination, if any + moved = tickPath(timestamp); + + // increment the display image if performing image animation + int nfidx = _frameIdx; + switch (_animMode) { + case NO_ANIMATION: + // nothing doing + break; + + case TIME_BASED: + nfidx = (int)((timestamp/_frameDelay) % fcount); + break; + + case TIME_SEQUENTIAL: + if (_firstStamp == 0L) { + _firstStamp = timestamp; + } + nfidx = (int) (((timestamp - _firstStamp) / _frameDelay) % fcount); + break; + + case MOVEMENT_CUED: + // update the frame if the sprite moved + if (moved) { + nfidx = (_frameIdx + 1) % fcount; + } + break; + } + + // update our frame (which will do nothing if this is the same as + // our existing frame index) + setFrameIndex(nfidx, false); + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", fidx=").append(_frameIdx); + } + + /** The images used to render the sprite. */ + protected MultiFrameImage _frames; + + /** The current frame index to render. */ + protected int _frameIdx; + + /** What type of animation is desired for this sprite. */ + protected int _animMode; + + /** For how many milliseconds to display an animation frame. */ + protected long _frameDelay; + + /** The first timestamp seen (in TIME_SEQUENTIAL mode). */ + protected long _firstStamp = 0L; + +// /** DEBUG: The alpha level used when rendering our bounds. */ +// protected static final Composite ALPHA_BOUNDS = +// AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.2f); +} diff --git a/src/java/com/threerings/media/sprite/LabelSprite.java b/src/java/com/threerings/media/sprite/LabelSprite.java new file mode 100644 index 00000000..00c930d1 --- /dev/null +++ b/src/java/com/threerings/media/sprite/LabelSprite.java @@ -0,0 +1,115 @@ +// +// $Id: LabelSprite.java 3542 2005-05-06 03:01:43Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sprite; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.RenderingHints; + +import com.samskivert.swing.Label; + +/** + * A sprite that uses a label to render itself. If the label has not been + * previously laid out (see {@link Label#layout}) it will be done when the + * sprite is added to a media panel. If the label is altered after the + * sprite is created, {@link #updateBounds} should be called. + */ +public class LabelSprite extends Sprite +{ + /** + * Constructs a label sprite that renders itself with the specified + * label. + */ + public LabelSprite (Label label) + { + _label = label; + } + + /** + * Returns the label displayed by this sprite. + */ + public Label getLabel () + { + return _label; + } + + /** + * Indicates that our label should be rendered with antialiased text. + */ + public void setAntiAliased (boolean antiAliased) + { + _antiAliased = antiAliased; + } + + /** + * Updates the bounds of the sprite after a change to the label. + */ + public void updateBounds () + { + Dimension size = _label.getSize(); + _bounds.width = size.width; + _bounds.height = size.height; + } + + // documentation inherited + protected void init () + { + super.init(); + + // if our label is not yet laid out, do the deed + if (!_label.isLaidOut()) { + layoutLabel(); + } + + // size the bounds to fit our label + updateBounds(); + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + _label.render(gfx, _bounds.x, _bounds.y); + } + + /** + * Lays out our underlying label which must be done if the text is + * changed. + */ + protected void layoutLabel () + { + Graphics2D gfx = (Graphics2D)_mgr.getMediaPanel().getGraphics(); + if (gfx != null) { + gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + (_antiAliased) ? + RenderingHints.VALUE_ANTIALIAS_ON : + RenderingHints.VALUE_ANTIALIAS_OFF); + _label.layout(gfx); + gfx.dispose(); + } + } + + /** The label associated with this sprite. */ + protected Label _label; + + /** Whether or not to use anti-aliased rendering. */ + protected boolean _antiAliased; +} diff --git a/src/java/com/threerings/media/sprite/OrientableImageSprite.java b/src/java/com/threerings/media/sprite/OrientableImageSprite.java new file mode 100644 index 00000000..bbee94fa --- /dev/null +++ b/src/java/com/threerings/media/sprite/OrientableImageSprite.java @@ -0,0 +1,198 @@ +// +// $Id: OrientableImageSprite.java 3586 2005-06-05 16:41:10Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sprite; + +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; + +import java.awt.image.*; + +import com.threerings.media.image.Mirage; + +import com.threerings.media.util.MultiFrameImage; + +/** + * An image sprite that uses AWT's rotation methods to render itself in + * different orientations. + */ +public class OrientableImageSprite extends ImageSprite +{ + /** + * Creates a new orientable image sprite. + */ + public OrientableImageSprite () + {} + + /** + * Creates a new orientable image sprite. + * + * @param image the image to render + */ + public OrientableImageSprite (Mirage image) + { + super(image); + } + + /** + * Creates a new orientable image sprite. + * + * @param frames the frames to render + */ + public OrientableImageSprite (MultiFrameImage frames) + { + super(frames); + } + + /** + * Computes and returns the rotation transform for this + * sprite. + * + * @return the newly computed rotation transform + */ + private AffineTransform getRotationTransform () + { + double theta; + + switch (_orient) { + case NORTH: + default: + theta = 0.0; + break; + + case SOUTH: + theta = Math.PI; + break; + + case EAST: + theta = Math.PI*0.5; + break; + + case WEST: + theta = -Math.PI*0.5; + break; + + case NORTHEAST: + theta = Math.PI*0.25; + break; + + case NORTHWEST: + theta = -Math.PI*0.25; + break; + + case SOUTHEAST: + theta = Math.PI*0.75; + break; + + case SOUTHWEST: + theta = -Math.PI*0.75; + break; + + case NORTHNORTHEAST: + theta = -Math.PI*0.125; + break; + + case NORTHNORTHWEST: + theta = Math.PI*0.125; + break; + + case SOUTHSOUTHEAST: + theta = -Math.PI*0.875; + break; + + case SOUTHSOUTHWEST: + theta = Math.PI*0.875; + break; + + case EASTNORTHEAST: + theta = -Math.PI*0.375; + break; + + case EASTSOUTHEAST: + theta = -Math.PI*0.625; + break; + + case WESTNORTHWEST: + theta = Math.PI*0.375; + break; + + case WESTSOUTHWEST: + theta = Math.PI*0.625; + break; + } + + return AffineTransform.getRotateInstance( + theta, + (_ox - _oxoff) + _frames.getWidth(_frameIdx)/2, + (_oy - _oyoff) + _frames.getHeight(_frameIdx)/2 + ); + } + + // Documentation inherited. + protected void accomodateFrame (int frameIdx, int width, int height) + { + Area area = new Area( + new Rectangle( + (_ox - _oxoff), + (_oy - _oyoff), + width, + height + ) + ); + + area.transform(getRotationTransform()); + + _bounds = area.getBounds(); + } + + // Documentation inherited. + public void setOrientation (int orient) + { + super.setOrientation(orient); + + layout(); + } + + // Documentation inherited. + public void paint (Graphics2D graphics) + { + AffineTransform at = graphics.getTransform(); + + graphics.transform(getRotationTransform()); + + if (_frames != null) { + _frames.paintFrame( + graphics, + _frameIdx, + _ox - _oxoff, + _oy - _oyoff + ); + } + else { + super.paint(graphics); + } + + graphics.setTransform(at); + } +} diff --git a/src/java/com/threerings/media/sprite/PathAdapter.java b/src/java/com/threerings/media/sprite/PathAdapter.java new file mode 100644 index 00000000..f4378401 --- /dev/null +++ b/src/java/com/threerings/media/sprite/PathAdapter.java @@ -0,0 +1,40 @@ +// +// $Id: PathAdapter.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sprite; + +import com.threerings.media.util.Path; + +/** + * An adapter class for {@link PathObserver}. + */ +public class PathAdapter implements PathObserver +{ + // documentation inherited from interface + public void pathCancelled (Sprite sprite, Path path) + { + } + + // documentation inherited from interface + public void pathCompleted (Sprite sprite, Path path, long when) + { + } +} diff --git a/src/java/com/threerings/media/sprite/PathObserver.java b/src/java/com/threerings/media/sprite/PathObserver.java new file mode 100644 index 00000000..de6cbfc8 --- /dev/null +++ b/src/java/com/threerings/media/sprite/PathObserver.java @@ -0,0 +1,49 @@ +// +// $Id: PathObserver.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sprite; + +import com.threerings.media.util.Path; + +/** + * An interface to be implemented by classes that would like to be + * notified when a sprite completes or cancels its path. + */ +public interface PathObserver +{ + /** + * Called when a sprite's path is cancelled either because a new path + * was started or the path was explicitly cancelled with {@link + * Sprite#cancelMove}. + */ + public void pathCancelled (Sprite sprite, Path path); + + /** + * Called when a sprite completes its traversal of a path. + * + * @param sprite the sprite that completed its path. + * @param path the path that was completed. + * @param when the tick stamp of the media tick on which the path was + * completed (see {@link SpriteManager#tick}) (this may not be in the + * same time domain as {@link System#currentTimeMillis}). + */ + public void pathCompleted (Sprite sprite, Path path, long when); +} diff --git a/src/java/com/threerings/media/sprite/Sprite.java b/src/java/com/threerings/media/sprite/Sprite.java new file mode 100644 index 00000000..9abf1c6a --- /dev/null +++ b/src/java/com/threerings/media/sprite/Sprite.java @@ -0,0 +1,462 @@ +// +// $Id: Sprite.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sprite; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; + +import com.samskivert.util.ObserverList; +import com.threerings.util.DirectionCodes; + +import com.threerings.media.AbstractMedia; +import com.threerings.media.util.Path; +import com.threerings.media.util.Pathable; + +import com.threerings.media.Log; + +/** + * The sprite class represents a single moveable object in an animated + * view. A sprite has a position and orientation within the view, and can + * be moved along a path. + */ +public abstract class Sprite extends AbstractMedia + implements DirectionCodes, Pathable +{ + /** + * Constructs a sprite with an initially invalid location. Because + * sprite derived classes generally want to get in on the business + * when a sprite's location is set, it is not safe to do so in the + * constructor because their derived methods will be called before + * their constructor has been called. Thus a sprite should be fully + * constructed and then its location should be set. + */ + public Sprite () + { + this(0, 0); + } + + /** + * Constructs a sprite with the supplied dimensions. Because + * sprite derived classes generally want to get in on the business + * when a sprite's location is set, it is not safe to do so in the + * constructor because their derived methods will be called before + * their constructor has been called. Thus a sprite should be fully + * constructed and then its location should be set. + */ + public Sprite (int width, int height) + { + super(new Rectangle(0, 0, width, height)); + } + + /** + * Returns the sprite's x position in screen coordinates. This is the + * x coordinate of the sprite's origin, not the upper left of its + * bounds. + */ + public int getX () + { + return _ox; + } + + /** + * Returns the sprite's y position in screen coordinates. This is the + * y coordinate of the sprite's origin, not the upper left of its + * bounds. + */ + public int getY () + { + return _oy; + } + + /** + * Returns the offset to the sprite's origin from the upper-left of + * the sprite's image. + */ + public int getXOffset () + { + return _oxoff; + } + + /** + * Returns the offset to the sprite's origin from the upper-left of + * the sprite's image. + */ + public int getYOffset () + { + return _oyoff; + } + + /** + * Returns the sprite's width in pixels. + */ + public int getWidth () + { + return _bounds.width; + } + + /** + * Returns the sprite's height in pixels. + */ + public int getHeight () + { + return _bounds.height; + } + + /** + * Sprites have an orientation in one of the eight cardinal + * directions: NORTH, NORTHEAST, etc. + * Derived classes can choose to override this member function and + * select a different set of images based on their orientation, or + * they can ignore the orientation information. + * + * @see DirectionCodes + */ + public void setOrientation (int orient) + { + _orient = orient; + } + + /** + * Returns the sprite's orientation as one of the eight cardinal + * directions: NORTH, NORTHEAST, etc. + * + * @see DirectionCodes + */ + public int getOrientation () + { + return _orient; + } + + // documentation inherited + public void setLocation (int x, int y) + { + if (x == _ox && y == _oy) { + return; // no-op + } + + // start with our current bounds + Rectangle dirty = new Rectangle(_bounds); + + // move ourselves + _ox = x; + _oy = y; + + // we need to update our draw position which is based on the size + // of our current bounds + updateRenderOrigin(); + + // grow the dirty rectangle to incorporate our new bounds and pass + // the dirty region to our region manager + if (_mgr != null) { + // if our new bounds intersect our old bounds, grow a single + // dirty rectangle to incorporate them both + if (_bounds.intersects(dirty)) { + dirty.add(_bounds); + } else { + // otherwise invalidate our new bounds separately + _mgr.getRegionManager().invalidateRegion(_bounds); + } + _mgr.getRegionManager().addDirtyRegion(dirty); + } + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + gfx.drawRect(_bounds.x, _bounds.y, _bounds.width-1, _bounds.height-1); + } + + /** + * Paint the sprite's path, if any, to the specified graphics context. + * + * @param gfx the graphics context. + */ + public void paintPath (Graphics2D gfx) + { + if (_path != null) { + _path.paint(gfx); + } + } + + /** + * Returns true if the sprite's bounds contain the specified point, + * false if not. + */ + public boolean contains (int x, int y) + { + return _bounds.contains(x, y); + } + + /** + * Returns true if the sprite's bounds contain the specified point, + * false if not. + */ + public boolean hitTest (int x, int y) + { + return _bounds.contains(x, y); + } + + /** + * Returns whether the sprite is inside the given shape in pixel + * coordinates. + */ + public boolean inside (Shape shape) + { + return shape.contains(_ox, _oy); + } + + /** + * Returns whether the sprite's drawn rectangle intersects the given + * shape in pixel coordinates. + */ + public boolean intersects (Shape shape) + { + return shape.intersects(_bounds); + } + + /** + * Returns true if this sprite is currently following a path, false if + * it is not. + */ + public boolean isMoving () + { + return (_path != null); + } + + /** + * Set the sprite's active path and start moving it along its merry + * way. If the sprite is already moving along a previous path the old + * path will be lost and the new path will begin to be traversed. + * + * @param path the path to follow. + */ + public void move (Path path) + { + // if there's a previous path, let it know that it's going away + cancelMove(); + + // save off this path + _path = path; + + // we'll initialize it on our next tick thanks to a zero path stamp + _pathStamp = 0; + } + + /** + * Cancels any path that the sprite may currently be moving along. + */ + public void cancelMove () + { + if (_path != null) { + Path oldpath = _path; + _path = null; + oldpath.wasRemoved(this); + if (_observers != null) { + _observers.apply(new CancelledOp(this, oldpath)); + } + } + } + + /** + * Returns the path being followed by this sprite or null if the + * sprite is not following a path. + */ + public Path getPath () + { + return _path; + } + + /** + * Called by the active path when it begins. + */ + public void pathBeginning () + { + // nothing for now + } + + /** + * Called by the active path when it has completed. + */ + public void pathCompleted (long timestamp) + { + Path oldpath = _path; + _path = null; + oldpath.wasRemoved(this); + if (_observers != null) { + _observers.apply(new CompletedOp(this, oldpath, timestamp)); + } + } + + // documentation inherited + public void tick (long tickStamp) + { + tickPath(tickStamp); + } + + /** + * Ticks any path assigned to this sprite. + * + * @return true if the path relocated the sprite as a result of this + * tick, false if it remained in the same position. + */ + protected boolean tickPath (long tickStamp) + { + if (_path == null) { + return false; + } + + // initialize the path if we haven't yet + if (_pathStamp == 0) { + _path.init(this, _pathStamp = tickStamp); + } + + // it's possible that as a result of init() the path completed and + // removed itself with a call to pathCompleted(), so we have to be + // careful here + return (_path == null) ? true : _path.tick(this, tickStamp); + } + + // documentation inherited + public void fastForward (long timeDelta) + { + // fast forward any path we're following + if (_path != null) { + _path.fastForward(timeDelta); + } + } + + /** + * Update the coordinates at which the sprite image is drawn to + * reflect the sprite's current position. + */ + protected void updateRenderOrigin () + { + // our bounds origin may differ from the sprite's origin + _bounds.x = _ox - _oxoff; + _bounds.y = _oy - _oyoff; + } + + /** + * Add a sprite observer to observe this sprite's events. + * + * @param obs the sprite observer. + */ + public void addSpriteObserver (Object obs) + { + addObserver(obs); + } + + /** + * Remove a sprite observer. + */ + public void removeSpriteObserver (Object obs) + { + removeObserver(obs); + } + + // documentation inherited + public void viewLocationDidChange (int dx, int dy) + { + if (_renderOrder >= HUD_LAYER) { + setLocation(_ox + dx, _oy + dy); + } + } + + // documentation inherited + protected void shutdown () + { + super.shutdown(); + cancelMove(); // cancel any active path + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", ox=").append(_ox); + buf.append(", oy=").append(_oy); + buf.append(", oxoff=").append(_oxoff); + buf.append(", oyoff=").append(_oyoff); + } + + /** Used to dispatch {@link PathObserver#pathCancelled}. */ + protected static class CancelledOp implements ObserverList.ObserverOp + { + public CancelledOp (Sprite sprite, Path path) { + _sprite = sprite; + _path = path; + } + + public boolean apply (Object observer) { + if (observer instanceof PathObserver) { + ((PathObserver)observer).pathCancelled(_sprite, _path); + } + return true; + } + + protected Sprite _sprite; + protected Path _path; + } + + /** Used to dispatch {@link PathObserver#pathCompleted}. */ + protected static class CompletedOp implements ObserverList.ObserverOp + { + public CompletedOp (Sprite sprite, Path path, long when) { + _sprite = sprite; + _path = path; + _when = when; + } + + public boolean apply (Object observer) { + if (observer instanceof PathObserver) { + ((PathObserver)observer).pathCompleted(_sprite, _path, _when); + } + return true; + } + + protected Sprite _sprite; + protected Path _path; + protected long _when; + } + + /** The location of the sprite's origin in pixel coordinates. If the + * sprite positions itself via a hotspot that is not the upper left + * coordinate of the sprite's bounds, the offset to the hotspot should + * be maintained in {@link #_oxoff} and {@link #_oyoff}. */ + protected int _ox = Integer.MIN_VALUE, _oy = Integer.MIN_VALUE; + + /** The offsets from our upper left coordinate to our origin (or hot + * spot). Derived classes will need to update these values if the + * sprite's origin is not coincident with the upper left coordinate of + * its bounds. */ + protected int _oxoff, _oyoff; + + /** The orientation of this sprite. */ + protected int _orient = NONE; + + /** When moving, the path the sprite is traversing. */ + protected Path _path; + + /** The timestamp at which we started along our path. */ + protected long _pathStamp; +} diff --git a/src/java/com/threerings/media/sprite/SpriteIcon.java b/src/java/com/threerings/media/sprite/SpriteIcon.java new file mode 100644 index 00000000..d8d3c9a3 --- /dev/null +++ b/src/java/com/threerings/media/sprite/SpriteIcon.java @@ -0,0 +1,94 @@ +// +// $Id: SpriteIcon.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sprite; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; + +import javax.swing.Icon; + +/** + * Implements the icon interface, using a {@link Sprite} to render the + * icon image. + */ +public class SpriteIcon implements Icon +{ + /** + * Creates a sprite icon that will use the supplied sprite to render + * itself. This sprite should not be used for anything else while + * being used in this icon because it will be "moved" when the icon is + * rendered. The sprite's origin will be set to the bottom center of + * the label. + */ + public SpriteIcon (Sprite sprite) + { + this(sprite, 0); + } + + /** + * Creates a sprite icon that will use the supplied sprite to render + * itself. This sprite should not be used for anything else while + * being used in this icon because it will be "moved" when the icon is + * rendered. The sprite's origin will be set to the bottom center of + * the label. + * + * @param sprite the sprite to render in this label. + * @param padding the number of pixels of blank space to put on all + * four sides of the sprite. + */ + public SpriteIcon (Sprite sprite, int padding) + { + _sprite = sprite; + // the sprite should be ticked once so that we can safely paint it + _sprite.tick(System.currentTimeMillis()); + _padding = padding; + } + + // documentation inherited from interface + public void paintIcon (Component c, Graphics g, int x, int y) + { + // move the sprite to a "location" that results in its image being + // in the upper left of the rectangle we desire + _sprite.setLocation(x + _sprite.getXOffset() + _padding, + y + _sprite.getYOffset() + _padding); + _sprite.paint((Graphics2D)g); + } + + // documentation inherited from interface + public int getIconWidth () + { + return _sprite.getWidth() + 2*_padding; + } + + // documentation inherited from interface + public int getIconHeight () + { + return _sprite.getHeight() + 2*_padding; + } + + /** The sprite used to render this icon. */ + protected Sprite _sprite; + + /** Used to put a bit of padding around the sprite image. */ + protected int _padding; +} diff --git a/src/java/com/threerings/media/sprite/SpriteManager.java b/src/java/com/threerings/media/sprite/SpriteManager.java new file mode 100644 index 00000000..d78081c5 --- /dev/null +++ b/src/java/com/threerings/media/sprite/SpriteManager.java @@ -0,0 +1,269 @@ +// +// $Id: SpriteManager.java 3733 2005-10-13 19:00:08Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sprite; + +import java.awt.Graphics2D; +import java.awt.Shape; + +import java.util.Collections; +import java.util.List; +import java.util.Iterator; + +import com.threerings.media.AbstractMediaManager; +import com.threerings.media.MediaPanel; + +/** + * The sprite manager manages the sprites running about in the game. + */ +public class SpriteManager extends AbstractMediaManager +{ + /** A predicate used to operate on sprites (see {@link #removeSprites}. */ + public static interface Predicate + { + /** Returns true if this sprite is to be included by the predicate, + * false if it should be excluded. */ + public boolean evaluate (Sprite sprite); + } + + /** + * Construct and initialize the sprite manager. + */ + public SpriteManager (MediaPanel panel) + { + super(panel); + } + + /** + * When an animated view processes its dirty rectangles, it may + * require an expansion of the dirty region which may in turn + * require the invalidation of more sprites than were originally + * invalid. In such cases, the animated view can call back to the + * sprite manager, asking it to append the sprites that intersect + * a particular region to the given list. + * + * @param list the list to fill with any intersecting sprites. + * @param shape the shape in which we have interest. + */ + public void getIntersectingSprites (List list, Shape shape) + { + int size = _media.size(); + for (int ii = 0; ii < size; ii++) { + Sprite sprite = (Sprite)_media.get(ii); + if (sprite.intersects(shape)) { + list.add(sprite); + } + } + } + + /** + * When an animated view is determining what entity in its view is + * under the mouse pointer, it may require a list of sprites that are + * "hit" by a particular pixel. The sprites' bounds are first checked + * and sprites with bounds that contain the supplied point are further + * checked for a non-transparent at the specified location. + * + * @param list the list to fill with any intersecting sprites, the + * sprites with the highest render order provided first. + * @param x the x (screen) coordinate to be checked. + * @param y the y (screen) coordinate to be checked. + */ + public void getHitSprites (List list, int x, int y) + { + for (int ii = _media.size() - 1; ii >= 0; ii--) { + Sprite sprite = (Sprite)_media.get(ii); + if (sprite.hitTest(x, y)) { + list.add(sprite); + } + } + } + + /** + * Finds the sprite with the highest render order that hits the + * specified pixel. + * + * @param x the x (screen) coordinate to be checked + * @param y the y (screen) coordinate to be checked + * @return the highest sprite hit + */ + public Sprite getHighestHitSprite (int x, int y) + { + // since they're stored in lowest -> highest order.. + for (int ii = _media.size() - 1; ii >= 0; ii--) { + Sprite sprite = (Sprite)_media.get(ii); + if (sprite.hitTest(x, y)) { + return sprite; + } + } + return null; + } + + /** + * Add a sprite to the set of sprites managed by this manager. + * + * @param sprite the sprite to add. + */ + public void addSprite (Sprite sprite) + { + if (insertMedia(sprite)) { + // and invalidate the sprite's original position + sprite.invalidate(); + } + } + + /** + * Returns a list of all sprites registered with the sprite manager. + * The returned list is immutable, sprites should be added or removed + * using {@link #addSprite} or {@link #removeSprite}. + */ + public List getSprites () + { + return Collections.unmodifiableList(_media); + } + + /** + * Returns an iterator over our managed sprites. Do not call + * {@link Iterator#remove}. + */ + public Iterator enumerateSprites () + { + return _media.iterator(); + } + + /** + * Removes the specified sprite from the set of sprites managed by + * this manager. + * + * @param sprite the sprite to remove. + */ + public void removeSprite (Sprite sprite) + { + removeMedia(sprite); + } + + /** + * Removes all sprites that match the supplied predicate. + */ + public void removeSprites (Predicate pred) + { + int idxoff = 0; + for (int ii = 0, ll = _media.size(); ii < ll; ii++) { + Sprite sprite = (Sprite)_media.get(ii-idxoff); + if (pred.evaluate(sprite)) { + _media.remove(sprite); + sprite.invalidate(); + sprite.shutdown(); + // we need to preserve the original "index" relative to the + // current tick position, so we don't decrement ii directly + idxoff++; + if (ii <= _tickpos) { + _tickpos--; + } + } + } + } + + /** + * Render the sprite paths to the given graphics context. + * + * @param gfx the graphics context. + */ + public void renderSpritePaths (Graphics2D gfx) + { + for (int ii=0, nn=_media.size(); ii < nn; ii++) { + Sprite sprite = (Sprite)_media.get(ii); + sprite.paintPath(gfx); + } + } + +// NOTE- collision handling code is turned off for now. To re-implement, +// a new array should be kept with sprites sorted in some sort of x/y order +// +// /** +// * Check all sprites for collisions with others and inform any +// * sprite observers. +// */ +// protected void handleCollisions () +// { +// // gather a list of all sprite collisions +// int size = _sprites.size(); +// for (int ii = 0; ii < size; ii++) { +// Sprite sprite = (Sprite)_sprites.get(ii); +// checkCollisions(ii, size, sprite); +// } +// } +// +// /** +// * Check a sprite for collision with any other sprites in the +// * sprite list and notify the sprite observers associated with any +// * sprites that do indeed collide. +// * +// * @param idx the starting sprite index. +// * @param size the total number of sprites. +// * @param sprite the sprite to check against other sprites for +// * collisions. +// */ +// protected void checkCollisions (int idx, int size, Sprite sprite) +// { +// // TODO: make this handle quickly moving objects that may pass +// // through each other. +// +// // if we're the last sprite we know we've already handled any +// // collisions +// if (idx == (size - 1)) { +// return; +// } +// +// // calculate the x-position of the right edge of the sprite we're +// // checking for collisions +// Rectangle bounds = sprite.getBounds(); +// int edgeX = bounds.x + bounds.width; +// +// for (int ii = (idx + 1); ii < size; ii++) { +// Sprite other = (Sprite)_sprites.get(ii); +// Rectangle obounds = other.getBounds(); +// if (obounds.x > edgeX) { +// // since sprites are stored in the list sorted by +// // ascending x-position, we know this sprite and any +// // other sprites farther on in the list can't possibly +// // intersect with the sprite we're checking, so we're +// // done. +// return; +// +// } else if (obounds.intersects(bounds)) { +// sprite.notifyObservers(new CollisionEvent(sprite, other)); +// } +// } +// } +// /** The comparator used to sort sprites by horizontal position. */ +// protected static final Comparator SPRITE_COMP = new SpriteComparator(); +// +// /** Used to sort sprites. */ +// protected static class SpriteComparator implements Comparator +// { +// public int compare (Object o1, Object o2) +// { +// Sprite s1 = (Sprite)o1; +// Sprite s2 = (Sprite)o2; +// return (s2.getX() - s1.getX()); +// } +// } +} diff --git a/src/java/com/threerings/media/sprite/action/ActionSprite.java b/src/java/com/threerings/media/sprite/action/ActionSprite.java new file mode 100644 index 00000000..30bb6158 --- /dev/null +++ b/src/java/com/threerings/media/sprite/action/ActionSprite.java @@ -0,0 +1,16 @@ +// +// $Id$ + +package com.threerings.media.sprite.action; + +/** + * An Action sprite is a sprite that may be pressed to generate an + * ActionEvent that will be posted to the Controller hierarchy. + */ +public interface ActionSprite +{ + /** + * @return the action command to submit if this sprite is clicked. + */ + public String getActionCommand (); +} diff --git a/src/java/com/threerings/media/sprite/action/ArmingSprite.java b/src/java/com/threerings/media/sprite/action/ArmingSprite.java new file mode 100644 index 00000000..8d0763f3 --- /dev/null +++ b/src/java/com/threerings/media/sprite/action/ArmingSprite.java @@ -0,0 +1,16 @@ +// +// $Id$ + +package com.threerings.media.sprite.action; + +/** + * An ActionSprite that wishes to be notified of events when it is armed + * or not. + */ +public interface ArmingSprite extends ActionSprite +{ + /** + * Render this sprite such that is is drawn "armed". + */ + public void setArmed (boolean armed); +} diff --git a/src/java/com/threerings/media/sprite/action/CommandSprite.java b/src/java/com/threerings/media/sprite/action/CommandSprite.java new file mode 100644 index 00000000..8384be2b --- /dev/null +++ b/src/java/com/threerings/media/sprite/action/CommandSprite.java @@ -0,0 +1,16 @@ +// +// $Id$ + +package com.threerings.media.sprite.action; + +/** + * Extends CommandSprite to be a sprite that posts CommandEvents to + * the Controller hierarchy. + */ +public interface CommandSprite extends ActionSprite +{ + /** + * @return the argument to the action command. + */ + public Object getCommandArgument(); +} diff --git a/src/java/com/threerings/media/sprite/action/DisableableSprite.java b/src/java/com/threerings/media/sprite/action/DisableableSprite.java new file mode 100644 index 00000000..c19a7d8b --- /dev/null +++ b/src/java/com/threerings/media/sprite/action/DisableableSprite.java @@ -0,0 +1,16 @@ +// +// $Id$ + +package com.threerings.media.sprite.action; + +/** + * Indicates a Sprite that may or may not be enabled to receive + * action / hover / arming notifications. + */ +public interface DisableableSprite +{ + /** + * @return true if this sprite is currently enabled. + */ + public boolean isEnabled (); +} diff --git a/src/java/com/threerings/media/sprite/action/HoverSprite.java b/src/java/com/threerings/media/sprite/action/HoverSprite.java new file mode 100644 index 00000000..409da3da --- /dev/null +++ b/src/java/com/threerings/media/sprite/action/HoverSprite.java @@ -0,0 +1,16 @@ +// +// $Id$ + +package com.threerings.media.sprite.action; + +/** + * An interface indicating that a sprite wishes to be notified when + * the mouse hovers over it. + */ +public interface HoverSprite +{ + /** + * Set the current hover state. + */ + public void setHovered (boolean hovered); +} diff --git a/src/java/com/threerings/media/tile/IMImageProvider.java b/src/java/com/threerings/media/tile/IMImageProvider.java new file mode 100644 index 00000000..d738bd90 --- /dev/null +++ b/src/java/com/threerings/media/tile/IMImageProvider.java @@ -0,0 +1,73 @@ +// +// $Id: IMImageProvider.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; + +import com.threerings.media.image.Colorization; +import com.threerings.media.image.ImageDataProvider; +import com.threerings.media.image.ImageManager; +import com.threerings.media.image.Mirage; + +/** + * Provides images to a tileset given a reference to the image manager and + * an image data provider. + */ +public class IMImageProvider implements ImageProvider +{ + public IMImageProvider (ImageManager imgr, ImageDataProvider dprov) + { + _imgr = imgr; + _dprov = dprov; + } + + public IMImageProvider (ImageManager imgr, String rset) + { + _imgr = imgr; + _rset = rset; + } + + // documentation inherited from interface + public BufferedImage getTileSetImage (String path, Colorization[] zations) + { + return _imgr.getImage(getImageKey(path), zations); + } + + // documentation inherited from interface + public Mirage getTileImage (String path, Rectangle bounds, + Colorization[] zations) + { + return _imgr.getMirage(getImageKey(path), bounds, zations); + } + + protected final ImageManager.ImageKey getImageKey (String path) + { + return (_dprov == null) ? + _imgr.getImageKey(_rset, path) : + _imgr.getImageKey(_dprov, path); + } + + protected ImageManager _imgr; + protected ImageDataProvider _dprov; + protected String _rset; +} diff --git a/src/java/com/threerings/media/tile/ImageProvider.java b/src/java/com/threerings/media/tile/ImageProvider.java new file mode 100644 index 00000000..9046e2a5 --- /dev/null +++ b/src/java/com/threerings/media/tile/ImageProvider.java @@ -0,0 +1,64 @@ +// +// $Id: ImageProvider.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; + +import com.threerings.media.image.Colorization; +import com.threerings.media.image.ImageManager; +import com.threerings.media.image.Mirage; + +/** + * Provides a generic interface via which tileset images may be loaded. In + * most cases, a running application will want to obtain images via the + * {@link ImageManager}, but in some circumstances a simpler image + * provider may be desirable to avoid the overhead of the image manager + * infrastructure when simple image loading is all that is desired. + */ +public interface ImageProvider +{ + /** + * Returns the raw tileset image with the specified path. + * + * @param path the path that identifies the desired image (corresponds + * to the image path from the tileset). + * @param zations if non-null, colorizations to apply to the source + * image before returning it. + */ + public BufferedImage getTileSetImage (String path, Colorization[] zations); + + /** + * Obtains the tile image with the specified path in the form of a + * {@link Mirage}. It should be cropped from the tileset image + * identified by the supplied path. + * + * @param path the path that identifies the desired image (corresponds + * to the image path from the tileset). + * @param bounds if non-null, the region of the image to be returned + * as a mirage. If null, the entire image should be returned. + * @param zations if non-null, colorizations to apply to the image + * before converting it into a mirage. + */ + public Mirage getTileImage (String path, Rectangle bounds, + Colorization[] zations); +} diff --git a/src/java/com/threerings/media/tile/NoSuchTileSetException.java b/src/java/com/threerings/media/tile/NoSuchTileSetException.java new file mode 100644 index 00000000..22e48c07 --- /dev/null +++ b/src/java/com/threerings/media/tile/NoSuchTileSetException.java @@ -0,0 +1,39 @@ +// +// $Id: NoSuchTileSetException.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +/** + * Thrown when an attempt is made to retrieve a non-existent tile set from + * the tile set manager. + */ +public class NoSuchTileSetException extends Exception +{ + public NoSuchTileSetException (String tileSetName) + { + super("No tile set named '" + tileSetName + "'"); + } + + public NoSuchTileSetException (int tileSetId) + { + super("No tile set with id '" + tileSetId + "'"); + } +} diff --git a/src/java/com/threerings/media/tile/ObjectTile.java b/src/java/com/threerings/media/tile/ObjectTile.java new file mode 100644 index 00000000..b5d8cf01 --- /dev/null +++ b/src/java/com/threerings/media/tile/ObjectTile.java @@ -0,0 +1,232 @@ +// +// $Id: ObjectTile.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Dimension; +import java.awt.Point; + +import com.samskivert.util.ListUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.util.DirectionUtil; + +/** + * An object tile extends the base tile to provide support for objects + * whose image spans more than one unit tile. + * + *

An object tile is generally positioned based on its origin rather + * than the upper left of its image. Generally this origin is in the + * bottom center of the object image, but can be configured to be anywhere + * that the natural center point of "contact" is for the object. Note that + * this does not automatically adjust the semantics of {@link #paint}, it + * is just expected that the caller will account for the object tile's + * origin when painting, if appropriate. + * + *

An object tile has dimensions (in tile units) that represent its + * footprint or "shadow". + */ +public class ObjectTile extends Tile +{ + /** + * Returns the object footprint width in tile units. + */ + public int getBaseWidth () + { + return _base.width; + } + + /** + * Returns the object footprint height in tile units. + */ + public int getBaseHeight () + { + return _base.height; + } + + /** + * Sets the object footprint in tile units. + */ + protected void setBase (int width, int height) + { + _base.width = width; + _base.height = height; + } + + /** + * Returns the x offset into the tile image of the origin (which will + * be aligned with the bottom center of the origin tile) or + * Integer.MIN_VALUE if the origin is not explicitly + * specified and should be computed from the image size and tile + * footprint. + */ + public int getOriginX () + { + return _origin.x; + } + + /** + * Returns the y offset into the tile image of the origin (which will + * be aligned with the bottom center of the origin tile) or + * Integer.MIN_VALUE if the origin is not explicitly + * specified and should be computed from the image size and tile + * footprint. + */ + public int getOriginY () + { + return _origin.y; + } + + /** + * Sets the offset in pixels from the origin of the tile image to the + * origin of the object. The object will be rendered such that its + * origin is at the bottom center of its origin tile. If no origin is + * specified, the bottom of the image is aligned with the bottom of + * the origin tile and the left side of the image is aligned with the + * left edge of the left-most base tile. + */ + protected void setOrigin (int x, int y) + { + _origin.x = x; + _origin.y = y; + } + + /** + * Returns this object tile's default render priority. + */ + public int getPriority () + { + return _priority; + } + + /** + * Sets this object tile's default render priority. + */ + protected void setPriority (int priority) + { + _priority = priority; + } + + /** + * Configures the "spot" associated with this object. + */ + public void setSpot (int x, int y, byte orient) + { + _spot = new Point(x, y); + _sorient = orient; + } + + /** + * Returns true if this object has a spot. + */ + public boolean hasSpot () + { + return (_spot != null); + } + + /** + * Returns the x-coordinate of the "spot" associated with this object. + */ + public int getSpotX () + { + return (_spot == null) ? 0 : _spot.x; + } + + /** + * Returns the x-coordinate of the "spot" associated with this object. + */ + public int getSpotY () + { + return (_spot == null) ? 0 : _spot.y; + } + + /** + * Returns the orientation of the "spot" associated with this object. + */ + public int getSpotOrient () + { + return _sorient; + } + + /** + * Returns the list of constraints associated with this object, or + * null if the object has no constraints. + */ + public String[] getConstraints () + { + return _constraints; + } + + /** + * Checks whether this object has the given constraint. + */ + public boolean hasConstraint (String constraint) + { + return (_constraints == null) ? false : + ListUtil.contains(_constraints, constraint); + } + + /** + * Configures this object's constraints. + */ + public void setConstraints (String[] constraints) + { + _constraints = constraints; + } + + // documentation inherited + public void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", base=").append(StringUtil.toString(_base)); + buf.append(", origin=").append(StringUtil.toString(_origin)); + buf.append(", priority=").append(_priority); + if (_spot != null) { + buf.append(", spot=").append(StringUtil.toString(_spot)); + buf.append(", sorient="); + buf.append(DirectionUtil.toShortString(_sorient)); + } + if (_constraints != null) { + buf.append(", constraints=").append(StringUtil.toString( + _constraints)); + } + } + + /** The object footprint width in unit tile units. */ + protected Dimension _base = new Dimension(1, 1); + + /** The offset from the origin of the tile image to the object's + * origin or MIN_VALUE if the origin should be calculated based on the + * footprint. */ + protected Point _origin = new Point(Integer.MIN_VALUE, Integer.MIN_VALUE); + + /** This object tile's default render priority. */ + protected int _priority; + + /** The coordinates of the "spot" associated with this object. */ + protected Point _spot; + + /** The orientation of the "spot" associated with this object. */ + protected byte _sorient; + + /** The list of constraints associated with this object. */ + protected String[] _constraints; +} diff --git a/src/java/com/threerings/media/tile/ObjectTileSet.java b/src/java/com/threerings/media/tile/ObjectTileSet.java new file mode 100644 index 00000000..d32c2bda --- /dev/null +++ b/src/java/com/threerings/media/tile/ObjectTileSet.java @@ -0,0 +1,295 @@ +// +// $Id: ObjectTileSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import com.samskivert.util.ListUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.media.image.Colorization; + +/** + * The object tileset supports the specification of object information for + * object tiles in addition to all of the features of the swiss army + * tileset. + * + * @see ObjectTile + */ +public class ObjectTileSet extends SwissArmyTileSet + implements RecolorableTileSet +{ + /** A constraint prefix indicating that the object must have empty space in + * the suffixed direction (N, E, S, or W). */ + public static final String SPACE = "SPACE_"; + + /** A constraint indicating that the object is a surface (e.g., table). */ + public static final String SURFACE = "SURFACE"; + + /** A constraint indicating that the object must be placed on a surface. */ + public static final String ON_SURFACE = "ON_SURFACE"; + + /** A constraint prefix indicating that the object is a wall facing the + * suffixed direction (N, E, S, or W). */ + public static final String WALL = "WALL_"; + + /** A constraint prefix indicating that the object must be placed on a + * wall facing the suffixed direction (N, E, S, or W). */ + public static final String ON_WALL = "ON_WALL_"; + + /** A constraint prefix indicating that the object must be attached to a + * wall facing the suffixed direction (N, E, S, or W). */ + public static final String ATTACH = "ATTACH_"; + + /** The low suffix for walls and attachments. Low attachments can be placed + * on low or normal walls; normal attachments can only be placed on normal + * walls. */ + public static final String LOW = "_LOW"; + + /** + * Sets the widths (in unit tile count) of the objects in this + * tileset. This must be accompanied by a call to {@link + * #setObjectHeights}. + */ + public void setObjectWidths (int[] objectWidths) + { + _owidths = objectWidths; + } + + /** + * Sets the heights (in unit tile count) of the objects in this + * tileset. This must be accompanied by a call to {@link + * #setObjectWidths}. + */ + public void setObjectHeights (int[] objectHeights) + { + _oheights = objectHeights; + } + + /** + * Sets the x offset in pixels to the image origin. + */ + public void setXOrigins (int[] xorigins) + { + _xorigins = xorigins; + } + + /** + * Sets the y offset in pixels to the image origin. + */ + public void setYOrigins (int[] yorigins) + { + _yorigins = yorigins; + } + + /** + * Sets the default render priorities for our object tiles. + */ + public void setPriorities (byte[] priorities) + { + _priorities = priorities; + } + + /** + * Provides a set of colorization classes that apply to objects in + * this tileset. + */ + public void setColorizations (String[] zations) + { + _zations = zations; + } + + /** + * Sets the x offset to the "spots" associated with our object tiles. + */ + public void setXSpots (short[] xspots) + { + _xspots = xspots; + } + + /** + * Sets the y offset to the "spots" associated with our object tiles. + */ + public void setYSpots (short[] yspots) + { + _yspots = yspots; + } + + /** + * Sets the orientation of the "spots" associated with our object + * tiles. + */ + public void setSpotOrients (byte[] sorients) + { + _sorients = sorients; + } + + /** + * Sets the lists of constraints associated with our object tiles. + */ + public void setConstraints (String[][] constraints) + { + _constraints = constraints; + } + + /** + * Returns the x coordinate of the spot associated with the specified + * tile index. + */ + public int getXSpot (int tileIdx) + { + return (_xspots == null) ? 0 : _xspots[tileIdx]; + } + + /** + * Returns the y coordinate of the spot associated with the specified + * tile index. + */ + public int getYSpot (int tileIdx) + { + return (_yspots == null) ? 0 : _yspots[tileIdx]; + } + + /** + * Returns the orientation of the spot associated with the specified + * tile index. + */ + public int getSpotOrient (int tileIdx) + { + return (_sorients == null) ? 0 : _sorients[tileIdx]; + } + + /** + * Returns the list of constraints associated with the specified tile + * index, or null if the index has no constraints. + */ + public String[] getConstraints (int tileIdx) + { + return (_constraints == null) ? null : _constraints[tileIdx]; + } + + /** + * Checks whether the tile at the specified index has the given constraint. + */ + public boolean hasConstraint (int tileIdx, String constraint) + { + return (_constraints == null) ? false : + ListUtil.contains(_constraints[tileIdx], constraint); + } + + // documentation inherited from interface RecolorableTileSet + public String[] getColorizations () + { + return _zations; + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", owidths=").append(StringUtil.toString(_owidths)); + buf.append(", oheights=").append(StringUtil.toString(_oheights)); + buf.append(", xorigins=").append(StringUtil.toString(_xorigins)); + buf.append(", yorigins=").append(StringUtil.toString(_yorigins)); + buf.append(", prios=").append(StringUtil.toString(_priorities)); + buf.append(", zations=").append(StringUtil.toString(_zations)); + buf.append(", xspots=").append(StringUtil.toString(_xspots)); + buf.append(", yspots=").append(StringUtil.toString(_yspots)); + buf.append(", sorients=").append(StringUtil.toString(_sorients)); + buf.append(", constraints=").append(StringUtil.toString(_constraints)); + } + + // documentation inherited + protected Colorization[] getColorizations (int tileIndex, Colorizer rizer) + { + Colorization[] zations = null; + if (rizer != null && _zations != null) { + zations = new Colorization[_zations.length]; + for (int ii = 0; ii < _zations.length; ii++) { + zations[ii] = rizer.getColorization(ii, _zations[ii]); + } + } + return zations; + } + + // documentation inherited + protected Tile createTile () + { + return new ObjectTile(); + } + + // documentation inherited + protected void initTile (Tile tile, int tileIndex, Colorization[] zations) + { + super.initTile(tile, tileIndex, zations); + + ObjectTile otile = (ObjectTile)tile; + if (_owidths != null) { + otile.setBase(_owidths[tileIndex], _oheights[tileIndex]); + } + if (_xorigins != null) { + otile.setOrigin(_xorigins[tileIndex], _yorigins[tileIndex]); + } + if (_priorities != null) { + otile.setPriority(_priorities[tileIndex]); + } + if (_xspots != null) { + otile.setSpot(_xspots[tileIndex], _yspots[tileIndex], + _sorients[tileIndex]); + } + if (_constraints != null) { + otile.setConstraints(_constraints[tileIndex]); + } + } + + /** The width (in tile units) of our object tiles. */ + protected int[] _owidths; + + /** The height (in tile units) of our object tiles. */ + protected int[] _oheights; + + /** The x offset in pixels to the origin of the tile images. */ + protected int[] _xorigins; + + /** The y offset in pixels to the origin of the tile images. */ + protected int[] _yorigins; + + /** The default render priorities of our objects. */ + protected byte[] _priorities; + + /** Colorization classes that apply to our objects. */ + protected String[] _zations; + + /** The x offset to the "spots" associated with our tiles. */ + protected short[] _xspots; + + /** The y offset to the "spots" associated with our tiles. */ + protected short[] _yspots; + + /** The orientation of the "spots" associated with our tiles. */ + protected byte[] _sorients; + + /** Lists of constraints associated with our tiles. */ + protected String[][] _constraints; + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 2; +} diff --git a/src/java/com/threerings/media/tile/RecolorableTileSet.java b/src/java/com/threerings/media/tile/RecolorableTileSet.java new file mode 100644 index 00000000..1e6e9a71 --- /dev/null +++ b/src/java/com/threerings/media/tile/RecolorableTileSet.java @@ -0,0 +1,16 @@ +// +// $Id: RecolorableTileSet.java 4191 2006-06-13 22:42:20Z ray $ + +package com.threerings.media.tile; + +/** + * Indicates that a tileset has recolorization classes defined. + */ +public interface RecolorableTileSet +{ + /** + * Returns the colorization classes that should be used to recolor + * objects in this tileset. + */ + public String[] getColorizations (); +} diff --git a/src/java/com/threerings/media/tile/SimpleCachingImageProvider.java b/src/java/com/threerings/media/tile/SimpleCachingImageProvider.java new file mode 100644 index 00000000..131624bd --- /dev/null +++ b/src/java/com/threerings/media/tile/SimpleCachingImageProvider.java @@ -0,0 +1,77 @@ +// +// $Id: SimpleCachingImageProvider.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import com.samskivert.util.LRUHashMap; + +import com.threerings.media.Log; +import com.threerings.media.image.BufferedMirage; +import com.threerings.media.image.Colorization; +import com.threerings.media.image.Mirage; + +/** + * An image provider that can be used by command line tools to load images + * and provide them to tilesets when doing things like preprocessing + * tileset images. + */ +public abstract class SimpleCachingImageProvider implements ImageProvider +{ + // documentation inherited from interface + public BufferedImage getTileSetImage (String path, Colorization[] zations) + { + BufferedImage image = (BufferedImage)_cache.get(path); + if (image == null) { + try { + image = loadImage(path); + _cache.put(path, image); + } catch (IOException ioe) { + Log.warning("Failed to load image [path=" + path + + ", ioe=" + ioe + "]."); + } + } + return image; + } + + // documentation inherited from interface + public Mirage getTileImage (String path, Rectangle bounds, + Colorization[] zations) + { + // mostly fake it + BufferedImage tsimg = getTileSetImage(path, zations); + tsimg = tsimg.getSubimage(bounds.x, bounds.y, + bounds.width, bounds.height); + return new BufferedMirage(tsimg); + } + + /** + * Derived classes must implement this method to actually load the raw + * source images. + */ + protected abstract BufferedImage loadImage (String path) + throws IOException; + + protected LRUHashMap _cache = new LRUHashMap(10); +} diff --git a/src/java/com/threerings/media/tile/SwissArmyTileSet.java b/src/java/com/threerings/media/tile/SwissArmyTileSet.java new file mode 100644 index 00000000..8068b7a1 --- /dev/null +++ b/src/java/com/threerings/media/tile/SwissArmyTileSet.java @@ -0,0 +1,207 @@ +// +// $Id: SwissArmyTileSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Dimension; +import java.awt.Point; +import java.awt.Rectangle; + +import java.io.IOException; +import java.io.ObjectInputStream; + +import com.samskivert.util.StringUtil; + +/** + * The swiss army tileset supports a diverse variety of tiles in the + * tileset image. Each row can contain varying numbers of tiles and each + * row can have its own width and height. Tiles can be separated from the + * edge of the tileset image by some border offset and can be separated + * from one another by a gap distance. + */ +public class SwissArmyTileSet extends TileSet +{ + // documentation inherited + public int getTileCount () + { + return _numTiles; + } + + /** + * Sets the tile counts which are the number of tiles in each row of + * the tileset image. Each row can have an arbitrary number of tiles. + */ + public void setTileCounts (int[] tileCounts) + { + _tileCounts = tileCounts; + + // compute our total tile count + computeTileCount(); + } + + /** + * Returns the tile count settings. + */ + public int[] getTileCounts () + { + return _tileCounts; + } + + /** + * Computes our total tile count from the individual counts for each + * row. + */ + protected void computeTileCount () + { + // compute our number of tiles + _numTiles = 0; + for (int i = 0; i < _tileCounts.length; i++) { + _numTiles += _tileCounts[i]; + } + } + + /** + * Sets the tile widths for each row. Each row can have tiles of a + * different width. + */ + public void setWidths (int[] widths) + { + _widths = widths; + } + + /** + * Returns the width settings. + */ + public int[] getWidths () + { + return _widths; + } + + /** + * Sets the tile heights for each row. Each row can have tiles of a + * different height. + */ + public void setHeights (int[] heights) + { + _heights = heights; + } + + /** + * Returns the height settings. + */ + public int[] getHeights () + { + return _heights; + } + + /** + * Sets the offset in pixels of the upper left corner of the first + * tile in the first row. If the tileset image has a border, this can + * be set to account for it. + */ + public void setOffsetPos (Point offsetPos) + { + _offsetPos = offsetPos; + } + + /** + * Sets the size of the gap between tiles (in pixels). If the tiles + * have space between them, this can be set to account for it. + */ + public void setGapSize (Dimension gapSize) + { + _gapSize = gapSize; + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", widths=").append(StringUtil.toString(_widths)); + buf.append(", heights=").append(StringUtil.toString(_heights)); + buf.append(", tileCounts=").append(StringUtil.toString(_tileCounts)); + buf.append(", offsetPos=").append(StringUtil.toString(_offsetPos)); + buf.append(", gapSize=").append(StringUtil.toString(_gapSize)); + } + + // documentation inherited + protected Rectangle computeTileBounds (int tileIndex) + { + // find the row number containing the sought-after tile + int ridx, tcount, ty, tx; + ridx = tcount = 0; + + // start tile image position at image start offset + tx = _offsetPos.x; + ty = _offsetPos.y; + + while ((tcount += _tileCounts[ridx]) < tileIndex + 1) { + // increment tile image position by row height and gap distance + ty += (_heights[ridx++] + _gapSize.height); + } + + // determine the horizontal index of this tile in the row + int xidx = tileIndex - (tcount - _tileCounts[ridx]); + + // final image x-position is based on tile width and gap distance + tx += (xidx * (_widths[ridx] + _gapSize.width)); + +// Log.info("Computed tile bounds [tileIndex=" + tileIndex + +// ", ridx=" + ridx + ", xidx=" + xidx + +// ", tx=" + tx + ", ty=" + ty + "]."); + + // crop the tile-sized image chunk from the full image + return new Rectangle(tx, ty, _widths[ridx], _heights[ridx]); + } + + private void readObject (ObjectInputStream in) + throws IOException, ClassNotFoundException + { + in.defaultReadObject(); + + // compute our total tile count + computeTileCount(); + } + + /** The number of tiles in each row. */ + protected int[] _tileCounts; + + /** The number of tiles in the tileset. */ + protected int _numTiles; + + /** The width of the tiles in each row in pixels. */ + protected int[] _widths; + + /** The height of the tiles in each row in pixels. */ + protected int[] _heights; + + /** The offset distance (x, y) in pixels from the top-left of the + * image to the start of the first tile image. */ + protected Point _offsetPos = new Point(); + + /** The distance (x, y) in pixels between each tile in each row + * horizontally, and between each row of tiles vertically. */ + protected Dimension _gapSize = new Dimension(); + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/media/tile/Tile.java b/src/java/com/threerings/media/tile/Tile.java new file mode 100644 index 00000000..7597014d --- /dev/null +++ b/src/java/com/threerings/media/tile/Tile.java @@ -0,0 +1,178 @@ +// +// $Id: Tile.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Graphics2D; +import java.util.Arrays; + +import com.threerings.media.image.Colorization; +import com.threerings.media.image.Mirage; + +/** + * A tile represents a single square in a single layer in a scene. + */ +public class Tile // implements Cloneable +{ + /** Used when caching tiles. */ + public static class Key + { + public TileSet tileSet; + public int tileIndex; + public Colorization[] zations; + + public Key (TileSet tileSet, int tileIndex, Colorization[] zations) { + this.tileSet = tileSet; + this.tileIndex = tileIndex; + this.zations = zations; + } + + public boolean equals (Object other) { + if (other instanceof Key) { + Key okey = (Key)other; + return (tileSet == okey.tileSet && + tileIndex == okey.tileIndex && + Arrays.equals(zations, okey.zations)); + } else { + return false; + } + } + + public int hashCode () { + int code = (tileSet == null) ? tileIndex : + (tileSet.hashCode() ^ tileIndex); + int zcount = (zations == null) ? 0 : zations.length; + for (int ii = 0; ii < zcount; ii++) { + if (zations[ii] != null) { + code ^= zations[ii].hashCode(); + } + } + return code; + } + } + + /** The key associated with this tile. */ + public Key key; + + /** + * Configures this tile with its tile image. + */ + public void setImage (Mirage image) + { + if (_mirage != null) { + _totalTileMemory -= _mirage.getEstimatedMemoryUsage(); + } + _mirage = image; + if (_mirage != null) { + _totalTileMemory += _mirage.getEstimatedMemoryUsage(); + } + } + + /** + * Returns the width of this tile. + */ + public int getWidth () + { + return _mirage.getWidth(); + } + + /** + * Returns the height of this tile. + */ + public int getHeight () + { + return _mirage.getHeight(); + } + + /** + * Returns the estimated memory usage of our underlying tile image. + */ + public long getEstimatedMemoryUsage () + { + return _mirage.getEstimatedMemoryUsage(); + } + + /** + * Render the tile image at the specified position in the given + * graphics context. + */ + public void paint (Graphics2D gfx, int x, int y) + { + _mirage.paint(gfx, x, y); + } + + /** + * Returns true if the specified coordinates within this tile contains + * a non-transparent pixel. + */ + public boolean hitTest (int x, int y) + { + return _mirage.hitTest(x, y); + } + +// /** +// * Creates a shallow copy of this tile object. +// */ +// public Object clone () +// { +// try { +// return (Tile)super.clone(); +// } catch (CloneNotSupportedException cnse) { +// String errmsg = "All is wrong with the universe: " + cnse; +// throw new RuntimeException(errmsg); +// } +// } + + /** + * Return a string representation of this tile. + */ + public String toString () + { + StringBuilder buf = new StringBuilder("["); + toString(buf); + return buf.append("]").toString(); + } + + /** + * This should be overridden by derived classes (which should be sure + * to call super.toString()) to append the derived class + * specific tile information to the string buffer. + */ + protected void toString (StringBuilder buf) + { + buf.append(_mirage.getWidth()).append("x"); + buf.append(_mirage.getHeight()); + } + + /** Decrement total tile memory by our value. */ + protected void finalize () + { + if (_mirage != null) { + _totalTileMemory -= _mirage.getEstimatedMemoryUsage(); + } + } + + /** Our tileset image. */ + protected Mirage _mirage; + + /** Used to track total (estimated) memory in use by tiles. */ + protected static long _totalTileMemory = 0L; +} diff --git a/src/java/com/threerings/media/tile/TileIcon.java b/src/java/com/threerings/media/tile/TileIcon.java new file mode 100644 index 00000000..7f379479 --- /dev/null +++ b/src/java/com/threerings/media/tile/TileIcon.java @@ -0,0 +1,64 @@ +// +// $Id: TileIcon.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Component; +import java.awt.Graphics2D; +import java.awt.Graphics; + +import javax.swing.Icon; + +/** + * Implements the icon interface, using a {@link Tile} to render the icon + * image. + */ +public class TileIcon implements Icon +{ + /** + * Creates a tile icon that will use the supplied tile to render itself. + */ + public TileIcon (Tile tile) + { + _tile = tile; + } + + // documentation inherited from interface + public void paintIcon (Component c, Graphics g, int x, int y) + { + _tile.paint((Graphics2D)g, x, y); + } + + // documentation inherited from interface + public int getIconWidth () + { + return _tile.getWidth(); + } + + // documentation inherited from interface + public int getIconHeight () + { + return _tile.getHeight(); + } + + /** The tile used to render this icon. */ + protected Tile _tile; +} diff --git a/src/java/com/threerings/media/tile/TileManager.java b/src/java/com/threerings/media/tile/TileManager.java new file mode 100644 index 00000000..5f66b057 --- /dev/null +++ b/src/java/com/threerings/media/tile/TileManager.java @@ -0,0 +1,247 @@ +// +// $Id: TileManager.java 3392 2005-03-10 01:30:34Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.lang.ref.SoftReference; + +import java.util.HashMap; + +import com.samskivert.io.PersistenceException; + +import com.threerings.media.Log; +import com.threerings.media.image.ImageManager; + +/** + * The tile manager provides a simplified interface for retrieving and + * caching tiles. Tiles can be loaded in two different ways. An + * application can load a tileset by hand, specifying the path to the + * tileset image and all of the tileset metadata necessary for extracting + * the image tiles, or it can provide a tileset repository which loads up + * tilesets using whatever repository mechanism is implemented by the + * supplied repository. In the latter case, tilesets are loaded by a + * unique identifier. + * + *

Loading tilesets by hand is intended for things like toolbar icons + * or games with a single set of tiles (think Stratego, for example). + * Loading tilesets from a repository supports games with vast numbers of + * tiles to which more tiles may be added on the fly (think the tiles for + * an isometric-display graphical MUD). + */ +public class TileManager +{ + /** + * Creates a tile manager and provides it with a reference to the + * image manager from which it will load tileset images. + * + * @param imgr the image manager via which the tile manager will + * decode and cache images. + */ + public TileManager (ImageManager imgr) + { + _imgr = imgr; + _defaultProvider = new IMImageProvider(_imgr, (String)null); + } + + /** + * Loads up a tileset from the specified image with the specified + * metadata parameters. + */ + public UniformTileSet loadTileSet (String imgPath, int width, int height) + { + return loadCachedTileSet("", imgPath, width, height); + } + + /** + * Loads up a tileset from the specified image (located in the + * specified resource set) with the specified metadata parameters. + */ + public UniformTileSet loadTileSet ( + String rset, String imgPath, int width, int height) + { + return loadTileSet( + getImageProvider(rset), rset, imgPath, width, height); + } + + /** + */ + public UniformTileSet loadTileSet ( + ImageProvider improv, String improvKey, String imgPath, + int width, int height) + { + UniformTileSet uts = loadCachedTileSet( + improvKey, imgPath, width, height); + uts.setImageProvider(improv); + return uts; + } + + /** + * Returns an image provider that will load images from the specified + * resource set. + */ + public ImageProvider getImageProvider (String rset) + { + return new IMImageProvider(_imgr, rset); + } + + /** + * Used to load and cache tilesets loaded via {@link #loadTileSet}. + */ + protected UniformTileSet loadCachedTileSet ( + String bundle, String imgPath, int width, int height) + { + String key = bundle + "::" + imgPath; + SoftReference ref = (SoftReference) _handcache.get(key); + UniformTileSet uts = (ref == null) ? null + : (UniformTileSet) ref.get(); + if (uts == null) { + uts = new UniformTileSet(); + uts.setImageProvider(_defaultProvider); + uts.setImagePath(imgPath); + uts.setWidth(width); + uts.setHeight(height); + _handcache.put(key, new SoftReference(uts)); + } + return uts; + } + + /** + * Sets the tileset repository that will be used by the tile manager + * when tiles are requested by tileset id. + */ + public void setTileSetRepository (TileSetRepository setrep) + { + _setrep = setrep; + } + + /** + * Returns the tileset repository currently in use. + */ + public TileSetRepository getTileSetRepository () + { + return _setrep; + } + + /** + * Returns the tileset with the specified id. Tilesets are fetched + * from the tileset repository supplied via {@link + * #setTileSetRepository}, and are subsequently cached. + * + * @param tileSetId the unique identifier for the desired tileset. + * + * @exception NoSuchTileSetException thrown if no tileset exists with + * the specified id or if an underlying error occurs with the tileset + * repository's persistence mechanism. + */ + public TileSet getTileSet (int tileSetId) + throws NoSuchTileSetException + { + // make sure we have a repository configured + if (_setrep == null) { + throw new NoSuchTileSetException(tileSetId); + } + + try { + return _setrep.getTileSet(tileSetId); + } catch (PersistenceException pe) { + Log.warning("Failure loading tileset [id=" + tileSetId + + ", error=" + pe + "]."); + throw new NoSuchTileSetException(tileSetId); + } + } + + /** + * Returns the tileset with the specified name. + * + * @throws NoSuchTileSetException if no tileset with the specified + * name is available via our configured tile set repository. + */ + public TileSet getTileSet (String name) + throws NoSuchTileSetException + { + // make sure we have a repository configured + if (_setrep == null) { + throw new NoSuchTileSetException(name); + } + + try { + return _setrep.getTileSet(name); + } catch (PersistenceException pe) { + Log.warning("Failure loading tileset [name=" + name + + ", error=" + pe + "]."); + throw new NoSuchTileSetException(name); + } + } + + /** + * Returns the {@link Tile} object with the specified fully qualified + * tile id. + * + * @see TileUtil#getFQTileId + */ + public Tile getTile (int fqTileId) + throws NoSuchTileSetException + { + return getTile(TileUtil.getTileSetId(fqTileId), + TileUtil.getTileIndex(fqTileId), null); + } + + /** + * Returns the {@link Tile} object with the specified fully qualified + * tile id. The supplied colorizer will be used to recolor the tile. + * + * @see TileUtil#getFQTileId + */ + public Tile getTile (int fqTileId, TileSet.Colorizer rizer) + throws NoSuchTileSetException + { + return getTile(TileUtil.getTileSetId(fqTileId), + TileUtil.getTileIndex(fqTileId), rizer); + } + + /** + * Returns the {@link Tile} object from the specified tileset at the + * specified index. + * + * @param tileSetId the tileset id. + * @param tileIndex the index of the tile to be retrieved. + * + * @return the tile object. + */ + public Tile getTile (int tileSetId, int tileIndex, TileSet.Colorizer rizer) + throws NoSuchTileSetException + { + TileSet set = getTileSet(tileSetId); + return set.getTile(tileIndex, rizer); + } + + /** The entity through which we decode and cache images. */ + protected ImageManager _imgr; + + /** A cache of tilesets that have been loaded by hand. */ + protected HashMap _handcache = new HashMap(); + + /** The tile set repository. */ + protected TileSetRepository _setrep; + + /** Used to load tileset images from the default resource source. */ + protected ImageProvider _defaultProvider; +} diff --git a/src/java/com/threerings/media/tile/TileMultiFrameImage.java b/src/java/com/threerings/media/tile/TileMultiFrameImage.java new file mode 100644 index 00000000..c02b5e64 --- /dev/null +++ b/src/java/com/threerings/media/tile/TileMultiFrameImage.java @@ -0,0 +1,84 @@ +// +// $Id: TileMultiFrameImage.java 3559 2005-05-17 00:42:12Z tedv $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Graphics2D; + +import com.threerings.media.image.Colorization; +import com.threerings.media.util.MultiFrameImage; + +/** + * A {@link MultiFrameImage} implementation that obtains its image frames + * from a tileset. + */ +public class TileMultiFrameImage implements MultiFrameImage +{ + /** + * Creates a tile MFI which will obtain its image frames from the + * specified source tileset. + */ + public TileMultiFrameImage (TileSet source) + { + _source = source; + } + + /** + * Creates a recoolored tile MFI which will obtain its image frames + * from the specified source tileset. + */ + public TileMultiFrameImage (TileSet source, Colorization[] zations) + { + this(source.clone(zations)); + } + + // documentation inherited from interface + public int getFrameCount () + { + return _source.getTileCount(); + } + + // documentation inherited from interface + public int getWidth (int index) + { + return _source.getTile(index).getWidth(); + } + + // documentation inherited from interface + public int getHeight (int index) + { + return _source.getTile(index).getHeight(); + } + + // documentation inherited from interface + public void paintFrame (Graphics2D g, int index, int x, int y) + { + _source.getTile(index).paint(g, x, y); + } + + // documentation inherited from interface + public boolean hitTest (int index, int x, int y) + { + return _source.getTile(index).hitTest(x, y); + } + + protected TileSet _source; +} diff --git a/src/java/com/threerings/media/tile/TileSet.java b/src/java/com/threerings/media/tile/TileSet.java new file mode 100644 index 00000000..546a28ac --- /dev/null +++ b/src/java/com/threerings/media/tile/TileSet.java @@ -0,0 +1,454 @@ +// +// $Id: TileSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.io.Serializable; +import java.lang.ref.SoftReference; +import java.util.HashMap; +import java.util.Iterator; + +import com.samskivert.util.StringUtil; +import com.samskivert.util.Throttle; + +import com.threerings.media.Log; +import com.threerings.media.image.Colorization; +import com.threerings.media.image.Mirage; +import com.threerings.media.image.ImageUtil; +import com.threerings.media.image.BufferedMirage; +import com.threerings.media.util.MultiFrameImage; +import com.threerings.media.util.MultiFrameImageImpl; + +/** + * A tileset stores information on a single logical set of tiles. It + * provides a clean interface for the {@link TileManager} or other + * entities to retrieve individual tiles from the tile set and + * encapsulates the potentially sophisticated process of extracting the + * tile image from a composite tileset image. + * + *

Tiles are referenced by their tile id. The tile id is essentially + * the tile number, assuming the tile at the top-left of the image is tile + * id zero and tiles are numbered, in ascending order, left to right, top + * to bottom. + * + *

This class is serializable and will be serialized, so derived + * classes should be sure to mark non-persistent fields as + * transient. + */ +public abstract class TileSet + implements Cloneable, Serializable +{ + /** Used to assign colorizations to tiles that require them. */ + public static interface Colorizer + { + /** + * Returns the colorization to be used for the specified named + * colorization class. + * + * @param index the index of the colorization being requested in + * the tileset's colorization list. + */ + public Colorization getColorization (int index, String zation); + } + + /** + * Configures this tileset with an image provider that it can use to + * load its tileset image. This will be called automatically when the + * tileset is fetched via the {@link TileManager}. + */ + public void setImageProvider (ImageProvider improv) + { + _improv = improv; + } + + /** + * Returns the tileset name. + */ + public String getName () + { + return (_name == null) ? _imagePath : _name; + } + + /** + * Specifies the tileset name. + */ + public void setName (String name) + { + _name = name; + } + + /** + * Sets the path to the image that will be used by this tileset. This + * must be called before the first call to {@link #getTile}. + */ + public void setImagePath (String imagePath) + { + _imagePath = imagePath; + } + + /** + * Returns the path to the composite image used by this tileset. + */ + public String getImagePath () + { + return _imagePath; + } + + /** + * Returns the number of tiles in the tileset. + */ + public abstract int getTileCount (); + + /** + * Creates a copy of this tileset which will apply the supplied + * colorizations to its tileset image when creating tiles. + */ + public TileSet clone (Colorization[] zations) + { + try { + TileSet tset = (TileSet)clone(); + tset._zations = zations; + return tset; + + } catch (CloneNotSupportedException cnse) { + Log.warning("Unable to clone tileset prior to colorization " + + "[tset=" + this + + ", zations=" + StringUtil.toString(zations) + + ", error=" + cnse + "]."); + return null; + } + } + + /** + * Returns a new tileset that is a clone of this tileset with the + * image path updated to reference the given path. Useful for + * configuring a single tileset and then generating additional + * tilesets with new images with the same configuration. + */ + public TileSet clone (String imagePath) + throws CloneNotSupportedException + { + TileSet dup = (TileSet)clone(); + dup.setImagePath(imagePath); + return dup; + } + + /** + * Equivalent to {@link #getTile(int,Colorizer)} with a null + * Colorizer argument. + */ + public Tile getTile (int tileIndex) + { + return getTile(tileIndex, _zations); + } + + /** + * Creates a {@link Tile} object from this tileset corresponding to + * the specified tile id and returns that tile. A null tile will never + * be returned, but one with an error image may be returned if a + * problem occurs loading the underlying tileset image. + * + * @param tileIndex the index of the tile in the tileset. Tile indexes + * start with zero as the upper left tile and increase by one as the + * tiles move left to right and top to bottom over the source image. + * @param rizer an entity that will be used to obtain colorizations + * for tilesets that are recolorizable. Passing null if the tileset is + * known not to be recolorizable is valid. + * + * @return the tile object. + */ + public Tile getTile (int tileIndex, Colorizer rizer) + { + return getTile(tileIndex, getColorizations(tileIndex, rizer)); + } + + /** + * Creates a {@link Tile} object from this tileset corresponding to + * the specified tile id and returns that tile. A null tile will never + * be returned, but one with an error image may be returned if a + * problem occurs loading the underlying tileset image. + * + * @param tileIndex the index of the tile in the tileset. Tile indexes + * start with zero as the upper left tile and increase by one as the + * tiles move left to right and top to bottom over the source image. + * @param zations colorizations to be applied to the tile image prior + * to returning it. These may be null for uncolorized images. + * + * @return the tile object. + */ + public Tile getTile (int tileIndex, Colorization[] zations) + { + Tile tile = null; + + // first look in the active set; if it's in use by anyone or in + // the cache, it will be in the active set + synchronized (_atiles) { + _key.tileSet = this; + _key.tileIndex = tileIndex; + _key.zations = zations; + SoftReference sref = (SoftReference)_atiles.get(_key); + if (sref != null) { + tile = (Tile)sref.get(); + } + } + + // if it's not in the active set, it's not in memory; so load it + if (tile == null) { + tile = createTile(); + tile.key = new Tile.Key(this, tileIndex, zations); + initTile(tile, tileIndex, zations); + synchronized (_atiles) { + _atiles.put(tile.key, new SoftReference(tile)); + } + } + + // periodically report our image cache performance + reportCachePerformance(); + + return tile; + } + + /** + * Returns a prepared version of the image that would be used by the + * tile at the specified index. Because tilesets are often used simply + * to provide access to a collection of uniform images, this method is + * provided to bypass the creation of a {@link Tile} object when all + * that is desired is access to the underlying image. + */ + public Mirage getTileMirage (int tileIndex) + { + return getTileMirage(tileIndex, getColorizations(tileIndex, null)); + } + + /** + * Returns a prepared version of the image that would be used by the + * tile at the specified index. Because tilesets are often used simply + * to provide access to a collection of uniform images, this method is + * provided to bypass the creation of a {@link Tile} object when all + * that is desired is access to the underlying image. + */ + public Mirage getTileMirage (int tileIndex, Colorization[] zations) + { + Rectangle bounds = computeTileBounds(tileIndex); + Mirage mirage = null; + if (checkTileIndex(tileIndex)) { + if (_improv == null) { + Log.warning("Aiya! Tile set missing image provider " + + "[path=" + _imagePath + "]."); + } else { + mirage = _improv.getTileImage(_imagePath, bounds, zations); + } + } + if (mirage == null) { + mirage = new BufferedMirage( + ImageUtil.createErrorImage(bounds.width, bounds.height)); + } + return mirage; + } + + /** + * Returns the entire, raw, uncut, unprepared tileset source image. + * Don't use this method unless you know what you're doing! This image + * should not be rendered directly to the screen, you should obtain a + * tile ({@link #getTile}), or a tile mirage ({@link #getTileMirage}). + */ + public BufferedImage getRawTileSetImage () + { + return _improv.getTileSetImage(_imagePath, _zations); + } + + /** + * Returns the raw (unprepared) image that would be used by the tile + * at the specified index. Don't use this method unless you know what + * you're doing! If you're going to be painting this image onto the + * screen directly, use {@link #getTileMirage} because that prepares + * the image for display. Only use this if you're going to do further + * processing and prepare the subsequent image for display onscreen. + */ + public BufferedImage getRawTileImage (int tileIndex) + { + Rectangle bounds = computeTileBounds(tileIndex); + BufferedImage img = null; + if (checkTileIndex(tileIndex)) { + BufferedImage timg = getRawTileSetImage(); + if (timg != null) { + img = timg.getSubimage(bounds.x, bounds.y, + bounds.width, bounds.height); + } else { + Log.warning("Missing source image " + this); + } + } + if (img == null) { + img = ImageUtil.createErrorImage(bounds.width, bounds.height); + } + return img; + } + + /** + * Returns colorizations for the specified tile image. The default is + * to return any colorizations associated with the tileset via a call + * to {@link #clone(Colorization[])}, however derived classes may have + * dynamic colorization policies that look up colorization assignments + * from the supplied colorizer. + */ + protected Colorization[] getColorizations (int tileIndex, Colorizer rizer) + { + return _zations; + } + + /** + * Used to ensure that the specified tile index is valid. + */ + protected boolean checkTileIndex (int tileIndex) + { + int tcount = getTileCount(); + if (tileIndex >= 0 && tileIndex < tcount) { + return true; + } else { + Log.warning("Requested invalid tile [tset=" + this + + ", index=" + tileIndex + "]."); + Thread.dumpStack(); + return false; + } + } + + /** + * Computes and returns the bounds for the specified tile based on the + * mechanism used by the derived class to do such things. The width + * and height of the bounds should be the size of the tile image and + * the x and y offset should be the offset in the tileset image for + * the image data of the specified tile. + * + * @param tileIndex the index of the tile whose bounds are to be + * computed. + */ + protected abstract Rectangle computeTileBounds (int tileIndex); + + /** + * Creates a blank tile of the appropriate type for this tileset. + * + * @return a blank tile ready to be populated with its image and + * metadata. + */ + protected Tile createTile () + { + return new Tile(); + } + + /** + * Initializes the supplied tile. Derived classes can override this + * method to add in their own tile information, but should be sure to + * call super.initTile(). + * + * @param tile the tile to initialize. + * @param tileIndex the index of the tile. + * @param zations the colorizations to be used when generating the + * tile image. + */ + protected void initTile (Tile tile, int tileIndex, Colorization[] zations) + { + if (_improv != null) { + tile.setImage(getTileMirage(tileIndex, zations)); + } + } + + /** + * Generates a string representation of the tileset information. + */ + public String toString () + { + StringBuilder buf = new StringBuilder("["); + toString(buf); + return buf.append("]").toString(); + } + + /** + * Reports statistics detailing the image manager cache performance + * and the current size of the cached images. + */ + protected void reportCachePerformance () + { + if (/* Log.getLevel() != Log.log.DEBUG || */ + _improv == null || + _cacheStatThrottle.throttleOp()) { + return; + } + + // compute our estimated memory usage + long amem = 0; + int asize = 0; + synchronized (_atiles) { + // first total up the active tiles + Iterator iter = _atiles.values().iterator(); + while (iter.hasNext()) { + SoftReference sref = (SoftReference)iter.next(); + Tile tile = (Tile)sref.get(); + if (tile != null) { + asize++; + amem += tile.getEstimatedMemoryUsage(); + } + } + } + Log.info("Tile caches [amem=" + (amem / 1024) + "k" + + ", tmem=" + (Tile._totalTileMemory / 1024) + "k" + + ", seen=" + _atiles.size() + ", asize=" + asize + "]."); + } + + /** + * Derived classes can override this, calling + * super.toString(buf) and then appending additional + * information to the buffer. + */ + protected void toString (StringBuilder buf) + { + buf.append("name=").append(_name); + buf.append(", path=").append(_imagePath); + buf.append(", tileCount=").append(getTileCount()); + } + + /** The path to the file containing the tile images. */ + protected String _imagePath; + + /** The tileset name. */ + protected String _name; + + /** Colorizations to be applied to tiles created from this tileset. */ + protected transient Colorization[] _zations; + + /** The entity from which we obtain our tile image. */ + protected transient ImageProvider _improv; + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; + + /** A map containing weak references to all "active" tiles. */ + protected static HashMap _atiles = new HashMap(); + + /** A key used to look things up in the cache without creating + * craploads of keys unduly. */ + protected static Tile.Key _key = new Tile.Key(null, 0, null); + + /** Throttle our cache status logging to once every 300 seconds. */ + protected static Throttle _cacheStatThrottle = new Throttle(1, 300000L); +} diff --git a/src/java/com/threerings/media/tile/TileSetIDBroker.java b/src/java/com/threerings/media/tile/TileSetIDBroker.java new file mode 100644 index 00000000..fd250d78 --- /dev/null +++ b/src/java/com/threerings/media/tile/TileSetIDBroker.java @@ -0,0 +1,70 @@ +// +// $Id: TileSetIDBroker.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import com.samskivert.io.PersistenceException; + +/** + * Brokers tileset ids. The tileset repository interface makes available a + * collection of tilesets based on a unique identifier. The expectation is + * that a collection of tilesets will be used to populate a repository and + * in that population process, tileset ids will be assigned to the + * tilesets. The tileset id broker system provides a means by which named + * tilesets can be mapped consistently to a set of tileset ids. Humans can + * then be responsible for assigning unique names to the tilesets and the + * broker will ensure that those names map to unique ids that won't change + * if the repository is rebuilt from the source tilesets. + */ +public interface TileSetIDBroker +{ + /** + * Returns the unique identifier for the named tileset. If no + * identifier has yet been assigned to the specified named tileset, + * one should be assigned and returned. + * + * @exception PersistenceException thrown if an error occurs + * communicating with the underlying persistence mechanism used to + * store the name to id mappings. + */ + public int getTileSetID (String tileSetName) + throws PersistenceException; + + /** + * Returns true if the specified tileset name is currently mapped to + * some value by this broker. + * + * @exception PersistenceException thrown if an error occurs + * communicating with the underlying persistence mechanism used to + * store the name to id mappings. + */ + public boolean tileSetMapped (String tileSetName) + throws PersistenceException; + + /** + * When the user of a tilset id broker is done obtaining tileset ids, + * it must call this method to give the tileset id broker an + * opportunity to flush any newly created tileset ids back to its + * persistent store. + */ + public void commit () + throws PersistenceException; +} diff --git a/src/java/com/threerings/media/tile/TileSetRepository.java b/src/java/com/threerings/media/tile/TileSetRepository.java new file mode 100644 index 00000000..c9f49dcb --- /dev/null +++ b/src/java/com/threerings/media/tile/TileSetRepository.java @@ -0,0 +1,86 @@ +// +// $Id: TileSetRepository.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.util.Iterator; + +import com.samskivert.io.PersistenceException; + +/** + * The tileset repository interface should be implemented by classes that + * provide access to tilesets keyed on a unique tileset identifier. The + * tileset id space is up to the repository implementation, which may or + * may not desire to use a {@link TileSetIDBroker} to manage the space. + */ +public interface TileSetRepository +{ + /** + * Returns an iterator over the identifiers of all {@link TileSet} + * objects available. + */ + public Iterator enumerateTileSetIds () + throws PersistenceException; + + /** + * Returns an iterator over all {@link TileSet} objects available. + */ + public Iterator enumerateTileSets () + throws PersistenceException; + + /** + * Returns the {@link TileSet} with the specified tile set + * identifier. The repository is responsible for configuring the tile + * set with an image provider. + * + * @exception NoSuchTileSetException thrown if no tileset exists with + * the specified identifier. + * @exception PersistenceException thrown if an error occurs + * communicating with the underlying persistence mechanism. + */ + public TileSet getTileSet (int tileSetId) + throws NoSuchTileSetException, PersistenceException; + + /** + * Returns the unique identifier of the {@link TileSet} with the + * specified tile set name. + * + * @exception NoSuchTileSetException thrown if no tileset exists with + * the specified name. + * @exception PersistenceException thrown if an error occurs + * communicating with the underlying persistence mechanism. + */ + public int getTileSetId (String setName) + throws NoSuchTileSetException, PersistenceException; + + /** + * Returns the {@link TileSet} with the specified tile set name. The + * repository is responsible for configuring the tile set with an + * image provider. + * + * @exception NoSuchTileSetException thrown if no tileset exists with + * the specified name. + * @exception PersistenceException thrown if an error occurs + * communicating with the underlying persistence mechanism. + */ + public TileSet getTileSet (String setName) + throws NoSuchTileSetException, PersistenceException; +} diff --git a/src/java/com/threerings/media/tile/TileUtil.java b/src/java/com/threerings/media/tile/TileUtil.java new file mode 100644 index 00000000..6ca17090 --- /dev/null +++ b/src/java/com/threerings/media/tile/TileUtil.java @@ -0,0 +1,70 @@ +// +// $Id: TileUtil.java 3628 2005-06-28 17:42:55Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +/** + * Miscellaneous utility routines for working with tiles. + */ +public class TileUtil +{ + /** + * Generates a fully-qualified tile id given the supplied tileset id + * and tile index. + */ + public static int getFQTileId (int tileSetId, int tileIndex) + { + return (tileSetId << 16) | tileIndex; + } + + /** + * Extracts the tile set id from the supplied fully qualified tile id. + */ + public static int getTileSetId (int fqTileId) + { + return (fqTileId >> 16); + } + + /** + * Extracts the tile index from the supplied fully qualified tile id. + */ + public static int getTileIndex (int fqTileId) + { + return (fqTileId & 0xFFFF); + } + + /** + * Compute some hash value for "randomizing" tileset picks + * based on x and y coordinates. + * + * @return a positive, seemingly random number based on x and y. + */ + public static int getTileHash (int x, int y) + { + long seed = ((x ^ y) ^ MULTIPLIER) & MASK; + long hash = (seed * MULTIPLIER + ADDEND) & MASK; + return (int) (hash >>> 30); + } + + protected static final long MULTIPLIER = 0x5DEECE66DL; + protected static final long ADDEND = 0xBL; + protected static final long MASK = (1L << 48) - 1; +} diff --git a/src/java/com/threerings/media/tile/TrimmedObjectTileSet.java b/src/java/com/threerings/media/tile/TrimmedObjectTileSet.java new file mode 100644 index 00000000..5318c76d --- /dev/null +++ b/src/java/com/threerings/media/tile/TrimmedObjectTileSet.java @@ -0,0 +1,301 @@ +// +// $Id: TrimmedObjectTileSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Rectangle; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Serializable; + +import com.samskivert.util.ListUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.media.image.Colorization; +import com.threerings.media.tile.util.TileSetTrimmer; + +/** + * An object tileset in which the objects have been trimmed to the + * smallest possible images that still contain all of their + * non-transparent pixels. The objects' origins are adjusted so that the + * objects otherwise behave exactly as the untrimmed objects and are thus + * interchangeable (and more memory efficient). + */ +public class TrimmedObjectTileSet extends TileSet + implements RecolorableTileSet +{ + // documentation inherited + public int getTileCount () + { + return _bounds.length; + } + + /** + * Returns the x coordinate of the spot associated with the specified + * tile index. + */ + public int getXSpot (int tileIdx) + { + return (_bits == null) ? 0 : _bits[tileIdx].xspot; + } + + /** + * Returns the y coordinate of the spot associated with the specified + * tile index. + */ + public int getYSpot (int tileIdx) + { + return (_bits == null) ? 0 : _bits[tileIdx].yspot; + } + + /** + * Returns the orientation of the spot associated with the specified + * tile index, or -1 if the object has no associated + * spot. + */ + public int getSpotOrient (int tileIdx) + { + return (_bits == null) ? -1 : _bits[tileIdx].sorient; + } + + /** + * Returns the constraints associated with the specified tile index, or + * null if the object has no associated constraints. + */ + public String[] getConstraints (int tileIdx) + { + return (_bits == null) ? null : _bits[tileIdx].constraints; + } + + /** + * Checks whether the tile at the specified index has the given constraint. + */ + public boolean hasConstraint (int tileIdx, String constraint) + { + return (_bits == null || _bits[tileIdx].constraints == null) ? false : + ListUtil.contains(_bits[tileIdx].constraints, constraint); + } + + // documentation inherited from interface RecolorableTileSet + public String[] getColorizations () + { + return _zations; + } + + /** + * Returns the base width for the specified object index. + */ + public int getBaseWidth (int tileIdx) + { + return _ometrics[tileIdx].width; + } + + /** + * Returns the base height for the specified object index. + */ + public int getBaseHeight (int tileIdx) + { + return _ometrics[tileIdx].height; + } + + // documentation inherited + protected Rectangle computeTileBounds (int tileIndex) + { + return _bounds[tileIndex]; + } + + // documentation inherited + protected Colorization[] getColorizations (int tileIndex, Colorizer rizer) + { + Colorization[] zations = null; + if (rizer != null && _zations != null) { + zations = new Colorization[_zations.length]; + for (int ii = 0; ii < _zations.length; ii++) { + zations[ii] = rizer.getColorization(ii, _zations[ii]); + } + } + return zations; + } + + // documentation inherited + protected Tile createTile () + { + return new ObjectTile(); + } + + // documentation inherited + protected void initTile (Tile tile, int tileIndex, Colorization[] zations) + { + super.initTile(tile, tileIndex, zations); + + ObjectTile otile = (ObjectTile)tile; + otile.setBase(_ometrics[tileIndex].width, _ometrics[tileIndex].height); + otile.setOrigin(_ometrics[tileIndex].x, _ometrics[tileIndex].y); + if (_bits != null) { + Bits bits = _bits[tileIndex]; + otile.setPriority(bits.priority); + if (bits.sorient != -1) { + otile.setSpot(bits.xspot, bits.yspot, bits.sorient); + } + otile.setConstraints(bits.constraints); + } + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", ometrics=").append(StringUtil.toString(_ometrics)); + buf.append(", bounds=").append(StringUtil.toString(_bounds)); + buf.append(", bits=").append(StringUtil.toString(_bits)); + buf.append(", zations=").append(StringUtil.toString(_zations)); + } + + /** + * Creates a trimmed object tileset from the supplied source object + * tileset. The image path must be set by hand to the appropriate path + * based on where the image data that is written to the + * destImage parameter is actually stored on the file + * system. See {@link TileSetTrimmer#trimTileSet} for further + * information. + */ + public static TrimmedObjectTileSet trimObjectTileSet ( + ObjectTileSet source, OutputStream destImage) + throws IOException + { + final TrimmedObjectTileSet tset = new TrimmedObjectTileSet(); + tset.setName(source.getName()); + int tcount = source.getTileCount(); + +// System.out.println("Trimming object tile set " + +// "[source=" + source + "]."); + + // create our metrics arrays + tset._bounds = new Rectangle[tcount]; + tset._ometrics = new Rectangle[tcount]; + + // create our bits if needed + if (source._priorities != null || + source._xspots != null || + source._constraints != null) { + tset._bits = new Bits[tcount]; + } + + // copy our colorizations + tset._zations = source.getColorizations(); + + // fill in the original object metrics + for (int ii = 0; ii < tcount; ii++) { + tset._ometrics[ii] = new Rectangle(); + if (source._xorigins != null) { + tset._ometrics[ii].x = source._xorigins[ii]; + } + if (source._yorigins != null) { + tset._ometrics[ii].y = source._yorigins[ii]; + } + tset._ometrics[ii].width = source._owidths[ii]; + tset._ometrics[ii].height = source._oheights[ii]; + + // fill in our bits + if (tset._bits != null) { + tset._bits[ii] = new Bits(); + } + if (source._priorities != null) { + tset._bits[ii].priority = source._priorities[ii]; + } + if (source._xspots != null) { + tset._bits[ii].xspot = source._xspots[ii]; + tset._bits[ii].yspot = source._yspots[ii]; + tset._bits[ii].sorient = source._sorients[ii]; + } + if (source._constraints != null) { + tset._bits[ii].constraints = source._constraints[ii]; + } + } + + // create the trimmed tileset image + TileSetTrimmer.TrimMetricsReceiver tmr = + new TileSetTrimmer.TrimMetricsReceiver() { + public void trimmedTile (int tileIndex, int imageX, int imageY, + int trimX, int trimY, + int trimWidth, int trimHeight) { + tset._ometrics[tileIndex].x -= trimX; + tset._ometrics[tileIndex].y -= trimY; + tset._bounds[tileIndex] = + new Rectangle(imageX, imageY, trimWidth, trimHeight); + } + }; + TileSetTrimmer.trimTileSet(source, destImage, tmr); + +// Log.info("Trimmed object tileset " + +// "[bounds=" + StringUtil.toString(tset._bounds) + +// ", metrics=" + StringUtil.toString(tset._ometrics) + "]."); + + return tset; + } + + /** Extra bits related to object tiles. */ + protected static class Bits implements Serializable + { + /** The default render priority for this object. */ + public byte priority; + + /** The x coordinate of the "spot" associated with this object. */ + public short xspot; + + /** The y coordinate of the "spot" associated with this object. */ + public short yspot; + + /** The orientation of the "spot" associated with this object. */ + public byte sorient = -1; + + /** The constraints associated with this object. */ + public String[] constraints; + + /** Generates a string representation of this instance. */ + public String toString () + { + return StringUtil.fieldsToString(this); + } + + /** Increase this value when object's serialized state is impacted + * by a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 2; + } + + /** Contains the width and height of each object tile and the offset + * into the tileset image of their image data. */ + protected Rectangle[] _bounds; + + /** Contains the origin offset for each object tile and the object + * footprint width and height (in tile units). */ + protected Rectangle[] _ometrics; + + /** Extra bits relating to our objects. */ + protected Bits[] _bits; + + /** Colorization classes that apply to our objects. */ + protected String[] _zations; + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/media/tile/TrimmedTile.java b/src/java/com/threerings/media/tile/TrimmedTile.java new file mode 100644 index 00000000..88ee888a --- /dev/null +++ b/src/java/com/threerings/media/tile/TrimmedTile.java @@ -0,0 +1,91 @@ +// +// $Id: TrimmedTile.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.samskivert.util.StringUtil; + +/** + * Behaves just like a regular tile, but contains a "trimmed" image which + * is one where the source image has been trimmed to the smallest + * rectangle that contains all the non-transparent pixels of the original + * image. + */ +public class TrimmedTile extends Tile +{ + /** + * Sets the trimmed bounds of this tile. + * + * @param tbounds contains the width and height of the + * untrimmed tile, but the x and y offset of the + * trimmed tile image in the original untrimmed tile image. + */ + public void setTrimmedBounds (Rectangle tbounds) + { + _tbounds = tbounds; + } + + // documentation inherited + public int getWidth () + { + return _tbounds.width; + } + + // documentation inherited + public int getHeight () + { + return _tbounds.height; + } + + // documentation inherited + public void paint (Graphics2D gfx, int x, int y) + { + _mirage.paint(gfx, x + _tbounds.x, y + _tbounds.y); + } + + /** + * Fills in the bounds of the trimmed image within the coordinate + * system defined by the complete virtual tile. + */ + public void getTrimmedBounds (Rectangle tbounds) + { + tbounds.setBounds(_tbounds.x, _tbounds.y, + _mirage.getWidth(), _mirage.getHeight()); + } + + // documentation inherited + public boolean hitTest (int x, int y) + { + return super.hitTest(x - _tbounds.x, y - _tbounds.y); + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + buf.append(", tbounds=").append(StringUtil.toString(_tbounds)); + } + + /** Our extra trimmed image dimension information. */ + protected Rectangle _tbounds; +} diff --git a/src/java/com/threerings/media/tile/TrimmedTileSet.java b/src/java/com/threerings/media/tile/TrimmedTileSet.java new file mode 100644 index 00000000..c60a9ed4 --- /dev/null +++ b/src/java/com/threerings/media/tile/TrimmedTileSet.java @@ -0,0 +1,114 @@ +// +// $Id: TrimmedTileSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Rectangle; + +import java.io.IOException; +import java.io.OutputStream; + +import com.threerings.media.image.Colorization; +import com.threerings.media.tile.util.TileSetTrimmer; + +/** + * Contains the necessary information to create a set of trimmed tiles + * from a base image and the associated trim metrics. + */ +public class TrimmedTileSet extends TileSet +{ + // documentation inherited + public int getTileCount () + { + return _obounds.length; + } + + // documentation inherited + protected Rectangle computeTileBounds (int tileIndex) + { + return _obounds[tileIndex]; + } + + // documentation inherited + protected Tile createTile () + { + return new TrimmedTile(); + } + + // documentation inherited + protected void initTile (Tile tile, int tileIndex, Colorization[] zations) + { + super.initTile(tile, tileIndex, zations); + ((TrimmedTile)tile).setTrimmedBounds(_tbounds[tileIndex]); + } + + /** + * Creates a trimmed tileset from the supplied source tileset. The + * image path must be set by hand to the appropriate path based on + * where the image data that is written to the destImage + * parameter is actually stored on the file system. See {@link + * TileSetTrimmer#trimTileSet} for further information. + */ + public static TrimmedTileSet trimTileSet ( + TileSet source, OutputStream destImage) + throws IOException + { + final TrimmedTileSet tset = new TrimmedTileSet(); + tset.setName(source.getName()); + int tcount = source.getTileCount(); + tset._tbounds = new Rectangle[tcount]; + tset._obounds = new Rectangle[tcount]; + + // grab the dimensions of the original tiles + for (int ii = 0; ii < tcount; ii++) { + tset._tbounds[ii] = source.computeTileBounds(ii); + } + + // create the trimmed tileset image + TileSetTrimmer.TrimMetricsReceiver tmr = + new TileSetTrimmer.TrimMetricsReceiver() { + public void trimmedTile (int tileIndex, int imageX, int imageY, + int trimX, int trimY, + int trimWidth, int trimHeight) { + tset._tbounds[tileIndex].x = trimX; + tset._tbounds[tileIndex].y = trimY; + tset._obounds[tileIndex] = + new Rectangle(imageX, imageY, trimWidth, trimHeight); + } + }; + TileSetTrimmer.trimTileSet(source, destImage, tmr); + + return tset; + } + + /** The width and height of the trimmed tile, and the x and y offset + * of the trimmed image within our tileset image. */ + protected Rectangle[] _obounds; + + /** The width and height of the untrimmed image and the x and y offset + * within the untrimmed image at which the trimmed image should be + * rendered. */ + protected Rectangle[] _tbounds; + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/media/tile/UniformTileSet.java b/src/java/com/threerings/media/tile/UniformTileSet.java new file mode 100644 index 00000000..c3e5fe72 --- /dev/null +++ b/src/java/com/threerings/media/tile/UniformTileSet.java @@ -0,0 +1,103 @@ +// +// $Id: UniformTileSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; + +import com.threerings.geom.GeomUtil; + +/** + * A uniform tileset is one that is composed of tiles that are all the + * same width and height and are arranged into rows, with each row having + * the same number of tiles except possibly the final row which can + * contain the same as or less than the number of tiles contained by the + * previous rows. + */ +public class UniformTileSet extends TileSet +{ + // documentation inherited + public int getTileCount () + { + BufferedImage tsimg = getRawTileSetImage(); + int perRow = tsimg.getWidth() / _width; + int perCol = tsimg.getHeight() / _height; + return perRow * perCol; + } + + /** + * Specifies the width of the tiles in this tileset. + */ + public void setWidth (int width) + { + _width = width; + } + + /** + * Returns the width of the tiles in this tileset. + */ + public int getWidth () + { + return _width; + } + + /** + * Specifies the height of the tiles in this tileset. + */ + public void setHeight (int height) + { + _height = height; + } + + /** + * Returns the height of the tiles in this tileset. + */ + public int getHeight () + { + return _height; + } + + // documentation inherited + protected Rectangle computeTileBounds (int tileIndex) + { + BufferedImage tsimg = getRawTileSetImage(); + return GeomUtil.getTile(tsimg.getWidth(), tsimg.getHeight(), + _width, _height, tileIndex); + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", width=").append(_width); + buf.append(", height=").append(_height); + } + + /** The width (in pixels) of the tiles in this tileset. */ + protected int _width; + + /** The height (in pixels) of the tiles in this tileset. */ + protected int _height; + + /** Our historic serialization version id. */ + private static final long serialVersionUID = 3536616655149232917L; +} diff --git a/src/java/com/threerings/media/tile/bundle/BundleUtil.java b/src/java/com/threerings/media/tile/bundle/BundleUtil.java new file mode 100644 index 00000000..20a6e826 --- /dev/null +++ b/src/java/com/threerings/media/tile/bundle/BundleUtil.java @@ -0,0 +1,82 @@ +// +// $Id: BundleUtil.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.bundle; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; + +import com.samskivert.io.StreamUtil; + +import com.threerings.resource.ResourceBundle; + +/** + * Bundle related utility functions. + */ +public class BundleUtil +{ + /** The path to the metadata resource that we will attempt to load + * from our resource set. */ + public static final String METADATA_PATH = "tsbundles.dat"; + + /** + * Extracts, but does not initialize, a serialized tileset bundle + * instance from the supplied resource bundle. + */ + public static TileSetBundle extractBundle (ResourceBundle bundle) + throws IOException, ClassNotFoundException + { + // unserialize the tileset bundles array + InputStream tbin = null; + try { + tbin = bundle.getResource(METADATA_PATH); + ObjectInputStream oin = new ObjectInputStream( + new BufferedInputStream(tbin)); + TileSetBundle tsb = (TileSetBundle)oin.readObject(); + return tsb; + } finally { + StreamUtil.close(tbin); + } + } + + /** + * Extracts, but does not initialize, a serialized tileset bundle + * instance from the supplied file. + */ + public static TileSetBundle extractBundle (File file) + throws IOException, ClassNotFoundException + { + // unserialize the tileset bundles array + FileInputStream fin = new FileInputStream(file); + try { + ObjectInputStream oin = new ObjectInputStream( + new BufferedInputStream(fin)); + TileSetBundle tsb = (TileSetBundle)oin.readObject(); + return tsb; + } finally { + StreamUtil.close(fin); + } + } +} diff --git a/src/java/com/threerings/media/tile/bundle/BundledTileSetRepository.java b/src/java/com/threerings/media/tile/bundle/BundledTileSetRepository.java new file mode 100644 index 00000000..0b8e0876 --- /dev/null +++ b/src/java/com/threerings/media/tile/bundle/BundledTileSetRepository.java @@ -0,0 +1,232 @@ +// +// $Id: BundledTileSetRepository.java 3608 2005-06-20 22:59:07Z andrzej $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.bundle; + +import java.util.HashMap; +import java.util.Iterator; + +import com.samskivert.io.PersistenceException; +import com.samskivert.util.HashIntMap; +import com.threerings.resource.ResourceBundle; +import com.threerings.resource.ResourceManager; + +import com.threerings.media.Log; +import com.threerings.media.image.ImageManager; +import com.threerings.media.tile.IMImageProvider; +import com.threerings.media.tile.NoSuchTileSetException; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileSetRepository; + +/** + * Loads tileset data from a set of resource bundles. + * + * @see ResourceManager + */ +public class BundledTileSetRepository + implements TileSetRepository +{ + /** + * Constructs a repository which will obtain its resource set from the + * supplied resource manager. + * + * @param rmgr the resource manager from which to obtain our resource + * set. + * @param imgr the image manager through which we will configure the + * tile sets to load their images, or null if image tiles + * should not be loaded (only the tile metadata) + * @param name the name of the resource set from which we will be + * loading our tile data. + */ + public BundledTileSetRepository (final ResourceManager rmgr, + final ImageManager imgr, + final String name) + { + _imgr = imgr; + + // unpack our bundles in the background + new Thread(new Runnable() { + public void run () { + initBundles(rmgr, name); + } + }).start(); + } + + /** + * Initializes our bundles, + */ + protected void initBundles (ResourceManager rmgr, String name) + { + // first we obtain the resource set from which we will load up our + // tileset bundles + ResourceBundle[] rbundles = rmgr.getResourceSet(name); + + // sanity check + if (rbundles == null) { + Log.warning("Unable to fetch tileset resource set " + + "[name=" + name + "]. Perhaps it's not defined " + + "in the resource manager config?"); + return; + } + + HashIntMap idmap = new HashIntMap(); + HashMap namemap = new HashMap(); + + // iterate over the resource bundles in the set, loading up the + // tileset bundles in each resource bundle + for (int i = 0; i < rbundles.length; i++) { + addBundle(idmap, namemap, rbundles[i]); + } + + // fill in our bundles array and wake up any waiters + synchronized (this) { + _idmap = idmap; + _namemap = namemap; + notifyAll(); + } + } + + /** + * Registers the bundle with the tileset repository, overriding any + * bundle with the same id or name. + */ + public void addBundle (ResourceBundle bundle) + { + addBundle(_idmap, _namemap, bundle); + } + + /** + * Extracts the tileset bundle from the supplied resource bundle + * and registers it. + */ + protected void addBundle (HashIntMap idmap, HashMap namemap, + ResourceBundle bundle) + { + try { + TileSetBundle tsb = BundleUtil.extractBundle(bundle); + // initialize it and add it to the list + tsb.init(bundle); + addBundle(idmap, namemap, tsb); + + } catch (Exception e) { + Log.warning("Unable to load tileset bundle '" + + BundleUtil.METADATA_PATH + "' from resource " + + "bundle [rbundle=" + bundle + + ", error=" + e + "]."); + Log.logStackTrace(e); + } + } + + /** + * Adds the tilesets in the supplied bundle to our tileset mapping + * tables. Any tilesets with the same name or id will be overwritten. + */ + protected void addBundle (HashIntMap idmap, HashMap namemap, + TileSetBundle bundle) + { + IMImageProvider improv = (_imgr == null) ? + null : new IMImageProvider(_imgr, bundle); + + // map all of the tilesets in this bundle + for (Iterator iter = bundle.entrySet().iterator(); iter.hasNext(); ) { + HashIntMap.Entry entry = (HashIntMap.Entry)iter.next(); + Integer tsid = (Integer)entry.getKey(); + TileSet tset = (TileSet)entry.getValue(); + tset.setImageProvider(improv); + idmap.put(tsid.intValue(), tset); + namemap.put(tset.getName(), tsid); + } + } + + // documentation inherited from interface + public Iterator enumerateTileSetIds () + throws PersistenceException + { + waitForBundles(); + return _idmap.keySet().iterator(); + } + + // documentation inherited from interface + public Iterator enumerateTileSets () + throws PersistenceException + { + waitForBundles(); + return _idmap.values().iterator(); + } + + // documentation inherited from interface + public TileSet getTileSet (int tileSetId) + throws NoSuchTileSetException, PersistenceException + { + waitForBundles(); + TileSet tset = (TileSet)_idmap.get(tileSetId); + if (tset == null) { + throw new NoSuchTileSetException(tileSetId); + } + return tset; + } + + // documentation inherited from interface + public int getTileSetId (String setName) + throws NoSuchTileSetException, PersistenceException + { + waitForBundles(); + Integer tsid = (Integer)_namemap.get(setName); + if (tsid != null) { + return tsid.intValue(); + } + throw new NoSuchTileSetException(setName); + } + + // documentation inherited from interface + public TileSet getTileSet (String setName) + throws NoSuchTileSetException, PersistenceException + { + waitForBundles(); + TileSet tset = null; + Integer tsid = (Integer)_namemap.get(setName); + if (tsid != null) { + return getTileSet(tsid.intValue()); + } + throw new NoSuchTileSetException(setName); + } + + /** Used to allow bundle unpacking to proceed asynchronously. */ + protected synchronized void waitForBundles () + { + while (_idmap == null) { + try { + wait(); + } catch (InterruptedException ie) { + Log.warning("Interrupted waiting for bundles " + ie); + } + } + } + + /** The image manager via which we load our images. */ + protected ImageManager _imgr; + + /** A mapping from tileset id to tileset. */ + protected HashIntMap _idmap; + + /** A mapping from tileset name to tileset id. */ + protected HashMap _namemap; +} diff --git a/src/java/com/threerings/media/tile/bundle/TileSetBundle.java b/src/java/com/threerings/media/tile/bundle/TileSetBundle.java new file mode 100644 index 00000000..effc9297 --- /dev/null +++ b/src/java/com/threerings/media/tile/bundle/TileSetBundle.java @@ -0,0 +1,148 @@ +// +// $Id: TileSetBundle.java 4007 2006-04-10 08:59:30Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.bundle; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Iterator; + +import javax.imageio.ImageIO; + +import com.samskivert.util.HashIntMap; +import com.threerings.resource.ResourceBundle; + +import com.threerings.media.image.FastImageIO; +import com.threerings.media.image.ImageDataProvider; +import com.threerings.media.tile.TileSet; + +/** + * A tileset bundle is used to load up tilesets by id from a persistent + * bundle of tilesets stored on the local filesystem. + */ +public class TileSetBundle extends HashIntMap + implements Serializable, ImageDataProvider +{ + /** + * Initializes this resource bundle with a reference to the jarfile + * from which it was loaded and from which it can load image data. The + * image manager will be used to decode the images. + */ + public void init (ResourceBundle bundle) + { + _bundle = bundle; + } + + /** + * Returns the bundle file from which our tiles are fetched. + */ + public File getSource () + { + return _bundle.getSource(); + } + + /** + * Adds a tileset to this tileset bundle. + */ + public final void addTileSet (int tileSetId, TileSet set) + { + put(tileSetId, set); + } + + /** + * Retrieves a tileset from this tileset bundle. + */ + public final TileSet getTileSet (int tileSetId) + { + return (TileSet)get(tileSetId); + } + + /** + * Enumerates the tileset ids in this tileset bundle. + */ + public Iterator enumerateTileSetIds () + { + return keySet().iterator(); + } + + /** + * Enumerates the tilesets in this tileset bundle. + */ + public Iterator enumerateTileSets () + { + return values().iterator(); + } + + // documentation inherited from interface + public String getIdent () + { + return "tsb:" + _bundle.getSource(); + } + + // documentation inherited from interface + public BufferedImage loadImage (String path) + throws IOException + { + if (path.endsWith(FastImageIO.FILE_SUFFIX)) { + return FastImageIO.read(_bundle.getResourceFile(path)); + } else { + return ImageIO.read(_bundle.getResourceFile(path)); + } + } + + // custom serialization process + private void writeObject (ObjectOutputStream out) + throws IOException + { + out.writeInt(size()); + + Iterator entries = intEntrySet().iterator(); + while (entries.hasNext()) { + IntEntry entry = (IntEntry)entries.next(); + out.writeInt(entry.getIntKey()); + out.writeObject(entry.getValue()); + } + } + + // custom unserialization process + private void readObject (ObjectInputStream in) + throws IOException, ClassNotFoundException + { + int count = in.readInt(); + + for (int i = 0; i < count; i++) { + int tileSetId = in.readInt(); + TileSet set = (TileSet)in.readObject(); + put(tileSetId, set); + } + } + + /** That from which we load our tile images. */ + protected transient ResourceBundle _bundle; + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 2; + } diff --git a/src/java/com/threerings/media/tile/bundle/tools/DumpBundle.java b/src/java/com/threerings/media/tile/bundle/tools/DumpBundle.java new file mode 100644 index 00000000..0083ea80 --- /dev/null +++ b/src/java/com/threerings/media/tile/bundle/tools/DumpBundle.java @@ -0,0 +1,93 @@ +// +// $Id: DumpBundle.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.bundle.tools; + +import java.io.File; +import java.util.Iterator; + +import com.threerings.resource.ResourceManager; +import com.threerings.resource.ResourceBundle; + +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.bundle.BundleUtil; +import com.threerings.media.tile.bundle.TileSetBundle; + +/** + * Dumps the contents of a tileset bundle to stdout (just the serialized + * object info, not the image data). + */ +public class DumpBundle +{ + public static void main (String[] args) + { + boolean dumpTiles = false; + + if (args.length < 1) { + String usage = "Usage: DumpBundle [-tiles] " + + "(bundle.jar|tsbundle.dat) [...]"; + System.err.println(usage); + System.exit(-1); + } + + // create a resource and image manager in case they want to dump + // the tiles + ResourceManager rmgr = new ResourceManager("rsrc"); + + for (int i = 0; i < args.length; i++) { + // oh the hackery + if (args[i].equals("-tiles")) { + dumpTiles = true; + continue; + } + + File file = new File(args[i]); + try { + TileSetBundle tsb = null; + if (args[i].endsWith(".jar")) { + ResourceBundle bundle = new ResourceBundle(file); + tsb = BundleUtil.extractBundle(bundle); + tsb.init(bundle); + } else { + tsb = BundleUtil.extractBundle(file); + } + + Iterator tsids = tsb.enumerateTileSetIds(); + while (tsids.hasNext()) { + Integer tsid = (Integer)tsids.next(); + TileSet set = tsb.getTileSet(tsid.intValue()); + System.out.println(tsid + " => " + set); + if (dumpTiles) { + for (int t = 0, nn = set.getTileCount(); t < nn; t++) { + System.out.println(" " + t + " => " + + set.getTile(t)); + } + } + } + + } catch (Exception e) { + System.err.println("Error dumping bundle [path=" + args[i] + + ", error=" + e + "]."); + e.printStackTrace(); + } + } + } +} diff --git a/src/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java b/src/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java new file mode 100644 index 00000000..d679b16e --- /dev/null +++ b/src/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java @@ -0,0 +1,482 @@ +// +// $Id: TileSetBundler.java 3788 2005-12-20 02:09:18Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.bundle.tools; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.Deflater; + +import javax.imageio.ImageIO; + +import org.apache.commons.digester.Digester; +import org.apache.commons.io.CopyUtils; +import org.xml.sax.SAXException; + +import com.samskivert.io.PersistenceException; + +import com.threerings.media.Log; +import com.threerings.media.image.FastImageIO; +import com.threerings.media.image.ImageUtil; + +import com.threerings.media.tile.ImageProvider; +import com.threerings.media.tile.ObjectTileSet; +import com.threerings.media.tile.SimpleCachingImageProvider; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileSetIDBroker; +import com.threerings.media.tile.TrimmedObjectTileSet; +import com.threerings.media.tile.UniformTileSet; +import com.threerings.media.tile.bundle.BundleUtil; +import com.threerings.media.tile.bundle.TileSetBundle; +import com.threerings.media.tile.tools.xml.TileSetRuleSet; + +/** + * The tileset bundler is used to create tileset bundles from a set of XML + * tileset descriptions in a bundle description file. The bundles contain + * a serialized representation of the tileset objects along with the + * actual image files referenced by those tilesets. + * + *

The organization of the bundle description file is customizable + * based on the an XML configuration file provided to the tileset bundler + * when constructed. The bundler configuration maps XML paths to tileset + * parsers. An example configuration follows: + * + *

+ * <bundler-config>
+ *   <mapping>
+ *     <path>bundle.tilesets.uniform</path>
+ *     <ruleset>
+ *       com.threerings.media.tile.tools.xml.UniformTileSetRuleSet
+ *     </ruleset>
+ *   </mapping>
+ *   <mapping>
+ *     <path>bundle.tilesets.object</path>
+ *     <ruleset>
+ *       com.threerings.media.tile.tools.xml.ObjectTileSetRuleSet
+ *     </ruleset>
+ *   </mapping>
+ * </bundler-config>
+ * 
+ * + * This configuration would be used to parse a bundle description that + * looked something like the following: + * + *
+ * <bundle>
+ *   <tilesets>
+ *     <uniform>
+ *       <tileset>
+ *         <!-- ... -->
+ *       </tileset>
+ *     </uniform>
+ *     <object>
+ *       <tileset>
+ *         <!-- ... -->
+ *       </tileset>
+ *     </object>
+ *   </tilesets>
+ * 
+ * + * The class specified in the ruleset element must derive + * from {@link TileSetRuleSet}. The images that will be included in the + * bundle must be in the same directory as the bundle description file and + * the tileset descriptions must reference the images without a preceding + * path. + */ +public class TileSetBundler +{ + /** + * Constructs a tileset bundler with the specified path to a bundler + * configuration file. The configuration file will be loaded and used + * to configure this tileset bundler. + */ + public TileSetBundler (String configPath) + throws IOException + { + this(new File(configPath)); + } + + /** + * Constructs a tileset bundler with the specified bundler config + * file. + */ + public TileSetBundler (File configFile) + throws IOException + { + // we parse our configuration with a digester + Digester digester = new Digester(); + + // push our mappings array onto the stack + ArrayList mappings = new ArrayList(); + digester.push(mappings); + + // create a mapping object for each mapping entry and append it to + // our mapping list + digester.addObjectCreate("bundler-config/mapping", + Mapping.class.getName()); + digester.addSetNext("bundler-config/mapping", + "add", "java.lang.Object"); + + // configure each mapping object with the path and ruleset + digester.addCallMethod("bundler-config/mapping", "init", 2); + digester.addCallParam("bundler-config/mapping/path", 0); + digester.addCallParam("bundler-config/mapping/ruleset", 1); + + // now go like the wind + FileInputStream fin = new FileInputStream(configFile); + try { + digester.parse(fin); + } catch (SAXException saxe) { + String errmsg = "Failure parsing bundler config file " + + "[file=" + configFile.getPath() + "]"; + throw (IOException) new IOException(errmsg).initCause(saxe); + } + fin.close(); + + // create our digester + _digester = new Digester(); + + // use the mappings we parsed to configure our actual digester + int msize = mappings.size(); + for (int i = 0; i < msize; i++) { + Mapping map = (Mapping)mappings.get(i); + try { + TileSetRuleSet ruleset = (TileSetRuleSet) + Class.forName(map.ruleset).newInstance(); + + // configure the ruleset + ruleset.setPrefix(map.path); + // add it to the digester + _digester.addRuleSet(ruleset); + // and add a rule to stick the parsed tilesets onto the + // end of an array list that we'll put on the stack + _digester.addSetNext(map.path + TileSetRuleSet.TILESET_PATH, + "add", "java.lang.Object"); + + } catch (Exception e) { + String errmsg = "Unable to create tileset rule set " + + "instance [mapping=" + map + "]."; + throw (IOException) new IOException(errmsg).initCause(e); + } + } + } + + /** + * Creates a tileset bundle at the location specified by the + * targetPath parameter, based on the description + * provided via the bundleDesc parameter. + * + * @param idBroker the tileset id broker that will be used to map + * tileset names to tileset ids. + * @param bundleDesc a file object pointing to the bundle description + * file. + * @param targetPath the path of the tileset bundle file that will be + * created. + * + * @exception IOException thrown if an error occurs reading, writing + * or processing anything. + */ + public void createBundle ( + TileSetIDBroker idBroker, File bundleDesc, String targetPath) + throws IOException + { + createBundle(idBroker, bundleDesc, new File(targetPath)); + } + + /** + * Creates a tileset bundle at the location specified by the + * targetPath parameter, based on the description + * provided via the bundleDesc parameter. + * + * @param idBroker the tileset id broker that will be used to map + * tileset names to tileset ids. + * @param bundleDesc a file object pointing to the bundle description + * file. + * @param target the tileset bundle file that will be created. + * + * @return true if the bundle was rebuilt, false if it was not because + * the bundle file was newer than all involved source files. + * + * @exception IOException thrown if an error occurs reading, writing + * or processing anything. + */ + public boolean createBundle ( + TileSetIDBroker idBroker, final File bundleDesc, File target) + throws IOException + { + // stick an array list on the top of the stack into which we will + // collect parsed tilesets + ArrayList sets = new ArrayList(); + _digester.push(sets); + + // parse the tilesets + FileInputStream fin = new FileInputStream(bundleDesc); + try { + _digester.parse(fin); + } catch (SAXException saxe) { + String errmsg = "Failure parsing bundle description file " + + "[path=" + bundleDesc.getPath() + "]"; + throw (IOException) new IOException(errmsg).initCause(saxe); + } finally { + fin.close(); + } + + // we want to make sure that at least one of the tileset image + // files or the bundle definition file is newer than the bundle + // file, otherwise consider the bundle up to date + long newest = bundleDesc.lastModified(); + + // create a tileset bundle to hold our tilesets + TileSetBundle bundle = new TileSetBundle(); + + // add all of the parsed tilesets to the tileset bundle + try { + for (int i = 0; i < sets.size(); i++) { + TileSet set = (TileSet)sets.get(i); + String name = set.getName(); + + // let's be robust + if (name == null) { + Log.warning("Tileset was parsed, but received no name " + + "[set=" + set + "]. Skipping."); + continue; + } + + // make sure this tileset's image file exists and check + // it's last modified date + File tsfile = new File(bundleDesc.getParent(), + set.getImagePath()); + if (!tsfile.exists()) { + System.err.println("Tile set missing image file " + + "[bundle=" + bundleDesc.getPath() + + ", name=" + set.getName() + + ", imgpath=" + tsfile.getPath() + "]."); + continue; + } + if (tsfile.lastModified() > newest) { + newest = tsfile.lastModified(); + } + + // assign a tilset id to the tileset and bundle it + try { + int tileSetId = idBroker.getTileSetID(name); + bundle.addTileSet(tileSetId, set); + } catch (PersistenceException pe) { + String errmsg = "Failure obtaining a tileset id for " + + "tileset [set=" + set + "]."; + throw (IOException) new IOException(errmsg).initCause(pe); + } + } + + // clear out our array list in preparation for another go + sets.clear(); + + } finally { + // before we go, we have to commit our brokered tileset ids + // back to the broker's persistent store + try { + idBroker.commit(); + } catch (PersistenceException pe) { + Log.warning("Failure committing brokered tileset ids " + + "back to broker's persistent store " + + "[error=" + pe + "]."); + } + } + + // see if our newest file is newer than the tileset bundle + if (newest < target.lastModified()) { + return false; + } + + // create an image provider for loading our tileset images + SimpleCachingImageProvider improv = new SimpleCachingImageProvider() { + protected BufferedImage loadImage (String path) + throws IOException { + return ImageIO.read(new File(bundleDesc.getParent(), path)); + } + }; + + return createBundle(target, bundle, improv, bundleDesc.getParent()); + } + + /** + * Finish the creation of a tileset bundle jar file. + * + * @param target the tileset bundle file that will be created. + * @param bundle contains the tilesets we'd like to save out to the + * bundle. + * @param improv the image provider. + * @param imageBase the base directory for getting images for non + * ObjectTileSet tilesets. + */ + public static boolean createBundle ( + File target, TileSetBundle bundle, ImageProvider improv, + String imageBase) + throws IOException + { + // now we have to create the actual bundle file + FileOutputStream fout = new FileOutputStream(target); + Manifest manifest = new Manifest(); + JarOutputStream jar = new JarOutputStream(fout, manifest); + jar.setLevel(Deflater.BEST_COMPRESSION); + + try { + // write all of the image files to the bundle, converting the + // tilesets to trimmed tilesets in the process + Iterator iditer = bundle.enumerateTileSetIds(); + while (iditer.hasNext()) { + int tileSetId = ((Integer)iditer.next()).intValue(); + TileSet set = bundle.getTileSet(tileSetId); + String imagePath = set.getImagePath(); + + // sanity checks + if (imagePath == null) { + Log.warning("Tileset contains no image path " + + "[set=" + set + "]. It ain't gonna work."); + continue; + } + + // if this is an object tileset, we can't trim it! + if (set instanceof ObjectTileSet) { + // set the tileset up with an image provider; we + // need to do this so that we can trim it! + set.setImageProvider(improv); + + // we're going to trim it, so adjust the path + imagePath = adjustImagePath(imagePath); + jar.putNextEntry(new JarEntry(imagePath)); + + try { + // create a trimmed object tileset, which will + // write the trimmed tileset image to the jar + // output stream + TrimmedObjectTileSet tset = + TrimmedObjectTileSet.trimObjectTileSet( + (ObjectTileSet)set, jar); + tset.setImagePath(imagePath); + // replace the original set with the trimmed + // tileset in the tileset bundle + bundle.addTileSet(tileSetId, tset); + + } catch (Exception e) { + System.err.println("Error adding tileset to bundle " + + "[set=" + set.getName() + + ", ipath=" + imagePath + "]."); + e.printStackTrace(System.err); + // replace the tileset with an error tileset + UniformTileSet ets = new UniformTileSet(); + ets.setName(set.getName()); + ets.setWidth(50); + ets.setHeight(50); + ets.setImagePath(imagePath); + bundle.addTileSet(tileSetId, ets); + // and write an error image to the jar file + ImageIO.write(ImageUtil.createErrorImage(50, 50), + "PNG", jar); + } + + } else { + // read the image file and convert it to our custom + // format in the bundle + File ifile = new File(imageBase, imagePath); + try { + BufferedImage image = ImageIO.read(ifile); + if (FastImageIO.canWrite(image)) { + imagePath = adjustImagePath(imagePath); + jar.putNextEntry(new JarEntry(imagePath)); + set.setImagePath(imagePath); + FastImageIO.write(image, jar); + } else { + jar.putNextEntry(new JarEntry(imagePath)); + FileInputStream imgin = new FileInputStream(ifile); + CopyUtils.copy(imgin, jar); + } + } catch (Exception e) { + String msg = "Failure bundling image " + ifile + + ": " + e; + throw (IOException) new IOException(msg).initCause(e); + } + } + } + + // now write a serialized representation of the tileset bundle + // object to the bundle jar file + JarEntry entry = new JarEntry(BundleUtil.METADATA_PATH); + jar.putNextEntry(entry); + ObjectOutputStream oout = new ObjectOutputStream(jar); + oout.writeObject(bundle); + oout.flush(); + + // finally close up the jar file and call ourself done + jar.close(); + + return true; + + } catch (Exception e) { + // remove the incomplete jar file and rethrow the exception + jar.close(); + if (!target.delete()) { + Log.warning("Failed to close botched bundle '" + target + "'."); + } + String errmsg = "Failed to create bundle " + target + ": " + e; + throw (IOException) new IOException(errmsg).initCause(e); + } + } + + /** Replaces the image suffix with .raw. */ + protected static String adjustImagePath (String imagePath) + { + int didx = imagePath.lastIndexOf("."); + return ((didx == -1) ? imagePath : + imagePath.substring(0, didx)) + ".raw"; + } + + /** Used to parse our configuration. */ + public static class Mapping + { + public String path; + public String ruleset; + + public void init (String path, String ruleset) + { + this.path = path; + this.ruleset = ruleset; + } + + public String toString () + { + return "[path=" + path + ", ruleset=" + ruleset + "]"; + } + } + + /** The digester we use to parse bundle descriptions. */ + protected Digester _digester; +} diff --git a/src/java/com/threerings/media/tile/bundle/tools/TileSetBundlerTask.java b/src/java/com/threerings/media/tile/bundle/tools/TileSetBundlerTask.java new file mode 100644 index 00000000..bcfefe5f --- /dev/null +++ b/src/java/com/threerings/media/tile/bundle/tools/TileSetBundlerTask.java @@ -0,0 +1,143 @@ +// +// $Id: TileSetBundlerTask.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.bundle.tools; + +import java.io.File; +import java.util.ArrayList; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.FileSet; + +import com.threerings.media.tile.tools.MapFileTileSetIDBroker; + +/** + * Ant task for creating tilset bundles. + */ +public class TileSetBundlerTask extends Task +{ + /** + * Sets the path to the bundler configuration file that we'll use when + * creating the bundle. + */ + public void setConfig (File config) + { + _config = config; + } + + /** + * Sets the path to the tileset id mapping file we'll use when + * creating the bundle. + */ + public void setMapfile (File mapfile) + { + _mapfile = mapfile; + } + + /** + * Adds a nested <fileset> element. + */ + public void addFileset (FileSet set) + { + _filesets.add(set); + } + + /** + * Performs the actual work of the task. + */ + public void execute () throws BuildException + { + // make sure everything was set up properly + ensureSet(_config, "Must specify the path to the bundler config " + + "file via the 'config' attribute."); + ensureSet(_mapfile, "Must specify the path to the tileset id map " + + "file via the 'mapfile' attribute."); + + File cfile = null; + try { + // create a tileset bundler + TileSetBundler bundler = new TileSetBundler(_config); + + // create our tileset id broker + MapFileTileSetIDBroker broker = + new MapFileTileSetIDBroker(_mapfile); + + // deal with the filesets + for (int i = 0; i < _filesets.size(); i++) { + FileSet fs = (FileSet)_filesets.get(i); + DirectoryScanner ds = fs.getDirectoryScanner(getProject()); + File fromDir = fs.getDir(getProject()); + String[] srcFiles = ds.getIncludedFiles(); + + for (int f = 0; f < srcFiles.length; f++) { + cfile = new File(fromDir, srcFiles[f]); + + // figure out the bundle file based on the definition + // file + String cpath = cfile.getPath(); + if (!cpath.endsWith(".xml")) { + System.err.println("Can't infer bundle name from " + + "bundle config name " + + "[path=" + cpath + "].\n" + + "Config file should end with .xml."); + continue; + } + String bpath = + cpath.substring(0, cpath.length()-4) + ".jar"; + File bfile = new File(bpath); + + // create the bundle + if (bundler.createBundle(broker, cfile, bfile)) { + System.out.println( + "Created bundle from '" + cpath + "'..."); + } else { + System.out.println( + "Tileset bundle up to date '" + bpath + "'."); + } + } + } + + // commit changes to the tileset id mapping + broker.commit(); + + } catch (Exception e) { + String errmsg = "Failure creating tileset bundle [source=" + cfile + + "]: " + e.getMessage(); + throw new BuildException(errmsg, e); + } + } + + protected void ensureSet (Object value, String errmsg) + throws BuildException + { + if (value == null) { + throw new BuildException(errmsg); + } + } + + protected File _config; + protected File _mapfile; + + /** A list of filesets that contain tileset bundle definitions. */ + protected ArrayList _filesets = new ArrayList(); +} diff --git a/src/java/com/threerings/media/tile/tools/DumpTileSetMap.java b/src/java/com/threerings/media/tile/tools/DumpTileSetMap.java new file mode 100644 index 00000000..4f627cc7 --- /dev/null +++ b/src/java/com/threerings/media/tile/tools/DumpTileSetMap.java @@ -0,0 +1,56 @@ +// +// $Id: DumpTileSetMap.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.tools; + +import java.io.File; +import java.util.Iterator; + +import com.samskivert.io.PersistenceException; + +/** + * Prints out the tileset mappings in a {@link MapFileTileSetIDBroker}. + */ +public class DumpTileSetMap +{ + public static void main (String[] args) + { + if (args.length < 1) { + System.err.println("Usage: DumpTileSetMap tileset.map"); + System.exit(-1); + } + + try { + MapFileTileSetIDBroker broker = + new MapFileTileSetIDBroker(new File(args[0])); + Iterator iter = broker.enumerateMappings(); + while (iter.hasNext()) { + String tsname = iter.next().toString(); + System.out.println(tsname + " => " + + broker.getTileSetID(tsname)); + } + + } catch (PersistenceException pe) { + System.err.println("Unable to dump mapping: " + pe); + System.exit(-1); + } + } +} diff --git a/src/java/com/threerings/media/tile/tools/MapFileTileSetIDBroker.java b/src/java/com/threerings/media/tile/tools/MapFileTileSetIDBroker.java new file mode 100644 index 00000000..649ef80d --- /dev/null +++ b/src/java/com/threerings/media/tile/tools/MapFileTileSetIDBroker.java @@ -0,0 +1,224 @@ +// +// $Id: MapFileTileSetIDBroker.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.tools; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +import java.util.HashMap; +import java.util.Iterator; + +import com.samskivert.io.PersistenceException; +import com.samskivert.util.QuickSort; + +import com.threerings.media.tile.TileSetIDBroker; + +/** + * Stores a set of tileset name to id mappings in a map file. + */ +public class MapFileTileSetIDBroker implements TileSetIDBroker +{ + /** + * Creates a broker that will use the specified file as its persistent + * store. The persistent store will be created if it does not yet + * exist. + */ + public MapFileTileSetIDBroker (File mapfile) + throws PersistenceException + { + // keep this for later + _mapfile = mapfile; + + // load up our map data + try { + BufferedReader bin = new BufferedReader(new FileReader(mapfile)); + // read in our metadata + _nextTileSetID = readInt(bin); + _storedTileSetID = _nextTileSetID; + // read in our mappings + _map = new HashMap(); + readMapFile(bin, _map); + + bin.close(); + + } catch (FileNotFoundException fnfe) { + // create a blank map if our map file doesn't exist + _map = new HashMap(); + + } catch (Exception e) { + // other errors are more fatal + String errmsg = "Failure reading map file."; + throw new PersistenceException(errmsg, e); + } + } + + protected int readInt (BufferedReader bin) + throws IOException + { + String line = bin.readLine(); + try { + return Integer.parseInt(line); + } catch (NumberFormatException nfe) { + throw new IOException("Expected number, got '" + line + "'"); + } + } + + // documentation inherited + public int getTileSetID (String tileSetName) + throws PersistenceException + { + Integer tsid = (Integer)_map.get(tileSetName); + if (tsid == null) { + tsid = Integer.valueOf(++_nextTileSetID); + _map.put(tileSetName, tsid); + } + return tsid.intValue(); + } + + // documentation inherited from interface + public boolean tileSetMapped (String tileSetName) + throws PersistenceException + { + return _map.containsKey(tileSetName); + } + + // documentation inherited + public void commit () + throws PersistenceException + { + // only write ourselves out if we've changed + if (_storedTileSetID == _nextTileSetID) { + return; + } + + try { + BufferedWriter bout = new BufferedWriter(new FileWriter(_mapfile)); + // write out our metadata + String tline = "" + _nextTileSetID; + bout.write(tline, 0, tline.length()); + bout.newLine(); + // write out our mappings + writeMapFile(bout, _map); + bout.close(); + + } catch (IOException ioe) { + String errmsg = "Failure writing map file."; + throw new PersistenceException(errmsg, ioe); + } + } + + /** + * Reads in a mapping from strings to integers, which should have been + * written via {@link #writeMapFile}. + */ + public static void readMapFile (BufferedReader bin, HashMap map) + throws IOException + { + String line; + while ((line = bin.readLine()) != null) { + int eidx = line.indexOf(SEP_STR); + if (eidx == -1) { + throw new IOException("Malformed line, no '" + SEP_STR + + "': '" + line + "'"); + } + try { + String code = line.substring(eidx+SEP_STR.length()); + map.put(line.substring(0, eidx), Integer.valueOf(code)); + } catch (NumberFormatException nfe) { + String errmsg = "Malformed line, invalid code: '" + line + "'"; + throw new IOException(errmsg); + } + } + } + + /** + * Writes out a mapping from strings to integers in a manner that can + * be read back in via {@link #readMapFile}. + */ + public static void writeMapFile (BufferedWriter bout, HashMap map) + throws IOException + { + String[] lines = new String[map.size()]; + Iterator iter = map.keySet().iterator(); + for (int ii = 0; iter.hasNext(); ii++) { + String key = (String)iter.next(); + Integer value = (Integer)map.get(key); + lines[ii] = key + SEP_STR + value; + } + QuickSort.sort(lines); + for (int ii = 0; ii < lines.length; ii++) { + bout.write(lines[ii], 0, lines[ii].length()); + bout.newLine(); + } + bout.flush(); + } + + /** + * Copies the ID from the old tileset to the new tileset which is + * useful when a tileset is renamed. This is called by the {@link + * RenameTileSet} utility. + */ + protected boolean renameTileSet (String oldName, String newName) + { + Integer tsid = (Integer)_map.get(oldName); + if (tsid != null) { + _map.put(newName, tsid); + // fudge our stored tileset ID so that we flush ourselves when + // the rename tool requests that we commit the changes + _storedTileSetID--; + return true; + + } else { + return false; + } + } + + /** + * Used by {@link DumpTileSetMap} to enumerate our tileset ID + * mappings. + */ + protected Iterator enumerateMappings () + { + return _map.keySet().iterator(); + } + + /** Our persistent map file. */ + protected File _mapfile; + + /** The next tileset id that we'll assign. */ + protected int _nextTileSetID; + + /** The last tileset id assigned when we were unserialized. */ + protected int _storedTileSetID; + + /** Our mapping from tileset names to ids. */ + protected HashMap _map; + + /** The character we use to separate tileset name from code in the map + * file. */ + protected static final String SEP_STR = " := "; +} diff --git a/src/java/com/threerings/media/tile/tools/RenameTileSet.java b/src/java/com/threerings/media/tile/tools/RenameTileSet.java new file mode 100644 index 00000000..ab846e66 --- /dev/null +++ b/src/java/com/threerings/media/tile/tools/RenameTileSet.java @@ -0,0 +1,74 @@ +// +// $Id: RenameTileSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.tools; + +import java.io.File; + +import com.samskivert.io.PersistenceException; + +/** + * Used to map a tileset name to the same ID as a pre-existing tileset. + * This only works for tileset mappings stored using a {@link + * MapFileTileSetIDBroker}. If a tileset is renamed, this utility must be + * used to map the new name to the old ID, otherwise scenes created with + * the renamed tileset will cease to work as the tileset will live under a + * new ID. + */ +public class RenameTileSet +{ + /** + * Loads up the tileset map file with the specified path and copies + * the tileset ID from the old tileset name to the new tileset name. + * This is necessary when a tileset is renamed so that the new name + * does not cause the tileset to be assigned a new tileset ID. Bear in + * mind that the old name should never again be used as it will + * conflict with a tileset provided under the new name. + */ + public static void renameTileSet ( + String mapPath, String oldName, String newName) + throws PersistenceException + { + MapFileTileSetIDBroker broker = + new MapFileTileSetIDBroker(new File(mapPath)); + if (!broker.renameTileSet(oldName, newName)) { + throw new PersistenceException( + "No such old tileset '" + oldName + "'."); + } + broker.commit(); + } + + public static void main (String[] args) + { + if (args.length < 3) { + System.err.println("Usage: RenameTileSet tileset.map " + + "old_name new_name"); + System.exit(-1); + } + + try { + renameTileSet(args[0], args[1], args[2]); + } catch (PersistenceException pe) { + System.err.println("Unable to rename tileset: " + pe); + System.exit(-1); + } + } +} diff --git a/src/java/com/threerings/media/tile/tools/xml/ObjectTileSetRuleSet.java b/src/java/com/threerings/media/tile/tools/xml/ObjectTileSetRuleSet.java new file mode 100644 index 00000000..c2dacd05 --- /dev/null +++ b/src/java/com/threerings/media/tile/tools/xml/ObjectTileSetRuleSet.java @@ -0,0 +1,188 @@ +// +// $Id: ObjectTileSetRuleSet.java 3607 2005-06-20 21:18:55Z andrzej $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.tools.xml; + +import org.apache.commons.digester.Digester; + +import com.samskivert.util.StringUtil; +import com.samskivert.xml.CallMethodSpecialRule; + +import com.threerings.media.tile.ObjectTileSet; + +import com.threerings.util.DirectionUtil; + +/** + * Parses {@link ObjectTileSet} instances from a tileset description. An + * object tileset description looks like so: + * + *
+ * <tileset name="Sample Object Tileset">
+ *   <imagePath>path/to/image.png</imagePath>
+ *   <!-- the widths (per row) of each tile in pixels -->
+ *   <widths>265</widths>
+ *   <!-- the heights (per row) of each tile in pixels -->
+ *   <heights>224</heights>
+ *   <!-- the number of tiles in each row -->
+ *   <tileCounts>4</tileCounts>
+ *   <!-- the offset in pixels to the upper left tile -->
+ *   <offsetPos>0, 0</offsetPos>
+ *   <!-- the gap between tiles in pixels -->
+ *   <gapSize>0, 0</gapSize>
+ *   <!-- the widths (in unit tile count) of the objects -->
+ *   <objectWidths>4, 3, 4, 3</objectWidths>
+ *   <!-- the heights (in unit tile count) of the objects -->
+ *   <objectHeights>3, 4, 3, 4</objectHeights>
+ *   <!-- the default render priorities for these object tiles -->
+ *   <priorities>0, 0, -1, 0</priorities>
+ *   <!-- the constraints for these object tiles -->
+ *   <constraints>ATTACH_N, ATTACH_E, ATTACH_S, ATTACH_W</constraints>
+ * </tileset>
+ * 
+ */ +public class ObjectTileSetRuleSet extends SwissArmyTileSetRuleSet +{ + // documentation inherited + public void addRuleInstances (Digester digester) + { + super.addRuleInstances(digester); + + digester.addRule( + _prefix + TILESET_PATH + "/objectWidths", + new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + int[] widths = StringUtil.parseIntArray(bodyText); + ((ObjectTileSet)target).setObjectWidths(widths); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/objectHeights", + new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + int[] heights = StringUtil.parseIntArray(bodyText); + ((ObjectTileSet)target).setObjectHeights(heights); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/xOrigins", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + int[] xorigins = StringUtil.parseIntArray(bodyText); + ((ObjectTileSet)target).setXOrigins(xorigins); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/yOrigins", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + int[] yorigins = StringUtil.parseIntArray(bodyText); + ((ObjectTileSet)target).setYOrigins(yorigins); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/priorities", + new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + byte[] prios = StringUtil.parseByteArray(bodyText); + ((ObjectTileSet)target).setPriorities(prios); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/zations", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + String[] zations = StringUtil.parseStringArray(bodyText); + ((ObjectTileSet)target).setColorizations(zations); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/xspots", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + short[] xspots = StringUtil.parseShortArray(bodyText); + ((ObjectTileSet)target).setXSpots(xspots); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/yspots", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + short[] yspots = StringUtil.parseShortArray(bodyText); + ((ObjectTileSet)target).setYSpots(yspots); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/sorients", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + ObjectTileSet set = (ObjectTileSet)target; + String[] ostrs = StringUtil.parseStringArray(bodyText); + byte[] sorients = new byte[ostrs.length]; + for (int ii = 0; ii < sorients.length; ii++) { + sorients[ii] = (byte) + DirectionUtil.fromShortString(ostrs[ii]); + if ((sorients[ii] == DirectionUtil.NONE) && + // don't complain if they didn't even try to + // specify a valid direction + (! ostrs[ii].equals("-1"))) { + System.err.println("Invalid spot orientation " + + "[set=" + set.getName() + + ", idx=" + ii + + ", orient=" + ostrs[ii] + "]."); + } + } + set.setSpotOrients(sorients); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/constraints", + new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + String[] constrs = StringUtil.parseStringArray( + bodyText); + String[][] constraints = new String[constrs.length][]; + for (int ii = 0; ii < constrs.length; ii++) { + constraints[ii] = constrs[ii].split("\\s*\\|\\s*"); + } + ((ObjectTileSet)target).setConstraints(constraints); + } + }); + } + + // documentation inherited + protected Class getTileSetClass () + { + return ObjectTileSet.class; + } +} diff --git a/src/java/com/threerings/media/tile/tools/xml/SwissArmyTileSetRuleSet.java b/src/java/com/threerings/media/tile/tools/xml/SwissArmyTileSetRuleSet.java new file mode 100644 index 00000000..83835ce6 --- /dev/null +++ b/src/java/com/threerings/media/tile/tools/xml/SwissArmyTileSetRuleSet.java @@ -0,0 +1,156 @@ +// +// $Id: SwissArmyTileSetRuleSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.tools.xml; + +import java.awt.Dimension; +import java.awt.Point; + +import org.apache.commons.digester.Digester; + +import com.samskivert.util.StringUtil; +import com.samskivert.xml.CallMethodSpecialRule; + +import com.threerings.media.Log; +import com.threerings.media.tile.SwissArmyTileSet; + +/** + * Parses {@link SwissArmyTileSet} instances from a tileset description. A + * swiss army tileset description looks like so: + * + *
+ * <tileset name="Sample Swiss Army Tileset">
+ *   <imagePath>path/to/image.png</imagePath>
+ *   <!-- the widths (per row) of each tile in pixels -->
+ *   <widths>64, 64, 64, 64</widths>
+ *   <!-- the heights (per row) of each tile in pixels -->
+ *   <heights>48, 48, 48, 64</heights>
+ *   <!-- the number of tiles in each row -->
+ *   <tileCounts>16, 5, 3, 10</tileCounts>
+ *   <!-- the offset in pixels to the upper left tile -->
+ *   <offsetPos>8, 8</offsetPos>
+ *   <!-- the gap between tiles in pixels -->
+ *   <gapSize>12, 12</gapSize>
+ * </tileset>
+ * 
+ */ +public class SwissArmyTileSetRuleSet extends TileSetRuleSet +{ + // documentation inherited + public void addRuleInstances (Digester digester) + { + super.addRuleInstances(digester); + + digester.addRule( + _prefix + TILESET_PATH + "/widths", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + int[] widths = StringUtil.parseIntArray(bodyText); + ((SwissArmyTileSet)target).setWidths(widths); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/heights", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + int[] heights = StringUtil.parseIntArray(bodyText); + ((SwissArmyTileSet)target).setHeights(heights); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/tileCounts", + new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + int[] tileCounts = StringUtil.parseIntArray(bodyText); + ((SwissArmyTileSet)target).setTileCounts(tileCounts); + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/offsetPos", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + int[] values = StringUtil.parseIntArray(bodyText); + SwissArmyTileSet starget = (SwissArmyTileSet)target; + if (values.length == 2) { + starget.setOffsetPos(new Point(values[0], values[1])); + } else { + Log.warning("Invalid 'offsetPos' definition '" + + bodyText + "'."); + } + } + }); + + digester.addRule( + _prefix + TILESET_PATH + "/gapSize", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + int[] values = StringUtil.parseIntArray(bodyText); + SwissArmyTileSet starget = (SwissArmyTileSet)target; + if (values.length == 2) { + starget.setGapSize(new Dimension(values[0], values[1])); + } else { + Log.warning("Invalid 'gapSize' definition '" + + bodyText + "'."); + } + } + }); + } + + // documentation inherited + public boolean isValid (Object target) + { + SwissArmyTileSet set = (SwissArmyTileSet)target; + boolean valid = super.isValid(target); + + // check for a element + if (set.getWidths() == null) { + Log.warning("Tile set definition missing valid " + + "element [set=" + set + "]."); + valid = false; + } + + // check for a element + if (set.getHeights() == null) { + Log.warning("Tile set definition missing valid " + + "element [set=" + set + "]."); + valid = false; + } + + // check for a element + if (set.getTileCounts() == null) { + Log.warning("Tile set definition missing valid " + + "element [set=" + set + "]."); + valid = false; + } + + return valid; + } + + // documentation inherited + protected Class getTileSetClass () + { + return SwissArmyTileSet.class; + } +} diff --git a/src/java/com/threerings/media/tile/tools/xml/TileSetRuleSet.java b/src/java/com/threerings/media/tile/tools/xml/TileSetRuleSet.java new file mode 100644 index 00000000..a4756872 --- /dev/null +++ b/src/java/com/threerings/media/tile/tools/xml/TileSetRuleSet.java @@ -0,0 +1,128 @@ +// +// $Id: TileSetRuleSet.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.tools.xml; + +import org.apache.commons.digester.Digester; +import org.apache.commons.digester.RuleSetBase; + +import com.samskivert.util.StringUtil; +import com.samskivert.xml.ValidatedSetNextRule.Validator; +import com.samskivert.xml.ValidatedSetNextRule; + +import com.threerings.media.Log; +import com.threerings.media.tile.TileSet; + +/** + * The tileset rule set is used to parse the base attributes of a tileset + * instance. Derived classes would extend this and add rules for their own + * special tilesets. + */ +public abstract class TileSetRuleSet + extends RuleSetBase implements Validator +{ + /** The component of the digester path that is appended by the tileset + * rule set to match a tileset. This is appended to whatever prefix is + * provided to the tileset rule set to obtain the complete XML path to + * a matched tileset. */ + public static final String TILESET_PATH = "/tileset"; + + /** + * Instructs the tileset rule set to match tilesets with the supplied + * prefix. For example, passing a prefix of + * tilesets.objectsets will match tilesets in the + * following XML file: + * + *
+     * <tilesets>
+     *   <objectsets>
+     *     <tileset>
+     *       // ...
+     *     </tileset>
+     *   </objectsets>
+     * </tilesets>
+     * 
+ * + * This must be called before adding the ruleset to a digester. + */ + public void setPrefix (String prefix) + { + _prefix = prefix; + } + + /** + * Adds the necessary rules to the digester to parse our tilesets. + * Derived classes should override this method, being sure to call the + * superclass method and then adding their own rule instances (which + * should register themselves relative to the _prefix + * member). + */ + public void addRuleInstances (Digester digester) + { + // this creates the appropriate instance when we encounter a + // tag + digester.addObjectCreate(_prefix + TILESET_PATH, + getTileSetClass().getName()); + + // grab the name attribute from the tag + digester.addSetProperties(_prefix + TILESET_PATH); + + // grab the image path from an element + digester.addCallMethod( + _prefix + TILESET_PATH + "/imagePath", "setImagePath", 0); + } + + /** + * The ruleset can be provided to a {@link ValidatedSetNextRule} to + * ensure that the tileset was fully parsed before doing something + * with it. + */ + public boolean isValid (Object target) + { + TileSet set = (TileSet)target; + boolean valid = true; + + // check for the 'name' attribute + if (StringUtil.isBlank(set.getName())) { + Log.warning("Tile set definition missing 'name' attribute " + + "[set=" + set + "]."); + valid = false; + } + + // check for an element + if (StringUtil.isBlank(set.getImagePath())) { + Log.warning("Tile set definition missing element " + + "[set=" + set + "]."); + valid = false; + } + + return valid; + } + + /** + * A tileset rule set will create tilesets of a particular class, + * which must be provided by the derived class via this method. + */ + protected abstract Class getTileSetClass (); + + /** The prefix at which me match our tilesets. */ + protected String _prefix; +} diff --git a/src/java/com/threerings/media/tile/tools/xml/UniformTileSetRuleSet.java b/src/java/com/threerings/media/tile/tools/xml/UniformTileSetRuleSet.java new file mode 100644 index 00000000..3bd6725f --- /dev/null +++ b/src/java/com/threerings/media/tile/tools/xml/UniformTileSetRuleSet.java @@ -0,0 +1,86 @@ +// +// $Id: UniformTileSetRuleSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.tools.xml; + +import org.apache.commons.digester.Digester; + +import com.threerings.media.Log; +import com.threerings.media.tile.UniformTileSet; + +/** + * Parses {@link UniformTileSet} instances from a tileset description. A + * uniform tileset description looks like so: + * + *
+ * <tileset name="Sample Uniform Tileset">
+ *   <imagePath>path/to/image.png</imagePath>
+ *   <!-- the width of each tile in pixels -->
+ *   <width>64</width>
+ *   <!-- the height of each tile in pixels -->
+ *   <height>48</height>
+ * </tileset>
+ * 
+ */ +public class UniformTileSetRuleSet extends TileSetRuleSet +{ + // documentation inherited + public void addRuleInstances (Digester digester) + { + super.addRuleInstances(digester); + + digester.addCallMethod( + _prefix + TILESET_PATH + "/width", "setWidth", 0, + new Class[] { java.lang.Integer.TYPE }); + digester.addCallMethod( + _prefix + TILESET_PATH + "/height", "setHeight", 0, + new Class[] { java.lang.Integer.TYPE }); + } + + // documentation inherited + public boolean isValid (Object target) + { + UniformTileSet set = (UniformTileSet)target; + boolean valid = super.isValid(target); + + // check for a element + if (set.getWidth() == 0) { + Log.warning("Tile set definition missing valid " + + "element [set=" + set + "]."); + valid = false; + } + + // check for a element + if (set.getHeight() == 0) { + Log.warning("Tile set definition missing valid " + + "element [set=" + set + "]."); + valid = false; + } + + return valid; + } + + // documentation inherited + protected Class getTileSetClass () + { + return UniformTileSet.class; + } +} diff --git a/src/java/com/threerings/media/tile/tools/xml/XMLTileSetParser.java b/src/java/com/threerings/media/tile/tools/xml/XMLTileSetParser.java new file mode 100644 index 00000000..a7820820 --- /dev/null +++ b/src/java/com/threerings/media/tile/tools/xml/XMLTileSetParser.java @@ -0,0 +1,172 @@ +// +// $Id: XMLTileSetParser.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.tools.xml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; + +import org.apache.commons.digester.Digester; +import org.xml.sax.SAXException; + +import com.samskivert.util.ConfigUtil; +import com.samskivert.xml.ValidatedSetNextRule; + +import com.threerings.media.Log; +import com.threerings.media.tile.TileSet; + +/** + * Parse an XML tileset description file and construct tileset objects for + * each valid description. Does not currently perform validation on the + * input XML stream, though the parsing code assumes the XML document is + * well-formed. + */ +public class XMLTileSetParser +{ + /** + * Constructs an xml tile set parser. + */ + public XMLTileSetParser () + { + // create our digester + _digester = new Digester(); + } + + /** + * Adds a ruleset to be used when parsing tiles. This should be an + * instance of a class derived from {@link TileSetRuleSet}. The prefix + * will be used to configure the ruleset so that it matches elements + * at a particular point in the XML hierarchy. For example: + * + *
+     * _parser.addRuleSet("tilesets", new UniformTileSetRuleSet());
+     * 
+ */ + public void addRuleSet (String prefix, TileSetRuleSet ruleset) + { + // configure the ruleset with the appropriate prefix + ruleset.setPrefix(prefix); + + // and have it set itself up with the digester + _digester.addRuleSet(ruleset); + + // add a set next rule which will put tilesets with this prefix + // into the array list that'll be on the top of the stack + _digester.addRule(prefix + TileSetRuleSet.TILESET_PATH, + new ValidatedSetNextRule("add", Object.class, + ruleset)); + } + + /** + * Loads all of the tilesets specified in the supplied XML tileset + * description file and places them into the supplied hashmap indexed + * by tileset name. This method is not reentrant, so don't go calling + * it from multiple threads. + * + * @param path a path, relative to the classpath, at which the tileset + * definition file can be found. + * @param tilesets the hashmap into which the tilesets will be placed, + * indexed by tileset name. + */ + public void loadTileSets (String path, HashMap tilesets) + throws IOException + { + // get an input stream for this XML file + InputStream is = ConfigUtil.getStream(path); + if (is == null) { + String errmsg = "Can't load tileset description file from " + + "classpath [path=" + path + "]."; + throw new FileNotFoundException(errmsg); + } + + // load up the tilesets + loadTileSets(is, tilesets); + } + + /** + * Loads all of the tilesets specified in the supplied XML tileset + * description file and places them into the supplied hashmap indexed + * by tileset name. This method is not reentrant, so don't go calling + * it from multiple threads. + * + * @param file the file in which the tileset definition file can be + * found. + * @param tilesets the hashmap into which the tilesets will be placed, + * indexed by tileset name. + */ + public void loadTileSets (File file, HashMap tilesets) + throws IOException + { + // load up the tilesets + loadTileSets(new FileInputStream(file), tilesets); + } + + /** + * Loads all of the tilesets specified in the supplied XML tileset + * description file and places them into the supplied hashmap indexed + * by tileset name. This method is not reentrant, so don't go calling + * it from multiple threads. + * + * @param source an input stream from which the tileset definition + * file can be read. + * @param tilesets the hashmap into which the tilesets will be placed, + * indexed by tileset name. + */ + public void loadTileSets (InputStream source, HashMap tilesets) + throws IOException + { + // stick an array list on the top of the stack for collecting + // parsed tilesets + ArrayList setlist = new ArrayList(); + _digester.push(setlist); + + // now fire up the digester to parse the stream + try { + _digester.parse(source); + } catch (SAXException saxe) { + Log.warning("Exception parsing tile set descriptions " + + "[error=" + saxe + "]."); + Log.logStackTrace(saxe); + } + + // stick the tilesets from the list into the hashtable + for (int i = 0; i < setlist.size(); i++) { + TileSet set = (TileSet)setlist.get(i); + if (set.getName() == null) { + Log.warning("Tileset did not receive name during " + + "parsing process [set=" + set + "]."); + } else { + tilesets.put(set.getName(), set); + } + } + + // and clear out the list for next time + setlist.clear(); + } + + /** Our XML digester. */ + protected Digester _digester; +} diff --git a/src/java/com/threerings/media/tile/util/TileSetTrimmer.java b/src/java/com/threerings/media/tile/util/TileSetTrimmer.java new file mode 100644 index 00000000..51a3c819 --- /dev/null +++ b/src/java/com/threerings/media/tile/util/TileSetTrimmer.java @@ -0,0 +1,146 @@ +// +// $Id: TileSetTrimmer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.util; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.image.RasterFormatException; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.io.OutputStream; + +import com.threerings.media.image.FastImageIO; +import com.threerings.media.image.ImageUtil; +import com.threerings.media.tile.TileSet; + +/** + * Contains routines for trimming the images from an existing tileset + * which means that each tile is converted to an image that contains the + * smallest rectangular region of the original image that contains all + * non-transparent pixels. These trimmed images are then written out to a + * single image, packed together left to right. + */ +public class TileSetTrimmer +{ + /** + * Used to communicate the metrics of the trimmed tiles back to the + * caller so that they can do what they like with them. + */ + public static interface TrimMetricsReceiver + { + /** + * Called for each trimmed tile. + * + * @param tileIndex the index of the tile in the original tileset. + * @param imageX the x offset into the newly created tileset image + * of the trimmed image data. + * @param imageY the y offset into the newly created tileset image + * of the trimmed image data. + * @param trimX the x offset into the untrimmed tile image where + * the trimmed data begins. + * @param trimY the y offset into the untrimmed tile image where + * the trimmed data begins. + * @param trimWidth the width of the trimmed tile image. + * @param trimHeight the height of the trimmed tile image. + */ + public void trimmedTile (int tileIndex, int imageX, int imageY, + int trimX, int trimY, + int trimWidth, int trimHeight); + } + + /** + * Generates a trimmed tileset image from the supplied source + * tileset. The source tileset must be configured with an image + * provider so that the tile images can be obtained. The tile images + * will be trimmed and a new tileset image generated and written to + * the destImage output stream argument. + * + * @param source the source tileset. + * @param destImage an output stream to which the new trimmed image + * will be written. + * @param tmr a callback object that will be used to inform the caller + * of the trimmed tile metrics. + */ + public static void trimTileSet ( + TileSet source, OutputStream destImage, TrimMetricsReceiver tmr) + throws IOException + { + int tcount = source.getTileCount(); + BufferedImage[] timgs = new BufferedImage[tcount]; + + // these will contain the bounds of the trimmed image in the + // coordinate system defined by the untrimmed image + Rectangle[] tbounds = new Rectangle[tcount]; + + // compute some tile metrics + int nextx = 0, maxy = 0; + for (int ii = 0; ii < tcount; ii++) { + // extract the image from the original tileset + try { + timgs[ii] = source.getRawTileImage(ii); + } catch (RasterFormatException rfe) { + throw new IOException("Failed to get tile image " + + "[tidx=" + ii + ", tset=" + source + + ", rfe=" + rfe + "]."); + } + + // figure out how tightly we can trim it + tbounds[ii] = new Rectangle(); + ImageUtil.computeTrimmedBounds(timgs[ii], tbounds[ii]); + + // let our caller know what we did + tmr.trimmedTile(ii, nextx, 0, tbounds[ii].x, tbounds[ii].y, + tbounds[ii].width, tbounds[ii].height); + + // adjust the new tileset image dimensions + maxy = Math.max(maxy, tbounds[ii].height); + nextx += tbounds[ii].width; + } + + // create the new tileset image + BufferedImage image = null; + try { + image = ImageUtil.createCompatibleImage( + (BufferedImage)source.getRawTileSetImage(), nextx, maxy); + + } catch (RasterFormatException rfe) { + throw new IOException("Failed to create trimmed tileset image " + + "[wid=" + nextx + ", hei=" + maxy + + ", tset=" + source + ", rfe=" + rfe + "]."); + } + + // copy the tile data + WritableRaster drast = image.getRaster(); + int xoff = 0; + for (int ii = 0; ii < tcount; ii++) { + Rectangle tb = tbounds[ii]; + Raster srast = timgs[ii].getRaster().createChild( + tb.x, tb.y, tb.width, tb.height, 0, 0, null); + drast.setRect(xoff, 0, srast); + xoff += tb.width; + } + + // write out trimmed image + FastImageIO.write(image, destImage); + } +} diff --git a/src/java/com/threerings/media/timer/MediaTimer.java b/src/java/com/threerings/media/timer/MediaTimer.java new file mode 100644 index 00000000..25925993 --- /dev/null +++ b/src/java/com/threerings/media/timer/MediaTimer.java @@ -0,0 +1,52 @@ +// +// $Id: MediaTimer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.timer; + +/** + * Provides a pluggable mechanism for delivering high resolution timing + * information. The timers are not intended to be used by different + * threads and thus must be protected by synchronization in such + * circumstances. + */ +public interface MediaTimer +{ + /** + * Resets the timer's monotonically increasing value. + */ + public void reset (); + + /** + * Returns the number of milliseconds that have elapsed since the + * timer was created or last {@link #reset}. Note: the + * accuracy of this method is highly dependent on the timer + * implementation used. + */ + public long getElapsedMillis (); + + /** + * Returns the number of microseconds that have elapsed since the + * timer was created or last {@link #reset}. Note: the + * accuracy of this method is highly dependent on the timer + * implementation used. + */ + public long getElapsedMicros (); +} diff --git a/src/java/com/threerings/media/timer/PerfTimer.java b/src/java/com/threerings/media/timer/PerfTimer.java new file mode 100644 index 00000000..1d5f21c1 --- /dev/null +++ b/src/java/com/threerings/media/timer/PerfTimer.java @@ -0,0 +1,69 @@ +// +// $Id: PerfTimer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.timer; + +import sun.misc.Perf; + +import com.threerings.media.Log; + +/** + * A timer that uses the performance clock exposed by Sun in JDK 1.4.2. + */ +public class PerfTimer implements MediaTimer +{ + public PerfTimer () + { + _timer = Perf.getPerf(); + _mfrequency = _timer.highResFrequency() / 1000; + _ufrequency = _timer.highResFrequency() / 1000000; + _startStamp = _timer.highResCounter(); + Log.info("Using high performance timer [mfreq=" + _mfrequency + + ", ufreq=" + _ufrequency + ", start=" + _startStamp + "]."); + } + + // documentation inherited from interface + public void reset () + { + _startStamp = _timer.highResCounter(); + } + + // documentation inherited from interface + public long getElapsedMillis () + { + return (_timer.highResCounter() - _startStamp) / _mfrequency; + } + + // documentation inherited from interface + public long getElapsedMicros () + { + return (_timer.highResCounter() - _startStamp) / _ufrequency; + } + + /** A performance timer object. */ + protected Perf _timer; + + /** The time at which this timer was last reset. */ + protected long _startStamp; + + /** The timer frequencies. */ + protected long _mfrequency, _ufrequency; +} diff --git a/src/java/com/threerings/media/timer/SystemMediaTimer.java b/src/java/com/threerings/media/timer/SystemMediaTimer.java new file mode 100644 index 00000000..3ebed746 --- /dev/null +++ b/src/java/com/threerings/media/timer/SystemMediaTimer.java @@ -0,0 +1,60 @@ +// +// $Id: SystemMediaTimer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.timer; + +/** + * Implements the {@link MediaTimer} interface using {@link + * System#currentTimeMillis} to obtain timing information. + * + *

Note: {@link System#currentTimeMillis} is notoriously + * inaccurate on different platforms. See + * bug report 4486109 for more information. + * + *

Also note: clock drift on Windows XP is especially + * pronounced and is exacerbated by the fact that WinXP periodically + * resyncs the system clock with the hardware clock, causing discontinuous + * jumps in the progression of time (usually backwards in time). + */ +public class SystemMediaTimer implements MediaTimer +{ + // documentation inherited from interface + public void reset () + { + _resetStamp = System.currentTimeMillis(); + } + + // documentation inherited from interface + public long getElapsedMillis () + { + return System.currentTimeMillis() - _resetStamp; + } + + // documentation inherited from interface + public long getElapsedMicros () + { + return getElapsedMillis() * 10; + } + + /** The time at which this timer was last reset. */ + protected long _resetStamp = System.currentTimeMillis(); +} diff --git a/src/java/com/threerings/media/tools/RecolorImage.java b/src/java/com/threerings/media/tools/RecolorImage.java new file mode 100644 index 00000000..ce0744e9 --- /dev/null +++ b/src/java/com/threerings/media/tools/RecolorImage.java @@ -0,0 +1,476 @@ +// +// $Id: RecolorImage.java 3887 2006-02-24 01:24:34Z mthomas $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tools; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Collections; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JTextField; +import javax.swing.JToggleButton; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.samskivert.swing.HGroupLayout; +import com.samskivert.swing.IntField; +import com.samskivert.swing.VGroupLayout; +import com.samskivert.swing.event.DocumentAdapter; +import com.samskivert.swing.util.SwingUtil; +import com.samskivert.util.Interator; + +import com.threerings.media.image.ColorPository; +import com.threerings.media.image.Colorization; +import com.threerings.media.image.ImageUtil; +import com.threerings.media.image.tools.xml.ColorPositoryParser; + +/** + * Tests the image recoloring code. + */ +public class RecolorImage extends JPanel + implements ActionListener +{ + public RecolorImage () + { + setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + VGroupLayout vlay = new VGroupLayout(VGroupLayout.STRETCH); + vlay.setOffAxisPolicy(VGroupLayout.STRETCH); + setLayout(vlay); + + JPanel images = new JPanel(new HGroupLayout()); + images.add(_oldImage = new JLabel()); + images.add(_newImage = new JLabel()); + add(new JScrollPane(images)); + + // Image file + JPanel file = new JPanel(new HGroupLayout(HGroupLayout.STRETCH)); + file.add(new JLabel("Image file:"), HGroupLayout.FIXED); + file.add(_imagePath = new JTextField()); + _imagePath.setEditable(false); + JButton browse = new JButton("Browse..."); + browse.setActionCommand("browse"); + browse.addActionListener(this); + file.add(browse, HGroupLayout.FIXED); + JButton reload = new JButton("Reload"); + reload.setActionCommand("reload"); + reload.addActionListener(this); + file.add(reload, HGroupLayout.FIXED); + add(file, VGroupLayout.FIXED); + + // Colorization file + JPanel colFile = new JPanel(new HGroupLayout(HGroupLayout.STRETCH)); + colFile.add(new JLabel("Colorize file:"), HGroupLayout.FIXED); + colFile.add(_colFilePath = new JTextField()); + _colFilePath.setEditable(false); + browse = new JButton("Browse..."); + browse.setActionCommand("browse_colorize"); + browse.addActionListener(this); + colFile.add(browse, HGroupLayout.FIXED); + add(colFile, VGroupLayout.FIXED); + + JPanel colMode = new JPanel(new HGroupLayout(HGroupLayout.STRETCH)); + colMode.add(_mode = new JToggleButton("All Colorizations")); + colMode.add(_classList = new JComboBox()); + ActionListener al = new ActionListener () { + public void actionPerformed (ActionEvent ae) { + convert(); + } + }; + _mode.addActionListener(al); + _classList.addActionListener(al); + + add(colMode, VGroupLayout.FIXED); + + JPanel controls = new JPanel(new HGroupLayout(HGroupLayout.STRETCH)); + controls.add(new JLabel("Source color:"), HGroupLayout.FIXED); + controls.add(_source = new JTextField("FF0000")); + _colorLabel = new JPanel(); + _colorLabel.setSize(48, 48); + _colorLabel.setOpaque(true); + controls.add(_colorLabel, HGroupLayout.FIXED); + controls.add(new JLabel("Target color:"), HGroupLayout.FIXED); + controls.add(_target = new JTextField()); + JButton update = new JButton("Update"); + update.setActionCommand("update"); + update.addActionListener(this); + controls.add(update, HGroupLayout.FIXED); + add(controls, VGroupLayout.FIXED); + + HGroupLayout hlay = new HGroupLayout(HGroupLayout.STRETCH); + JPanel dists = new JPanel(hlay); + dists.add(new JLabel("HSV distances:"), HGroupLayout.FIXED); + dists.add(_hueD = new SliderAndLabel(0.0f, 1.0f, 0.05f)); + dists.add(_saturationD = new SliderAndLabel(0.0f, 1.0f, 0.8f)); + dists.add(_valueD = new SliderAndLabel(0.0f, 1.0f, 0.6f)); + add(dists, VGroupLayout.FIXED); + + hlay = new HGroupLayout(HGroupLayout.STRETCH); + JPanel offsets = new JPanel(hlay); + offsets.add(new JLabel("HSV offsets:"), HGroupLayout.FIXED); + offsets.add(_hueO = new SliderAndLabel(-1.0f, 1.0f, 0.1f)); + offsets.add(_saturationO = new SliderAndLabel(-1.0f, 1.0f, 0.0f)); + offsets.add(_valueO = new SliderAndLabel(-1.0f, 1.0f, 0.0f)); + add(offsets, VGroupLayout.FIXED); + + add(_status = new JTextField(), VGroupLayout.FIXED); + _status.setEditable(false); + + hlay = new HGroupLayout(); + hlay.setJustification(HGroupLayout.CENTER); + JPanel buttons = new JPanel(hlay); + JButton convert = new JButton("Convert"); + convert.setActionCommand("convert"); + convert.addActionListener(this); + buttons.add(convert); + add(buttons, VGroupLayout.FIXED); + + // listen for mouse clicks + images.addMouseListener(new MouseAdapter() { + public void mousePressed (MouseEvent event) { + RecolorImage.this.mousePressed(event); + } + }); + + // we'll be using a file chooser + String cwd = System.getProperty("user.dir"); + if (cwd == null) { + _chooser = new JFileChooser(); + _colChooser = new JFileChooser(); + } else { + _chooser = new JFileChooser(cwd); + _colChooser = new JFileChooser(cwd); + } + } + + /** + * Performs colorizations. + */ + protected void convert () + { + if (_image == null) { + return; + } + + // obtain the target color and offset + try { + BufferedImage image; + if (_mode.isSelected()) { + // All recolorings from file. + image = getAllRecolors(); + if (image == null) { + return; + } + } else { + // Normal recoloring + int color = Integer.parseInt(_source.getText(), 16); + + float hueD = _hueD.getValue(); + float satD = _saturationD.getValue(); + float valD = _valueD.getValue(); + float[] dists = new float[] { hueD, satD, valD }; + + float hueO = _hueO.getValue(); + float satO = _saturationO.getValue(); + float valO = _valueO.getValue(); + float[] offsets = new float[] { hueO, satO, valO }; + + image = ImageUtil.recolorImage( + _image, new Color(color), dists, offsets); + } + _newImage.setIcon(new ImageIcon(image)); + _status.setText("Recolored image."); + repaint(); + + } catch (NumberFormatException nfe) { + _status.setText("Invalid value: " + nfe.getMessage()); + } + } + + /** + * Gets an image with all recolorings of the selection colorization class. + */ + public BufferedImage getAllRecolors () + { + if (_colRepo == null) { + return null; + } + + ColorPository.ClassRecord colClass = + _colRepo.getClassRecord((String)_classList.getSelectedItem()); + int classId = colClass.classId; + + ArrayList imgs = new ArrayList(); + Interator iter = colClass.colors.keys(); + BufferedImage img = new BufferedImage(_image.getWidth(), + _image.getHeight()*colClass.colors.size(), + BufferedImage.TYPE_INT_ARGB); + Graphics gfx = img.getGraphics(); + int y = 0; + + while (iter.hasNext()) { + Colorization coloriz = + _colRepo.getColorization(classId, iter.nextInt()); + BufferedImage subImg = ImageUtil.recolorImage( + _image, coloriz.rootColor, coloriz.range, coloriz.offsets); + + gfx.drawImage(subImg, 0, y, null, null); + + y += subImg.getHeight(); + } + + return img; + } + + public void actionPerformed (ActionEvent event) + { + String cmd = event.getActionCommand(); + + if (cmd.equals("convert")) { + convert(); + } else if (cmd.equals("update")) { + // obtain the target color and offset + try { + int source = Integer.parseInt(_source.getText(), 16); + int target = Integer.parseInt(_target.getText(), 16); + float[] shsv = rgbToHSV(source); + float[] thsv = rgbToHSV(target); + + // set the offsets based on the differences + _hueO.setValue(thsv[0] - shsv[0]); + _saturationO.setValue(thsv[1] - shsv[1]); + _valueO.setValue(thsv[2] - shsv[2]); + + } catch (NumberFormatException nfe) { + _status.setText("Invalid value: " + nfe.getMessage()); + } + + } else if (cmd.equals("browse")) { + int result = _chooser.showOpenDialog(this); + if (result == JFileChooser.APPROVE_OPTION) { + setImage(_chooser.getSelectedFile()); + } + } else if (cmd.equals("reload")) { + setImage(_chooser.getSelectedFile()); + } else if (cmd.equals("browse_colorize")) { + int result = _colChooser.showOpenDialog(this); + if (result == JFileChooser.APPROVE_OPTION) { + setColorizeFile(_colChooser.getSelectedFile()); + } + } + } + + public void setImage (File path) + { + try { + _image = ImageIO.read(path); + _imagePath.setText(path.getPath()); + _oldImage.setIcon(new ImageIcon(_image)); + + } catch (IOException ioe) { + _status.setText("Error opening image file: " + ioe); + } + } + + /** + * Loads up the colorization classes from the specified file. + */ + public void setColorizeFile (File path) + { + try { + if (path.getName().endsWith("xml")) { + ColorPositoryParser parser = new ColorPositoryParser(); + _colRepo = + (ColorPository)(parser.parseConfig(path)); + } else { + _colRepo = + ColorPository.loadColorPository(new FileInputStream(path)); + } + + _classList.removeAllItems(); + Iterator iter = _colRepo.enumerateClasses(); + ArrayList names = new ArrayList(); + while (iter.hasNext()) { + String str = ((ColorPository.ClassRecord)iter.next()).name; + names.add(str); + } + + Collections.sort(names); + + iter = names.iterator(); + while (iter.hasNext()) { + _classList.addItem((String)iter.next()); + } + + _classList.setSelectedIndex(0); + _colFilePath.setText(path.getPath()); + } catch (Exception ex) { + _status.setText("Error opening colorization file: " + ex); + } + } + + protected static float[] rgbToHSV (int rgb) + { + float[] hsv = new float[3]; + Color color = new Color(rgb); + Color.RGBtoHSB(color.getRed(), color.getGreen(), + color.getBlue(), hsv); + return hsv; + } + + public void mousePressed (MouseEvent event) + { + // if the click was in the bounds of the source image, grab the + // pixel color and use that to set the "source" color + int x = event.getX(), y = event.getY(); + Rectangle ibounds = _oldImage.getBounds(); + if (ibounds.contains(x, y)) { + int argb = _image.getRGB(x - ibounds.x, y - ibounds.y); + String cstr = Integer.toString(argb & 0xFFFFFF, 16); + _source.setText(cstr.toUpperCase()); + _colorLabel.setBackground(new Color(argb)); + _colorLabel.repaint(); + } + } + + /** + * Class with linked slider and label arranged vertically. + */ + protected class SliderAndLabel extends JPanel + { + public SliderAndLabel (float minf, float maxf, float valuef) + { + int min = (int)(minf*CONVERSION); + int max = (int)(maxf*CONVERSION); + int value = (int)(valuef*CONVERSION); + setLayout(new VGroupLayout(VGroupLayout.STRETCH)); + _intField = new JLabel(String.valueOf(value/CONVERSION)); + _slider = new JSlider(min, max, value); + + _slider.addChangeListener(new ChangeListener() { + public void stateChanged (ChangeEvent ce) { + _intField.setText(String.valueOf( + ((float)_slider.getValue())/CONVERSION)); + + convert(); + } + }); + add(_intField); + add(_slider); + } + + public float getValue () + { + return _slider.getValue()/CONVERSION; + } + + public void setValue (float val) + { + _slider.setValue((int)(val*CONVERSION)); + } + + protected JSlider _slider; + protected JLabel _intField; + + protected final static float CONVERSION = 1000.0f; + } + + public static void main (String[] args) + { + try { + JFrame frame = new JFrame("Image recoloring test"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + RecolorImage panel = new RecolorImage(); + + // load up the image from the command line if one was + // specified + if (args.length > 0) { + panel.setImage(new File(args[0])); + } + + frame.getContentPane().add(panel, BorderLayout.CENTER); + frame.setSize(600, 600); + SwingUtil.centerWindow(frame); + frame.setVisible(true); + + } catch (Exception e) { + e.printStackTrace(System.err); + } + } + + protected BufferedImage _image; + protected JFileChooser _chooser; + protected JFileChooser _colChooser; + protected JTextField _imagePath; + protected JTextField _colFilePath; + + protected JLabel _oldImage; + protected JLabel _newImage; + protected JPanel _colorLabel; + + protected JTextField _source; + protected JTextField _target; + + protected SliderAndLabel _hueO; + protected SliderAndLabel _saturationO; + protected SliderAndLabel _valueO; + + protected SliderAndLabel _hueD; + protected SliderAndLabel _saturationD; + protected SliderAndLabel _valueD; + + protected JTextField _status; + + protected JComboBox _classList; + protected JToggleButton _mode; + + protected ColorPository _colRepo; + + protected static final String IMAGE_PATH = + // "bundles/components/pirate/torso/regular/standing.png"; + "bundles/components/pirate/head/regular/standing.png"; +} diff --git a/src/java/com/threerings/media/util/AStarPathUtil.java b/src/java/com/threerings/media/util/AStarPathUtil.java new file mode 100644 index 00000000..4583fda0 --- /dev/null +++ b/src/java/com/threerings/media/util/AStarPathUtil.java @@ -0,0 +1,437 @@ +// +// $Id: AStarPathUtil.java 3413 2005-03-18 01:13:09Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Point; +import java.util.*; + +import com.samskivert.util.HashIntMap; + +import com.threerings.media.util.MathUtil; + +/** + * The AStarPathUtil class provides a facility for + * finding a reasonable path between two points in a scene using the + * A* search algorithm. + * + *

See the path-finding article on + * + * Gamasutra for more detailed information. + */ +public class AStarPathUtil +{ + /** + * Provides traversibility information when computing paths. + */ + public static interface TraversalPred + { + /** + * Requests to know if the specified traverser (which was provided + * in the call to {@link #getPath}) can traverse the specified + * tile coordinate. + */ + public boolean canTraverse (Object traverser, int x, int y); + } + + /** + * Considers all the possible steps the piece in question can take. + */ + public static class Stepper + { + public void init (Info info, Node n) + { + _info = info; + _node = n; + } + + /** + * Should call {@link #considerStep} in turn on all possible steps + * from the specified coordinates. No checking must be done as to + * whether the step is legal, that will be handled later. Just + * enumerate all possible steps. + */ + public void considerSteps (int x, int y) + { + considerStep(x - 1, y - 1, DIAGONAL_COST); + considerStep(x, y - 1, ADJACENT_COST); + considerStep(x + 1, y - 1, DIAGONAL_COST); + considerStep(x - 1, y, ADJACENT_COST); + considerStep(x + 1, y, ADJACENT_COST); + considerStep(x - 1, y + 1, DIAGONAL_COST); + considerStep(x, y + 1, ADJACENT_COST); + considerStep(x + 1, y + 1, DIAGONAL_COST); + } + + protected void considerStep (int x, int y, int cost) + { + AStarPathUtil.considerStep(_info, _node, x, y, cost); + } + + protected Info _info; + protected Node _node; + } + + /** The standard cost to move between nodes. */ + public static final int ADJACENT_COST = 10; + + /** The cost to move diagonally. */ + public static final int DIAGONAL_COST = (int)Math.sqrt( + (ADJACENT_COST * ADJACENT_COST) * 2); + + /** + * Return a list of Point objects representing a path + * from coordinates (ax, by) to (bx, by), + * inclusive, determined by performing an A* search in the given + * scene's base tile layer. Assumes the starting and destination nodes + * are traversable by the specified traverser. + * + * @param tpred lets us know what tiles are traversible. + * @param stepper enumerates the possible steps. + * @param trav the traverser to follow the path. + * @param longest the longest allowable path in tile traversals. + * @param ax the starting x-position in tile coordinates. + * @param ay the starting y-position in tile coordinates. + * @param bx the ending x-position in tile coordinates. + * @param by the ending y-position in tile coordinates. + * @param partial if true, a partial path will be returned that gets + * us as close as we can to the goal in the event that a complete path + * cannot be located. + * + * @return the list of points in the path. + */ + public static List getPath (TraversalPred tpred, Stepper stepper, + Object trav, int longest, int ax, int ay, + int bx, int by, boolean partial) + { + Info info = new Info(tpred, trav, longest, bx, by); + + // set up the starting node + Node s = info.getNode(ax, ay); + s.g = 0; + s.h = getDistanceEstimate(ax, ay, bx, by); + s.f = s.g + s.h; + + // push starting node on the open list + info.open.add(s); + _considered = 1; + + // track the best path + float bestdist = Float.MAX_VALUE; + Node bestpath = null; + + // while there are more nodes on the open list + while (info.open.size() > 0) { + + // pop the best node so far from open + Node n = (Node)info.open.first(); + info.open.remove(n); + + // if node is a goal node + if (n.x == bx && n.y == by) { + // construct and return the acceptable path + return getNodePath(n); + } else if (partial) { + float pathdist = MathUtil.distance(n.x, n.y, bx, by); + if (pathdist < bestdist) { + bestdist = pathdist; + bestpath = n; + } + } + + // consider each successor of the node + stepper.init(info, n); + stepper.considerSteps(n.x, n.y); + + // push the node on the closed list + info.closed.add(n); + } + + // return the best path we could find if we were asked to do so + if (bestpath != null) { + return getNodePath(bestpath); + } + + // no path found + return null; + } + + /** + * Gets a path with the default stepper which assumes the piece can + * move one in any of the eight cardinal directions. + */ + public static List getPath (TraversalPred tpred, Object trav, int longest, + int ax, int ay, int bx, int by, boolean partial) + { + return getPath( + tpred, new Stepper(), trav, longest, ax, ay, bx, by, partial); + } + + /** + * Returns the number of nodes considered in computing the most recent + * path. + */ + public static int getConsidered () + { + return _considered; + } + + /** + * Consider the step (n.x, n.y) to (x, y) + * for possible inclusion in the path. + * + * @param info the info object. + * @param n the originating node for the step. + * @param x the x-coordinate for the destination step. + * @param y the y-coordinate for the destination step. + */ + protected static void considerStep ( + Info info, Node n, int x, int y, int cost) + { + // skip node if it's outside the map bounds or otherwise impassable + if (!info.isStepValid(n.x, n.y, x, y)) { + return; + } + + // calculate the new cost for this node + int newg = n.g + cost; + + // make sure the cost is reasonable + if (newg > info.maxcost) { +// Log.info("Rejected costly step."); + return; + } + + // retrieve the node corresponding to this location + Node np = info.getNode(x, y); + + // skip if it's already in the open or closed list or if its + // actual cost is less than the just-calculated cost + if ((info.open.contains(np) || info.closed.contains(np)) && + np.g <= newg) { + return; + } + + // remove the node from the open list since we're about to + // modify its score which determines its placement in the list + info.open.remove(np); + + // update the node's information + np.parent = n; + np.g = newg; + np.h = getDistanceEstimate(np.x, np.y, info.destx, info.desty); + np.f = np.g + np.h; + + // remove it from the closed list if it's present + info.closed.remove(np); + + // add it to the open list for further consideration + info.open.add(np); + _considered++; + } + + /** + * Return a list of Point objects detailing the path + * from the first node (the given node's ultimate parent) to the + * ending node (the given node itself.) + * + * @param n the ending node in the path. + * + * @return the list detailing the path. + */ + protected static List getNodePath (Node n) + { + Node cur = n; + ArrayList path = new ArrayList(); + + while (cur != null) { + // add to the head of the list since we're traversing from + // the end to the beginning + path.add(0, new Point(cur.x, cur.y)); + + // advance to the next node in the path + cur = cur.parent; + } + + return path; + } + + /** + * Return a heuristic estimate of the cost to get from (ax, + * ay) to (bx, by). + */ + protected static int getDistanceEstimate (int ax, int ay, int bx, int by) + { + // we're doing all of our cost calculations based on geometric + // distance times ten + int xsq = bx - ax; + int ysq = by - ay; + return (int) (ADJACENT_COST * Math.sqrt(xsq * xsq + ysq * ysq)); + } + + /** + * A holding class to contain the wealth of information referenced + * while performing an A* search for a path through a tile array. + */ + protected static class Info + { + /** Knows whether or not tiles are traversable. */ + public TraversalPred tpred; + + /** The tile array dimensions. */ + public int tilewid, tilehei; + + /** The traverser moving along the path. */ + public Object trav; + + /** The set of open nodes being searched. */ + public SortedSet open; + + /** The set of closed nodes being searched. */ + public ArrayList closed; + + /** The destination coordinates in the tile array. */ + public int destx, desty; + + /** The maximum cost of any path that we'll consider. */ + public int maxcost; + + public Info (TraversalPred tpred, Object trav, + int longest, int destx, int desty) + { + // save off references + this.tpred = tpred; + this.trav = trav; + this.destx = destx; + this.desty = desty; + + // compute our maximum path cost + this.maxcost = longest * ADJACENT_COST; + + // construct the open and closed lists + open = new TreeSet(); + closed = new ArrayList(); + } + + /** + * Returns whether moving from the given source to destination + * coordinates is a valid move. + */ + protected boolean isStepValid (int sx, int sy, int dx, int dy) + { + // not traversable if the destination itself fails test + if (!isTraversable(dx, dy)) { + return false; + } + + // if the step is diagonal, make sure the corners don't impede + // our progress + if ((Math.abs(dx - sx) == 1) && (Math.abs(dy - sy) == 1)) { + return isTraversable(dx, sy) && isTraversable(sx, dy); + } + + // non-diagonals are always traversable + return true; + } + + /** + * Returns whether the given coordinate is valid and traversable. + */ + protected boolean isTraversable (int x, int y) + { + return tpred.canTraverse(trav, x, y); + } + + /** + * Get or create the node for the specified point. + */ + public Node getNode (int x, int y) + { + // note: this _could_ break for unusual values of x and y. + // perhaps use a IntTuple as a key? Bleah. + int key = (x << 16) | (y & 0xffff); + Node node = (Node) _nodes.get(key); + if (node == null) { + node = new Node(x, y); + _nodes.put(key, node); + } + return node; + } + + /** The nodes being considered in the path. */ + protected HashIntMap _nodes = new HashIntMap(); + } + + /** + * A class that represents a single traversable node in the tile array + * along with its current A*-specific search information. + */ + protected static class Node implements Comparable + { + /** The node coordinates. */ + public int x, y; + + /** The actual cheapest cost of arriving here from the start. */ + public int g; + + /** The heuristic estimate of the cost to the goal from here. */ + public int h; + + /** The score assigned to this node. */ + public int f; + + /** The node from which we reached this node. */ + public Node parent; + + /** The node's monotonically-increasing unique identifier. */ + public int id; + + public Node (int x, int y) + { + this.x = x; + this.y = y; + id = _nextid++; + } + + public int compareTo (Object o) + { + int bf = ((Node)o).f; + + // since the set contract is fulfilled using the equality results + // returned here, and we'd like to allow multiple nodes with + // equivalent scores in our set, we explicitly define object + // equivalence as the result of object.equals(), else we use the + // unique node id since it will return a consistent ordering for + // the objects. + if (f == bf) { + return (this == o) ? 0 : (id - ((Node)o).id); + } + + return f - bf; + } + + /** The next unique node id. */ + protected static int _nextid = 0; + } + + /** The number of nodes considered in computing our path. */ + protected static int _considered = 0; +} diff --git a/src/java/com/threerings/media/util/ArcPath.java b/src/java/com/threerings/media/util/ArcPath.java new file mode 100644 index 00000000..d8a2562b --- /dev/null +++ b/src/java/com/threerings/media/util/ArcPath.java @@ -0,0 +1,238 @@ +// +// $Id: ArcPath.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; + +import com.samskivert.util.StringUtil; + +import com.threerings.util.DirectionUtil; + +/** + * The line path is used to cause a pathable to go from point A to point B + * along the arc of an ellipse in a certain number of milliseconds. + */ +public class ArcPath extends TimedPath +{ + /** An orientation constant indicating that the normal (eight) + * directions should be used to orient the pathable being made to + * follow this path. */ + public static final int NORMAL = 0; + + /** An orientation constant indicating that the fine (sixteen) + * directions should be used to orient the pathable being made to + * follow this path. */ + public static final int FINE = 1; + + /** An orientation indicating that the pathable should not be oriented + * as it moves along the path. + */ + public static final int NONE = 2; + + /** + * Creates an arc path that will animate a pathable from the specified + * starting position along an ellipse defined by the supplied + * parameters. The pathable will travel the specified number of + * radians along the arc of that ellipse. A positive number of radians + * indicates counter-clockwise travel along the circle, a negative + * number, clockwise rotation. + * + * @param start the starting point for the pathable. + * @param xradius the length of the x radius. + * @param yradius the length of the y radius. + * @param sangle the starting angle. + * @param delta the angle through which the pathable should be moved. + * @param duration the number of milliseconds during which to effect + * the animation. + * @param orient an orientation code indicating how the pathable + * should be oriented when following the path, either {@link #NORMAL}, + * or {@link #FINE}. + */ + public ArcPath (Point start, double xradius, double yradius, + double sangle, double delta, long duration, + int orient) + { + super(duration); + _xradius = xradius; + _yradius = yradius; + _sangle = sangle; + _delta = delta; + _orient = orient; + + // compute the center of the ellipse + _center = new Point( + (int)(start.x - Math.round(Math.cos(sangle) * xradius)), + (int)(start.y - Math.round(Math.sin(sangle) * yradius))); + } + + /** + * Return a copy of the path, translated by the specified amounts. + */ + public Path getTranslatedInstance (int x, int y) + { + int startx = + (int)(_center.x + Math.round(Math.cos(_sangle) * _xradius)); + int starty = + (int)(_center.y + Math.round(Math.sin(_sangle) * _yradius)); + + return new ArcPath(new Point (startx + x, starty + y), + _xradius, _yradius, _sangle, _delta, _duration, _orient); + } + + /** + * Sets the offset that is applied to the pathable whenever it is + * oriented. This offset is in clockwise units whose granularity is + * specified by the {@link #NORMAL} or {@link #FINE} setting supplied + * to the path at construct time. The intent here is to allow arc + * paths to be applied that don't orient the pathable in the direction + * they are traveling but instead in some fixed offset from that + * direction. + */ + public void setOrientOffset (int offset) + { + _orientOffset = offset; + } + + // documentation inherited + public boolean tick (Pathable pable, long timestamp) + { + double angle; + boolean modified = false; + + // if we've blown past our arrival time... + if (timestamp >= _startStamp + _duration) { + // ...force the angle to the destination angle + angle = _sangle + _delta; + + } else { + // otherwise, compute the angle at which we should place the + // pathable based on the elapsed time + long elapsed = timestamp - _startStamp; + angle = _sangle + _delta * elapsed / _duration; + } + + // determine where we should be along the path + computePosition(_center, _xradius, _yradius, angle, _tpos); + + // Skip this if we are not reorienting as we follow the path. + if (_orient != NONE) { + // compute the pathable's new orientation + double theta = angle + ((_delta > 0) ? Math.PI/2 : -Math.PI/2); + int orient; + switch (_orient) { + default: + case NORMAL: + orient = DirectionUtil.getDirection(theta); + // adjust it appropriately + orient = DirectionUtil.rotateCW(orient, 2*_orientOffset); + break; + + case FINE: + orient = DirectionUtil.getFineDirection(theta); + // adjust it appropriately + orient = DirectionUtil.rotateCW(orient, _orientOffset); + break; + } + + // update the pathable's orientation if it changed + if (pable.getOrientation() != orient) { + pable.setOrientation(orient); + modified = true; + } + } + + // update the pathable's location if it moved + if (pable.getX() != _tpos.x || pable.getY() != _tpos.y) { + pable.setLocation(_tpos.x, _tpos.y); + modified = true; + } + + // if we completed our path, let the sprite know + if (angle == _sangle + _delta) { + pable.pathCompleted(timestamp); + } + + return modified; + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + int x = (int)(_center.x - _xradius), y = (int)(_center.y - _yradius); + int width = (int)(2*_xradius), height = (int)(2*_yradius); + int sangle = (int)(Math.round(180 * _sangle / Math.PI)), + delta = (int)(Math.round(180 * _delta / Math.PI)); + + gfx.setColor(Color.blue); + gfx.drawRect(x, y, width-1, height-1); + + gfx.setColor(Color.yellow); + gfx.drawArc(x, y, width-1, height-1, 0, 360); + + gfx.setColor(Color.red); + gfx.drawArc(x, y, width-1, height-1, 360-sangle, -delta); + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", center=").append(StringUtil.toString(_center)); + buf.append(", sangle=").append(_sangle); + buf.append(", delta=").append(_delta); + buf.append(", radii=").append(_xradius).append("/").append(_yradius); + } + + /** + * Computes the position of an entity along the path defined by the + * supplied parameters assuming that it must finish the path in the + * specified duration (in millis) and has been traveling the path for + * the specified number of elapsed milliseconds. + */ + public static void computePosition ( + Point center, double xradius, double yradius, double angle, Point pos) + { + pos.x = (int)Math.round(center.x + xradius * Math.cos(angle)); + pos.y = (int)Math.round(center.y + yradius * Math.sin(angle)); + } + + /** The center of our ellipse. */ + protected Point _center; + + /** Our ellipse radii. */ + protected double _xradius, _yradius; + + /** Our starting and delta angles. */ + protected double _sangle, _delta; + + /** The method to be used to orient the pathable. */ + protected int _orient; + + /** An orientation offset used when orienting our pathable. */ + protected int _orientOffset = 0; + + /** A temporary point used when computing our position along the + * path. */ + protected Point _tpos = new Point(); +} diff --git a/src/java/com/threerings/media/util/BackgroundTiler.java b/src/java/com/threerings/media/util/BackgroundTiler.java new file mode 100644 index 00000000..200cdd8a --- /dev/null +++ b/src/java/com/threerings/media/util/BackgroundTiler.java @@ -0,0 +1,208 @@ +// +// $Id: BackgroundTiler.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Graphics; +import java.awt.image.BufferedImage; + +import com.threerings.media.Log; + +/** + * Used to tile a background image into regions of various sizes. The + * source image is divided into nine quadrants (of mostly equal size) + * which are tiled accordingly to fill whatever size background image is + * desired. + */ +public class BackgroundTiler +{ + /** + * Creates a background tiler with the specified source image. + */ + public BackgroundTiler (BufferedImage src) + { + // make sure we were given the goods + if (src == null) { + Log.info("Backgrounder given null source image. Coping."); + return; + } + + // compute some values + int width = src.getWidth(null); + int height = src.getHeight(null); + + _w3 = width/3; + _cw3 = width-2*_w3; + _h3 = height/3; + _ch3 = height-2*_h3; + + // make sure the image suits our minimum useful dimensions + if (_w3 <= 0 || _cw3 <= 0 || _h3 <= 0 || _ch3 <= 0) { + Log.warning("Backgrounder given source image of insufficient " + + "size for tiling " + + "[width=" + width + ", height=" + height + "]."); + return; + } + + // create our sub-divided images + _tiles = new BufferedImage[9]; + int[] sy = { 0, _h3, _h3+_ch3 }; + int[] thei = { _h3, _ch3, _h3 }; + for (int i = 0; i < 3; i++) { + _tiles[3*i] = src.getSubimage(0, sy[i], _w3, thei[i]); + _tiles[3*i+1] = src.getSubimage(_w3, sy[i], _cw3, thei[i]); + _tiles[3*i+2] = + src.getSubimage(width-_w3, sy[i], _w3, thei[i]); + } + } + + /** + * Returns the "natural" width of the image being used to tile the + * background. + */ + public int getNaturalWidth () + { + return _w3*2+_cw3; + } + + /** + * Returns the "natural" height of the image being used to tile the + * background. + */ + public int getNaturalHeight () + { + return _h3*2+_ch3; + } + + /** + * Fills the requested region with the background defined by our + * source image. + */ + public void paint (Graphics g, int x, int y, int width, int height) + { + // bail out now if we were passed a bogus source image at + // construct time + if (_tiles == null) { + return; + } + + int rwid = width-2*_w3, rhei = height-2*_h3; + + g.drawImage(_tiles[0], x, y, _w3, _h3, null); + g.drawImage(_tiles[1], x + _w3, y, rwid, _h3, null); + g.drawImage(_tiles[2], x + _w3 + rwid, y, _w3, _h3, null); + + y += _h3; + g.drawImage(_tiles[3], x, y, _w3, rhei, null); + g.drawImage(_tiles[4], x + _w3, y, rwid, rhei, null); + g.drawImage(_tiles[5], x + _w3 + rwid, y, _w3, rhei, null); + + y += rhei; + g.drawImage(_tiles[6], x, y, _w3, _h3, null); + g.drawImage(_tiles[7], x + _w3, y, rwid, _h3, null); + g.drawImage(_tiles[8], x + _w3 + rwid, y, _w3, _h3, null); + } + + /** Our nine sub-divided images. */ + protected BufferedImage[] _tiles; + + /** One third of width/height of our source image. */ + protected int _w3, _h3; + + /** The size of the center chunk of our subdivided images. */ + protected int _cw3, _ch3; +} + + +// Below is an alternate implementation that uses only the source image +// without chopping it up. It ends up running very slightly slower, that +// may be because the documentation for the version of drawImage used below +// states that scaled instances will not be cached, that the scaling will +// happen each time on the fly. On the other hand, maybe that's a good thing. +// I like it better because it doesn't have to have 9 separate images and +// because it does the render in a loop, which is good fun. + +//public class BackgroundTiler +//{ +// /** +// * Creates a background tiler with the specified source image. +// */ +// public BackgroundTiler (BufferedImage src) +// { +// // make sure we were given the goods +// if (src == null) { +// Log.info("Backgrounder given null source image. Coping."); +// return; +// } +// +// _src = src; +// +// // compute some values +// int width = src.getWidth(null); +// int height = src.getHeight(null); +// +// _w3 = width/3; +// _cw3 = width-2*_w3; +// _h3 = height/3; +// _ch3 = height-2*_h3; +// +// _sx = new int[] { 0, _w3, width - _w3, width }; +// _sy = new int[] { 0, _h3, height - _h3, height }; +// _dx = new int[4]; +// _dy = new int[4]; +// } +// +// /** +// * Fills the requested region with the background defined by our +// * source image. +// */ +// public void paint (Graphics g, int x, int y, int width, int height) +// { +// _dx[0] = x; +// _dx[1] = x + _w3; +// _dx[2] = x + width - _w3; +// _dx[3] = x + width; +// +// _dy[0] = y; +// _dy[1] = y + _h3; +// _dy[2] = y + height - _h3; +// _dy[3] = y + height; +// +// for (int jj=0; jj < 3; jj++) { +// for (int ii=0; ii < 3; ii++) { +// g.drawImage(_src, _dx[ii], _dy[jj], _dx[ii + 1], _dy[jj + 1], +// _sx[ii], _sy[jj], _sx[ii + 1], _sy[jj + 1], null); +// } +// } +// } +// +// /** Our source image. */ +// protected BufferedImage _src; +// +// /** Coordinates. */ +// protected int[] _sx, _sy, _dx, _dy; +// +// /** One third of width/height of our source image. */ +// protected int _w3, _h3; +// +// /** The size of the center chunk of our subdivided images. */ +// protected int _cw3, _ch3; +//} diff --git a/src/java/com/threerings/media/util/BobblePath.java b/src/java/com/threerings/media/util/BobblePath.java new file mode 100644 index 00000000..c6d8db79 --- /dev/null +++ b/src/java/com/threerings/media/util/BobblePath.java @@ -0,0 +1,191 @@ +// +// $Id: BobblePath.java 4188 2006-06-13 18:03:48Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Color; +import java.awt.Graphics2D; + +import com.samskivert.util.RandomUtil; + +import com.threerings.media.Log; + +/** + * Bobble a Pathable. + */ +public class BobblePath implements Path +{ + /** + * Construct a bobble path that updates as often as possible. + * + * @param dx the variance in the x direction. + * @param dy the variance in the y direction. + * @param duration the duration to bobble, or -1 to bobble until + * stop() is called. + */ + public BobblePath (int dx, int dy, long duration) + { + this(dx, dy, duration, 1L); + } + + /** + * Construct a bobble path. + * + * @param dx the variance in the x direction. + * @param dy the variance in the y direction. + * @param duration the duration to bobble, or -1 to bobble until + * stop() is called. + * @param updateFreq how often to update the Pathable's location. + */ + public BobblePath (int dx, int dy, long duration, long updateFreq) + { + _updateFreq = updateFreq; + _duration = duration; + setVariance(dx, dy); + } + + /** + * Set the variance of bobblin' in each direction. + */ + public void setVariance (int dx, int dy) + { + if ((dx < 0) || (dy < 0) || ((dx == 0) && (dy == 0))) { + throw new IllegalArgumentException( + "Variance values must be positive, " + + "and at least one must be non-zero."); + } else { + _dx = dx; + _dy = dy; + } + } + + /** + * Set a new update frequency. + */ + public void setUpdateFrequency (long freq) + { + _updateFreq = freq; + } + + /** + * Have the Pathable stop bobbling asap. + */ + public void stop () + { + // it will stop on the next tick.. + _stopTime = 0L; + } + + // documentation inherited from interface Path + public void init (Pathable pable, long tickstamp) + { + _sx = pable.getX(); + _sy = pable.getY(); + // change the duration to a real stop time + if (_duration == -1L) { + _stopTime = Long.MAX_VALUE; + } else { + _stopTime = tickstamp + _duration; + } + _nextMove = tickstamp; + } + + // documentation inherited from interface Path + public boolean tick (Pathable pable, long tickStamp) + { + // see if we need to stop + if (_stopTime <= tickStamp) { + boolean updated = updatePositionTo(pable, _sx, _sy); + pable.pathCompleted(tickStamp); + return updated; + } + + // see if it's time to move.. + if (_nextMove > tickStamp) { + return false; + } + + // when bobbling, it's bad form to bobble into the same position + int newx, newy; + do { + newx = _sx + RandomUtil.getInt(_dx * 2 + 1) - _dx; + newy = _sy + RandomUtil.getInt(_dy * 2 + 1) - _dy; + } while (! updatePositionTo(pable, newx, newy)); + + // and update the next time to move + _nextMove = tickStamp + _updateFreq; + return true; + } + + // documentation inherited from interface Path + public void fastForward (long timeDelta) + { + _stopTime += timeDelta; + _nextMove += timeDelta; + } + + // documentation inherited from interface Path + public void paint (Graphics2D gfx) + { + // for debugging, show the bobble bounds + gfx.setColor(Color.RED); + gfx.drawRect(_sx - _dx, _sy - _dy, _dx * 2, _dy * 2); + } + + // documentation inherited from interface + public void wasRemoved (Pathable pable) + { + // reset the pathable to its initial location + pable.setLocation(_sx, _sy); + } + + /** + * Update the position of the pathable or return false + * if it's already there. + */ + protected boolean updatePositionTo (Pathable pable, int x, int y) + { + if ((pable.getX() == x) && (pable.getY() == y)) { + return false; + } else { + pable.setLocation(x, y); + return true; + } + } + + /** The initial position of the pathable. */ + protected int _sx, _sy; + + /** The variance we will bobble around that initial position. */ + protected int _dx, _dy; + + /** How long we'll bobble. */ + protected long _duration; + + /** How often we update the locations. */ + protected long _updateFreq; + + /** The time at which we'll stop pathin'. */ + protected long _stopTime; + + /** The time at which we'll next update the position of the pathable. */ + protected long _nextMove = 0L; +} diff --git a/src/java/com/threerings/media/util/DelayPath.java b/src/java/com/threerings/media/util/DelayPath.java new file mode 100644 index 00000000..ef83b482 --- /dev/null +++ b/src/java/com/threerings/media/util/DelayPath.java @@ -0,0 +1,91 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Graphics2D; +import java.awt.Point; + +/** + * A convenience path that waits a specified amount of time. + */ +public class DelayPath extends TimedPath +{ + /** + * Cause the current path to remain unchanged for the duration. + */ + public DelayPath (long duration) + { + this(null, duration); + } + + /** + * Move to the sprite to the supplied location then wait for the duration. + */ + public DelayPath (int x, int y, long duration) + { + this(new Point(x, y), duration); + } + + /** + * Move to the sprite to the supplied location then wait for the duration. + */ + public DelayPath (Point source, long duration) + { + super(duration); + _source = source; + } + + // documentation inherited + public void init (Pathable pable, long timestamp) + { + super.init(pable, timestamp); + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + } + + // documentation inherited + public boolean tick (Pathable pable, long tickstamp) + { + if (tickstamp >= _startStamp + _duration) { + if (_source != null) { + pable.setLocation(_source.x, _source.y); + } + pable.pathCompleted(tickstamp); + return (_source != null); + } + + // If necessary, move the sprite to the supplied location + if (_source != null && (pable.getX() != _source.x || + pable.getY() != _source.y)) { + pable.setLocation(_source.x, _source.y); + return true; + } + + return false; + } + + /** Source point. */ + protected Point _source; +} diff --git a/src/java/com/threerings/media/util/DelegatingPathable.java b/src/java/com/threerings/media/util/DelegatingPathable.java new file mode 100644 index 00000000..46eb58fc --- /dev/null +++ b/src/java/com/threerings/media/util/DelegatingPathable.java @@ -0,0 +1,87 @@ +// +// $Id: DelegatingPathable.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Rectangle; + +/** + * Delegates all calls to a delegate pathable. One would derive from this + * class and override just the methods in which that one desired to + * intervene. + */ +public class DelegatingPathable implements Pathable +{ + public DelegatingPathable (Pathable delegate) + { + _delegate = delegate; + } + + // documentation inherited from interface + public int getX () + { + return _delegate.getX(); + } + + // documentation inherited from interface + public int getY () + { + return _delegate.getY(); + } + + // documentation inherited from interface + public Rectangle getBounds () + { + return _delegate.getBounds(); + } + + // documentation inherited from interface + public void setLocation (int x, int y) + { + _delegate.setLocation(x, y); + } + + // documentation inherited from interface + public void setOrientation (int orient) + { + _delegate.setOrientation(orient); + } + + // documentation inherited from interface + public int getOrientation () + { + return _delegate.getOrientation(); + } + + // documentation inherited from interface + public void pathBeginning () + { + _delegate.pathBeginning(); + } + + // documentation inherited from interface + public void pathCompleted (long timestamp) + { + _delegate.pathCompleted(timestamp); + } + + protected Pathable _delegate; +} diff --git a/src/java/com/threerings/media/util/FrameSequencer.java b/src/java/com/threerings/media/util/FrameSequencer.java new file mode 100644 index 00000000..c5b47bbe --- /dev/null +++ b/src/java/com/threerings/media/util/FrameSequencer.java @@ -0,0 +1,113 @@ +// +// $Id: FrameSequencer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +/** + * Used to control animation timing when displaying a {@link + * MultiFrameImage}. This interface allows for constant framerates, or + * more sophisticated animation timing. + */ +public interface FrameSequencer +{ + /** + * Called prior to the execution of an animation sequence (not + * necessarily immediately, so time stamps should be obtained in the + * first call to tick). + * + * @param source the multie-frame image that is providing the + * animation frames. + */ + public void init (MultiFrameImage source); + + /** + * Called every display frame, the frame sequencer should return the + * index of the animation frame that should be displayed during this + * tick. If the sequencer returns -1, it is taken as an indication + * that the animation is finished and should be stopped. + */ + public int tick (long tickStamp); + + /** + * Called if the display is paused for some length of time and then + * unpaused. Sequencers should adjust any time stamps they are + * maintaining internally by the delta so that time maintains the + * illusion of flowing smoothly forward. + */ + public void fastForward (long timeDelta); + + /** + * A frame sequencer that delivers a constant frame rate in either one + * shot or looping mode. + */ + public static class ConstantRate implements FrameSequencer + { + /** + * Creates a constant rate frame sequencer with the desired target + * frames per second. + * + * @param framesPerSecond the target frames per second. + * @param loop if false, the sequencer will report the end of + * the animation after progressing through all of the frames once, + * otherwise it will loop indefinitely. + */ + public ConstantRate (double framesPerSecond, boolean loop) + { + _millisPerFrame = (long)(1000d / framesPerSecond); + _loop = loop; + } + + // documentation inherited from interface + public void init (MultiFrameImage source) + { + _frameCount = source.getFrameCount(); + _startStamp = 0l; + } + + // documentation inherited from interface + public int tick (long tickStamp) + { + // obtain our starting timestamp if we don't already have one + if (_startStamp == 0) { + _startStamp = tickStamp; + } + + // compute our current frame index + int frameIdx = (int)((tickStamp - _startStamp) / _millisPerFrame); + + // if we're not looping and we've exhausted our frames, we + // return -1 to indicate that the animation should stop + return (_loop || frameIdx < _frameCount) ? (frameIdx % _frameCount) + : -1; + } + + // documentation inherited from interface + public void fastForward (long timeDelta) + { + _startStamp += timeDelta; + } + + protected long _millisPerFrame; + protected boolean _loop; + protected int _frameCount; + protected long _startStamp; + } +} diff --git a/src/java/com/threerings/media/util/LinePath.java b/src/java/com/threerings/media/util/LinePath.java new file mode 100644 index 00000000..4e0ad4bc --- /dev/null +++ b/src/java/com/threerings/media/util/LinePath.java @@ -0,0 +1,151 @@ +// +// $Id: LinePath.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; + +import com.samskivert.util.StringUtil; + +/** + * The line path is used to cause a pathable to go from point A to point B + * in a certain number of milliseconds. + */ +public class LinePath extends TimedPath +{ + /** + * Constructs a line path between the two specified points that will + * be followed in the specified number of milliseconds. + */ + public LinePath (int x1, int y1, int x2, int y2, long duration) + { + this(new Point(x1, y1), new Point(x2, y2), duration); + } + + /** + * Constructs a line path between the two specified points that will + * be followed in the specified number of milliseconds. + */ + public LinePath (Point source, Point dest, long duration) + { + super(duration); + _source = source; + _dest = dest; + } + + /** + * Constructs a line path that moves a pathable from + * whatever its location is at init time to the dest point over + * the specified number of milliseconds. + */ + public LinePath (Point dest, long duration) + { + this(null, dest, duration); + } + + /** + * Return a copy of the path, translated by the specified amounts. + */ + public Path getTranslatedInstance (int x, int y) + { + if (_source == null) { + return new LinePath(null, new Point(_dest.x + x, _dest.y + y), + _duration); + } else { + return new LinePath(_source.x + x, _source.y + y, _dest.x + x, + _dest.y + y, _duration); + } + } + + // documentation inherited + public void init (Pathable pable, long timestamp) + { + super.init(pable, timestamp); + + // fill in the source if necessary. + if (_source == null) { + _source = new Point(pable.getX(), pable.getY()); + } + } + + // documentation inherited + public boolean tick (Pathable pable, long timestamp) + { + // if we've blown past our arrival time, we need to get our bootay + // to the prearranged spot and get the hell out + if (timestamp >= _startStamp + _duration) { + pable.setLocation(_dest.x, _dest.y); + pable.pathCompleted(timestamp); + return true; + } + + // determine where we should be along the path + long elapsed = timestamp - _startStamp; + computePosition(_source, _dest, elapsed, _duration, _tpos); + + // only update the pathable's location if it actually moved + if (pable.getX() != _tpos.x || pable.getY() != _tpos.y) { + pable.setLocation(_tpos.x, _tpos.y); + return true; + } + + return false; + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + gfx.setColor(Color.red); + gfx.drawLine(_source.x, _source.y, _dest.x, _dest.y); + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", src=").append(StringUtil.toString(_source)); + buf.append(", dest=").append(StringUtil.toString(_dest)); + } + + /** + * Computes the position of an entity along the path defined by the + * supplied start and end points assuming that it must finish the path + * in the specified duration (in millis) and has been traveling the + * path for the specified number of elapsed milliseconds. + */ + public static void computePosition (Point start, Point end, long elapsed, + long duration, Point pos) + { + float pct = (float)elapsed / duration; + int travx = (int)((end.x - start.x) * pct); + int travy = (int)((end.y - start.y) * pct); + pos.setLocation(start.x + travx, start.y + travy); + } + + /** Our source and destination points. */ + protected Point _source, _dest; + + /** A temporary point used when computing our position along the + * path. */ + protected Point _tpos = new Point(); +} diff --git a/src/java/com/threerings/media/util/LineSegmentPath.java b/src/java/com/threerings/media/util/LineSegmentPath.java new file mode 100644 index 00000000..0b512608 --- /dev/null +++ b/src/java/com/threerings/media/util/LineSegmentPath.java @@ -0,0 +1,388 @@ +// +// $Id: LineSegmentPath.java 3880 2006-02-22 18:29:05Z mjohnson $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Color; +import java.awt.Point; +import java.awt.Graphics2D; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.samskivert.util.StringUtil; + +import com.threerings.util.DirectionCodes; +import com.threerings.util.DirectionUtil; + +import com.threerings.media.Log; +import com.threerings.media.util.MathUtil; + +/** + * The line segment path is used to cause a pathable to follow a path that + * is made up of a sequence of line segments. There must be at least two + * nodes in any worthwhile path. The direction of the first node in the + * path is meaningless since the pathable begins at that node and will + * therefore never be heading towards it. + */ +public class LineSegmentPath + implements DirectionCodes, Path +{ + /** + * Constructs an empty line segment path. + */ + public LineSegmentPath () + { + } + + /** + * Constructs a line segment path that consists of a single segment + * connecting the point (x1, y1) with (x2, + * y2). The orientation for the first node is set arbitrarily + * and the second node is oriented based on the vector between the two + * nodes (in top-down coordinates). + */ + public LineSegmentPath (int x1, int y1, int x2, int y2) + { + addNode(x1, y1, NORTH); + Point p1 = new Point(x1, y1), p2 = new Point(x2, y2); + int dir = DirectionUtil.getDirection(p1, p2); + addNode(x2, y2, dir); + } + + /** + * Construct a line segment path between the two nodes with the + * specified direction. + */ + public LineSegmentPath (Point p1, Point p2, int dir) + { + addNode(p1.x, p1.y, NORTH); + addNode(p2.x, p2.y, dir); + } + + /** + * Constructs a line segment path with the specified list of points. + * An arbitrary direction will be assigned to the starting node. + */ + public LineSegmentPath (List points) + { + createPath(points); + } + + /** + * Returns the orientation the sprite will face at the end of the + * path. + */ + public int getFinalOrientation () + { + return (_nodes.size() == 0) ? NORTH : + ((PathNode)_nodes.get(_nodes.size()-1)).dir; + } + + /** + * Add a node to the path with the specified destination point and + * facing direction. + * + * @param x the x-position. + * @param y the y-position. + * @param dir the facing direction. + */ + public void addNode (int x, int y, int dir) + { + _nodes.add(new PathNode(x, y, dir)); + } + + /** + * Return the requested node index in the path, or null if no such + * index exists. + * + * @param idx the node index. + * + * @return the path node. + */ + public PathNode getNode (int idx) + { + return (PathNode)_nodes.get(idx); + } + + /** + * Return the number of nodes in the path. + */ + public int size () + { + return _nodes.size(); + } + + /** + * Sets the velocity of this pathable in pixels per millisecond. The + * velocity is measured as pixels traversed along the path that the + * pathable is traveling rather than in the x or y directions + * individually. Note that the pathable velocity should not be + * changed while a path is being traversed; doing so may result in the + * pathable position changing unexpectedly. + * + * @param velocity the pathable velocity in pixels per millisecond. + */ + public void setVelocity (float velocity) + { + _vel = velocity; + } + + /** + * Computes the velocity at which the pathable will need to travel + * along this path such that it will arrive at the destination in + * approximately the specified number of milliseconds. Efforts are + * taken to get the pathable there as close to the desired time as + * possible, but framerate variation may prevent it from arriving + * exactly on time. + */ + public void setDuration (long millis) + { + // if we have only zero or one nodes, we don't have enough + // information to compute our velocity + int ncount = _nodes.size(); + if (ncount < 2) { + Log.warning("Requested to set duration of bogus path " + + "[path=" + this + ", duration=" + millis + "]."); + return; + } + + // compute the total distance along our path + float distance = 0; + PathNode start = (PathNode)_nodes.get(0); + for (int ii = 1; ii < ncount; ii++) { + PathNode end = (PathNode)_nodes.get(ii); + distance += MathUtil.distance( + start.loc.x, start.loc.y, end.loc.x, end.loc.y); + start = end; + } + + // set the velocity accordingly + setVelocity(distance/millis); + } + + // documentation inherited + public void init (Pathable pable, long timestamp) + { + // give the pathable a chance to perform any starting antics + pable.pathBeginning(); + + // if we have only one node then let the pathable know that we're + // done straight away + if (size() < 2) { + // move the pathable to the location specified by the first + // node (assuming we have a first node) + if (size() == 1) { + PathNode node = (PathNode)_nodes.get(0); + pable.setLocation(node.loc.x, node.loc.y); + } + // and let the pathable know that we're done + pable.pathCompleted(timestamp); + return; + } + + // and an enumeration of the path nodes + _niter = _nodes.iterator(); + + // pretend like we were previously heading to our starting position + _dest = getNextNode(); + + // begin traversing the path + headToNextNode(pable, timestamp, timestamp); + } + + // documentation inherited + public boolean tick (Pathable pable, long timestamp) + { + // figure out how far along this segment we should be + long msecs = timestamp - _nodestamp; + float travpix = msecs * _vel; + float pctdone = travpix / _seglength; + + // if we've moved beyond the end of the path, we need to adjust + // the timestamp to determine how much time we used getting to the + // end of this node, then move to the next one + if (pctdone >= 1.0) { + long used = (long)(_seglength / _vel); + return headToNextNode(pable, _nodestamp + used, timestamp); + } + + // otherwise we position the pathable along the path + int ox = pable.getX(); + int oy = pable.getY(); + int nx = _src.loc.x + (int)((_dest.loc.x - _src.loc.x) * pctdone); + int ny = _src.loc.y + (int)((_dest.loc.y - _src.loc.y) * pctdone); + +// Log.info("Moving pathable [msecs=" + msecs + ", pctdone=" + pctdone + +// ", travpix=" + travpix + ", seglength=" + _seglength + +// ", dx=" + (nx-ox) + ", dy=" + (ny-oy) + "]."); + + // only update the pathable's location if it actually moved + if (ox != nx || oy != ny) { + pable.setLocation(nx, ny); + return true; + } + + return false; + } + + // documentation inherited + public void fastForward (long timeDelta) + { + _nodestamp += timeDelta; + } + + // documentation inherited from interface + public void wasRemoved (Pathable pable) + { + // nothing doing + } + + /** + * Place the pathable moving along the path at the end of the previous + * path node, face it appropriately for the next node, and start it on + * its way. Returns whether the pathable position moved. + */ + protected boolean headToNextNode ( + Pathable pable, long startstamp, long now) + { + if (_niter == null) { + throw new IllegalStateException( + "headToNextNode() called before init()"); + } + + // check to see if we've completed our path + if (!_niter.hasNext()) { + // move the pathable to the location of our last destination + pable.setLocation(_dest.loc.x, _dest.loc.y); + pable.pathCompleted(now); + return true; + } + + // our previous destination is now our source + _src = _dest; + + // pop the next node off the path + _dest = getNextNode(); + + // adjust the pathable's orientation + if (_dest.dir != NONE) { + pable.setOrientation(_dest.dir); + } + + // make a note of when we started traversing this node + _nodestamp = startstamp; + + // figure out the distance from source to destination + _seglength = MathUtil.distance( + _src.loc.x, _src.loc.y, _dest.loc.x, _dest.loc.y); + + // if we're already there (the segment length is zero), we skip to + // the next segment + if (_seglength == 0) { + return headToNextNode(pable, startstamp, now); + } + + // now update the pathable's position based on our progress thus far + return tick(pable, now); + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + gfx.setColor(Color.red); + Point prev = null; + int size = size(); + for (int ii = 0; ii < size; ii++) { + PathNode n = (PathNode)getNode(ii); + if (prev != null) { + gfx.drawLine(prev.x, prev.y, n.loc.x, n.loc.y); + } + prev = n.loc; + } + } + + // documentation inherited + public String toString () + { + return StringUtil.toString(_nodes.iterator()); + } + + /** + * Populate the path with the path nodes that lead the pathable from + * its starting position to the given destination coordinates + * following the given list of screen coordinates. + */ + protected void createPath (List points) + { + Point last = null; + int size = points.size(); + for (int ii = 0; ii < size; ii++) { + Point p = (Point)points.get(ii); + + int dir = (ii == 0) ? NORTH : DirectionUtil.getDirection(last, p); + addNode(p.x, p.y, dir); + last = p; + } + } + + /** + * Gets the next node in the path. + */ + protected PathNode getNextNode () + { + return (PathNode)_niter.next(); + } + + /** The nodes that make up the path. */ + protected ArrayList _nodes = new ArrayList(); + + /** We use this when moving along this path. */ + protected Iterator _niter; + + /** When moving, the pathable's source path node. */ + protected PathNode _src; + + /** When moving, the pathable's destination path node. */ + protected PathNode _dest; + + /** The time at which we started traversing the current node. */ + protected long _nodestamp; + + /** The length in pixels of the current path segment. */ + protected float _seglength; + + /** The path velocity in pixels per millisecond. */ + protected float _vel = DEFAULT_VELOCITY; + + /** When moving, the pathable position including fractional pixels. */ + protected float _movex, _movey; + + /** When moving, the distance to move on each axis per tick. */ + protected float _incx, _incy; + + /** The distance to move on the straight path line per tick. */ + protected float _fracx, _fracy; + + /** Default pathable velocity. */ + protected static final float DEFAULT_VELOCITY = 200f/1000f; +} diff --git a/src/java/com/threerings/media/util/LinearTimeFunction.java b/src/java/com/threerings/media/util/LinearTimeFunction.java new file mode 100644 index 00000000..8cbc1976 --- /dev/null +++ b/src/java/com/threerings/media/util/LinearTimeFunction.java @@ -0,0 +1,22 @@ +// +// $Id: LinearTimeFunction.java 4191 2006-06-13 22:42:20Z ray $ + +package com.threerings.media.util; + +/** + * Varies a value linearly with time. + */ +public class LinearTimeFunction extends TimeFunction +{ + public LinearTimeFunction (int start, int end, int duration) + { + super(start, end, duration); + } + + // documentation inherited + protected int computeValue (int dt) + { + int dv = (_end - _start); + return (dt * dv / _duration) + _start; + } +} diff --git a/src/java/com/threerings/media/util/MathUtil.java b/src/java/com/threerings/media/util/MathUtil.java new file mode 100644 index 00000000..68ededf7 --- /dev/null +++ b/src/java/com/threerings/media/util/MathUtil.java @@ -0,0 +1,162 @@ +// +// $Id: MathUtil.java 3580 2005-05-27 23:04:42Z tedv $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Point; + +/** + * Provides miscellaneous useful utility routines for mathematical + * calculations. + */ +public class MathUtil +{ + /** + * Bounds the supplied value within the specified range. + * + * @return low if value < low, high if value > high and value + * otherwise. + */ + public static int bound (int low, int value, int high) + { + return Math.min(high, Math.max(low, value)); + } + + /** + * Return the squared distance between the given points. + */ + public static int distanceSq (int x0, int y0, int x1, int y1) + { + return ((x1 - x0) * (x1 - x0)) + ((y1 - y0) * (y1 - y0)); + } + + /** + * Return the distance between the given points. + */ + public static float distance (int x0, int y0, int x1, int y1) + { + return (float)Math.sqrt(((x1 - x0) * (x1 - x0)) + + ((y1 - y0) * (y1 - y0))); + } + + /** + * Return the distance between the given points. + */ + public static float distance (Point source, Point dest) + { + return MathUtil.distance(source.x, source.y, dest.x, dest.y); + } + + /** + * Return a string representation of the given line. + */ + public static String lineToString (int x0, int y0, int x1, int y1) + { + return "(" + x0 + ", " + y0 + ") -> (" + x1 + ", " + y1 + ")"; + } + + /** + * Return a string representation of the given line. + */ + public static String lineToString (Point p1, Point p2) + { + return lineToString(p1.x, p1.y, p2.x, p2.y); + } + + /** + * Returns the approximate circumference of the ellipse defined by the + * specified minor and major axes. The formula used (due to Ramanujan, + * via a paper of his entitled "Modular Equations and Approximations + * to Pi"), is Pi(3a + 3b - sqrt[(a+3b)(b+3a)]). + */ + public static double ellipseCircum (double a, double b) + { + return Math.PI * (3*a + 3*b - Math.sqrt((a + 3*b) * (b + 3*a))); + } + + /** + * Returns positive 1 if the sign of the argument is positive, or -1 + * if the sign of the argument is negative. + */ + public static int sign (int value) + { + return (value < 0) ? -1 : 1; + } + + /** + * Computes the floored division dividend/divisor which + * is useful when dividing potentially negative numbers into bins. For + * positive numbers, it is the same as normal division, for negative + * numbers it returns (dividend - divisor + 1) / divisor. + * + *

For example, the following numbers floorDiv 10 are: + *

+     * -15 -10 -8 -2 0 2 8 10 15
+     *  -2  -1 -1 -1 0 0 0  1  1
+     * 
+ */ + public static int floorDiv (int dividend, int divisor) + { + return ((dividend >= 0) ? dividend : (dividend - divisor + 1))/divisor; + } + + /** + * Computes the standard deviation of the supplied values. + * + * @return an array of three values: the mean, variance and standard + * deviation, in that order. + */ + public static float[] stddev (int[] values, int start, int length) + { + // first we need the mean + float mean = 0f; + for (int ii = start, end = start + length; ii < end; ii++) { + mean += values[ii]; + } + mean /= length; + + // next we compute the variance + float variance = 0f; + for (int ii = start, end = start + length; ii < end; ii++) { + float value = values[ii] - mean; + variance += value * value; + } + variance /= (length - 1); + + // the standard deviation is the square root of the variance + return new float[] { mean, variance, (float)Math.sqrt(variance) }; + } + + /** + * Computes (N choose K), the number of ways to select K different + * elements from a set of size N. + */ + public static int choose (int n, int k) + { + // Base case: One way to select or not select the whole set + if (k <= 0 || k >= n) { + return 1; + } + + // Recurse using pascal's triangle + return (choose(n-1, k-1) + choose(n-1, k)); + } +} diff --git a/src/java/com/threerings/media/util/ModeUtil.java b/src/java/com/threerings/media/util/ModeUtil.java new file mode 100644 index 00000000..d4ac9193 --- /dev/null +++ b/src/java/com/threerings/media/util/ModeUtil.java @@ -0,0 +1,99 @@ +// +// $Id: ModeUtil.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.DisplayMode; +import java.awt.GraphicsDevice; + +import java.util.Comparator; +import java.util.TreeSet; + +/** + * Display mode related utilities. + */ +public class ModeUtil +{ + /** + * Gets a display mode that matches the specified parameters. The + * screen resolution must match the specified resolution exactly, the + * specified desired depth will be used if it is available, and if + * not, the highest depth greater than or equal to the specified + * minimum depth is used. The highest refresh rate available for the + * desired mode is also used. + */ + public static DisplayMode getDisplayMode ( + GraphicsDevice gd, int width, int height, + int desiredDepth, int minimumDepth) + { + DisplayMode[] modes = gd.getDisplayModes(); + final int ddepth = desiredDepth; + + // we sort modes in order of desirability + Comparator mcomp = new Comparator () { + public int compare (Object o1, Object o2) { + DisplayMode m1 = (DisplayMode)o1; + DisplayMode m2 = (DisplayMode)o2; + int bd1 = m1.getBitDepth(), bd2 = m2.getBitDepth(); + int rr1 = m1.getRefreshRate(), rr2 = m2.getRefreshRate(); + + // prefer the desired depth + if (bd1 == ddepth && bd2 != ddepth) { + return -1; + } else if (bd2 == ddepth && bd1 != ddepth) { + return 1; + } + + // otherwise prefer higher depths + if (bd1 != bd2) { + return bd2 - bd1; + } + + // for same bitrates, prefer higher refresh rates + return rr2 - rr1; + } + }; + + // but we only add modes that meet our minimum requirements + TreeSet mset = new TreeSet(mcomp); + for (int i = 0; i < modes.length; i++) { + if (modes[i].getWidth() == width && + modes[i].getHeight() == height && + modes[i].getBitDepth() >= minimumDepth && + modes[i].getRefreshRate() <= 75) { + mset.add(modes[i]); + } + } + + return (mset.size() > 0) ? (DisplayMode)mset.first() : null; + } + + /** + * Returns a string representation of the supplied display mode. + */ + public static String toString (DisplayMode mode) + { + return "[width=" + mode.getWidth() + + ", height=" + mode.getHeight() + + ", depth=" + mode.getBitDepth() + + ", refresh=" + mode.getRefreshRate() + "]"; + } +} diff --git a/src/java/com/threerings/media/util/MultiFrameImage.java b/src/java/com/threerings/media/util/MultiFrameImage.java new file mode 100644 index 00000000..8a200d86 --- /dev/null +++ b/src/java/com/threerings/media/util/MultiFrameImage.java @@ -0,0 +1,58 @@ +// +// $Id: MultiFrameImage.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Graphics2D; + +/** + * The multi-frame image interface provides encapsulated access to a set + * of images that are used to create a multi-frame animation. + */ +public interface MultiFrameImage +{ + /** + * Returns the number of frames in this multi-frame image. + */ + public int getFrameCount (); + + /** + * Returns the width of the specified frame image. + */ + public int getWidth (int index); + + /** + * Returns the height of the specified frame image. + */ + public int getHeight (int index); + + /** + * Renders the specified frame into the specified graphics object at + * the specified coordinates. + */ + public void paintFrame (Graphics2D g, int index, int x, int y); + + /** + * Returns true if the specified frame contains a non-transparent + * pixel at the specified coordinates. + */ + public boolean hitTest (int index, int x, int y); +} diff --git a/src/java/com/threerings/media/util/MultiFrameImageImpl.java b/src/java/com/threerings/media/util/MultiFrameImageImpl.java new file mode 100644 index 00000000..8958d4b4 --- /dev/null +++ b/src/java/com/threerings/media/util/MultiFrameImageImpl.java @@ -0,0 +1,75 @@ +// +// $Id: MultiFrameImageImpl.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Graphics2D; + +import com.threerings.media.image.Mirage; + +/** + * A basic implementation of the {@link MultiFrameImage} interface + * intended to facilitate the creation of MFIs whose display frames + * consist of multiple image objects. + */ +public class MultiFrameImageImpl implements MultiFrameImage +{ + /** + * Constructs a multiple frame image object. + */ + public MultiFrameImageImpl (Mirage[] mirages) + { + _mirages = mirages; + } + + // documentation inherited + public int getFrameCount () + { + return _mirages.length; + } + + // documentation inherited from interface + public int getWidth (int index) + { + return _mirages[index].getWidth(); + } + + // documentation inherited from interface + public int getHeight (int index) + { + return _mirages[index].getHeight(); + } + + // documentation inherited from interface + public void paintFrame (Graphics2D g, int index, int x, int y) + { + _mirages[index].paint(g, x, y); + } + + // documentation inherited from interface + public boolean hitTest (int index, int x, int y) + { + return _mirages[index].hitTest(x, y); + } + + /** The frame images. */ + protected Mirage[] _mirages; +} diff --git a/src/java/com/threerings/media/util/Path.java b/src/java/com/threerings/media/util/Path.java new file mode 100644 index 00000000..0d7c2fa6 --- /dev/null +++ b/src/java/com/threerings/media/util/Path.java @@ -0,0 +1,82 @@ +// +// $Id: Path.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Graphics2D; + +/** + * A path is used to cause a {@link Pathable} to follow a particular path + * along the screen. The {@link Pathable} is responsible for calling + * {@link #tick} on the path with reasonable frequency (generally as a + * part of the frame tick. The path is responsible for updating the + * position of the {@link Pathable} based on the time that has elapsed + * since the {@link Pathable} started down the path. + * + *

The path should call the appropriate callbacks on the {@link + * Pathable} when appropriate (e.g. {@link Pathable#pathBeginning}, {@link + * Pathable#pathCompleted}). + */ +public interface Path +{ + /** + * Called once to let the path prepare itself for the process of + * animating the supplied pathable. Path users should also call {@link + * #tick} after {@link #init} with the same initialization timestamp. + */ + public void init (Pathable pable, long tickStamp); + + /** + * Called to request that this path update the position of the + * specified pathable based on the supplied timestamp information. A + * path should record its initial timestamp and determine the progress + * of the pathable along the path based on the time elapsed since the + * pathable began down the path. + * + * @param pable the pathable whose position should be updated. + * @param tickStamp the timestamp associated with this frame. + * + * @return true if the pathable's position was updated, false if the + * path determined that the pathable should not move at this time. + */ + public boolean tick (Pathable pable, long tickStamp); + + /** + * This is called if the pathable is paused for some length of time + * and then unpaused. Paths should adjust any time stamps they are + * maintaining internally by the delta so that time maintains the + * illusion of flowing smoothly forward. + */ + public void fastForward (long timeDelta); + + /** + * Paint this path on the screen (used for debugging purposes only). + */ + public void paint (Graphics2D gfx); + + /** + * When a path is removed from a pathable, whether that is because the + * path was completed or because it was replaced by another path, this + * method will be called to let the path know that it is no longer + * associated with this pathable. + */ + public void wasRemoved (Pathable pable); +} diff --git a/src/java/com/threerings/media/util/PathNode.java b/src/java/com/threerings/media/util/PathNode.java new file mode 100644 index 00000000..cef316da --- /dev/null +++ b/src/java/com/threerings/media/util/PathNode.java @@ -0,0 +1,72 @@ +// +// $Id: PathNode.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Point; + +/** + * A path node is a single destination point in a {@link Path}. + */ +public class PathNode +{ + /** The node coordinates in screen pixels. */ + public Point loc; + + /** The direction to face while heading toward the node. */ + public int dir; + + /** + * Construct a path node object. + * + * @param x the node x-position. + * @param y the node y-position. + * @param dir the facing direction. + */ + public PathNode (int x, int y, int dir) + { + loc = new Point(x, y); + this.dir = dir; + } + + /** + * Return a string representation of this path node. + */ + public String toString () + { + StringBuilder buf = new StringBuilder(); + buf.append("["); + toString(buf); + return buf.append("]").toString(); + } + + /** + * This should be overridden by derived classes (which should be sure + * to call super.toString()) to append the derived class + * specific path node information to the string buffer. + */ + public void toString (StringBuilder buf) + { + buf.append("x=").append(loc.x); + buf.append(", y=").append(loc.y); + buf.append(", dir=").append(dir); + } +} diff --git a/src/java/com/threerings/media/util/PathSequence.java b/src/java/com/threerings/media/util/PathSequence.java new file mode 100644 index 00000000..d8d981d9 --- /dev/null +++ b/src/java/com/threerings/media/util/PathSequence.java @@ -0,0 +1,143 @@ +// +// $Id: PathSequence.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Graphics2D; + +import java.util.ArrayList; +import java.util.List; + +import com.threerings.media.Log; + +/** + * Used to create a path that is a sequence of several other paths. + */ +public class PathSequence + implements Path +{ + /** + * Conveniently construct a path sequence with the two specified + * paths. + */ + public PathSequence (Path first, Path second) + { + this(new ArrayList()); + _paths.add(first); + _paths.add(second); + } + + /** + * Construct a path sequence with the list of paths. + * Note: Paths may be added to the end of the list while + * the pathable is still traversing earlier paths! + */ + public PathSequence (List paths) + { + _paths = paths; + } + + // documentation inherited from interface Path + public void init (Pathable pable, long tickStamp) + { + _pable = pable; + _pableRep = new DelegatingPathable(_pable) { + public void pathCompleted (long timeStamp) { + long initStamp; + // if we just finished a timed path, we can figure out how + // long ago it really finished and init the next path at + // that time in the past. + if (_curPath instanceof TimedPath) { + initStamp = _lastInit + ((TimedPath) _curPath)._duration; + } else { + // we don't know + initStamp = timeStamp; + } + initNextPath(initStamp, timeStamp); + } + }; + initNextPath(tickStamp, tickStamp); + } + + // documentation inherited from interface Path + public boolean tick (Pathable pable, long tickStamp) + { + if (pable != _pable) { + Log.warning("PathSequence ticked with different path than " + + "it was inited with."); + } + return _curPath.tick(_pableRep, tickStamp); + } + + // documentation inherited from interface Path + public void fastForward (long timeDelta) + { + _lastInit += timeDelta; + _curPath.fastForward(timeDelta); + } + + // documentation inherited from interface Path + public void paint (Graphics2D gfx) + { + // for now.. + _curPath.paint(gfx); + } + + // documentation inherited from interface Path + public void wasRemoved (Pathable pable) + { + if (_curPath != null) { + _curPath.wasRemoved(_pableRep); + } + } + + /** + * Initialize and start the next path in the sequence. + */ + protected void initNextPath (long initStamp, long tickStamp) + { + if (_paths.size() == 0) { + _pable.pathCompleted(tickStamp); + + } else { + _curPath = (Path) _paths.remove(0); + _lastInit = initStamp; + + _curPath.init(_pableRep, initStamp); + _curPath.tick(_pableRep, tickStamp); + } + } + + /** The list of paths. */ + protected List _paths; + + /** The timestamp at which we last inited a path. */ + protected long _lastInit; + + /** The current path we're pathing. */ + protected Path _curPath; + + /** The pathable we're duping bigtime. */ + protected Pathable _pable; + + /** A fake pathable that we pass to the subpaths. */ + protected Pathable _pableRep; +} diff --git a/src/java/com/threerings/media/util/Pathable.java b/src/java/com/threerings/media/util/Pathable.java new file mode 100644 index 00000000..be571e11 --- /dev/null +++ b/src/java/com/threerings/media/util/Pathable.java @@ -0,0 +1,77 @@ +// +// $Id: Pathable.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Rectangle; + +import com.threerings.util.DirectionCodes; + +/** + * Used in conjunction with a {@link Path}. + */ +public interface Pathable +{ + /** + * Returns the pathable's current x coordinate. + */ + public int getX (); + + /** + * Returns the pathable's current y coordinate. + */ + public int getY (); + + /** + * Returns the rectangle that bounds the pathable. + */ + public Rectangle getBounds (); + + /** + * Updates the pathable's current coordinates. + */ + public void setLocation (int x, int y); + + /** + * Will be called by a path when it moves the pathable in the + * specified direction. Pathables that wish to face in the direction + * they are moving can take advantage of this callback. + * + * @see DirectionCodes + */ + public void setOrientation (int orient); + + /** + * Should return the orientation of the pathable, or {@link + * DirectionCodes#NONE} if the pathable does not support orientation. + */ + public int getOrientation (); + + /** + * Called by a path when this pathable is made to start along a path. + */ + public void pathBeginning (); + + /** + * Called by a path when this pathable finishes moving along its path. + */ + public void pathCompleted (long timestamp); +} diff --git a/src/java/com/threerings/media/util/PerformanceMonitor.java b/src/java/com/threerings/media/util/PerformanceMonitor.java new file mode 100644 index 00000000..306e5cd6 --- /dev/null +++ b/src/java/com/threerings/media/util/PerformanceMonitor.java @@ -0,0 +1,215 @@ +// +// $Id: PerformanceMonitor.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.util.HashMap; + +import com.threerings.media.Log; +import com.threerings.media.timer.MediaTimer; +import com.threerings.media.timer.SystemMediaTimer; + +/** + * Provides a simple mechanism for monitoring the number of times an + * action takes place within a certain time period. + * + *

The action being tracked should be registered with a suitable name + * via {@link #register}, and {@link #tick} should be called each time the + * action is performed. + * + *

Whenever {@link #tick} is called and the checkpoint time interval + * has elapsed since the last checkpoint (if any), the observer will be + * notified to that effect by a call to {@link + * PerformanceObserver#checkpoint}. + * + *

Note that this is not intended to be used as an + * industrial-strength profiling or performance monitoring tool. The + * checkpoint time interval granularity is in milliseconds, not + * microseconds, and the observer's checkpoint() method will + * never be called until/unless a subsequent call to {@link #tick} is made + * after the requested number of milliseconds have passed since the last + * checkpoint. + */ +public class PerformanceMonitor +{ + /** + * Register a new action with an observer, the action name, and + * the milliseconds to wait between checkpointing the action's + * performance. + * + * @param obs the action observer. + * @param name the action name. + * @param delta the milliseconds between checkpoints. + */ + public static void register (PerformanceObserver obs, String name, + long delta) + { + // get the observer's action hashtable + HashMap actions = (HashMap)_observers.get(obs); + if (actions == null) { + // create it if it didn't exist + _observers.put(obs, actions = new HashMap()); + } + + // add the action to the set we're tracking + actions.put(name, new PerformanceAction(obs, name, delta)); + } + + /** + * Un-register the named action associated with the given observer. + * + * @param obs the action observer. + * @param name the action name. + */ + public static void unregister (PerformanceObserver obs, String name) + { + // get the observer's action hashtable + HashMap actions = (HashMap)_observers.get(obs); + if (actions == null) { + Log.warning("Attempt to unregister by unknown observer " + + "[observer=" + obs + ", name=" + name + "]."); + return; + } + + // attempt to remove the specified action + PerformanceAction action = (PerformanceAction)actions.remove(name); + if (action == null) { + Log.warning("Attempt to unregister unknown action " + + "[observer=" + obs + ", name=" + name + "]."); + return; + } + + // if the observer has no actions left, remove the observer's action + // hash in its entirety + if (actions.size() == 0) { + _observers.remove(obs); + } + } + + /** + * Tick the named action associated with the given observer. + * + * @param obs the action observer. + * @param name the action name. + */ + public static void tick (PerformanceObserver obs, String name) + { + // get the observer's action hashtable + HashMap actions = (HashMap)_observers.get(obs); + if (actions == null) { + Log.warning("Attempt to tick by unknown observer " + + "[observer=" + obs + ", name=" + name + "]."); + return; + } + + // get the specified action + PerformanceAction action = (PerformanceAction)actions.get(name); + if (action == null) { + Log.warning("Attempt to tick unknown value " + + "[observer=" + obs + ", name=" + name + "]."); + return; + } + + // tick the action + action.tick(); + } + + /** + * Used to configure the performance monitor with a particular {@link + * MediaTimer} implementation. By default it uses a pure-Java + * implementation which isn't extremely accurate across platforms. + */ + public static void setMediaTimer (MediaTimer timer) + { + _timer = timer; + } + + /** Used by the performance actions. */ + protected synchronized static long getTimeStamp () + { + return _timer.getElapsedMillis(); + } + + /** The observers monitoring some set of actions. */ + protected static HashMap _observers = new HashMap(); + + /** Used to obtain high resolution time stamps. */ + protected static MediaTimer _timer = new SystemMediaTimer(); +} + +/** + * This class represents the individual actions being tracked by the + * PerformanceMonitor class. + */ +class PerformanceAction +{ + public PerformanceAction (PerformanceObserver obs, String name, long delta) + { + _obs = obs; + _name = name; + _delta = delta; + _lastDelta = PerformanceMonitor.getTimeStamp(); + } + + public void tick () + { + _numTicks++; + + long now = PerformanceMonitor.getTimeStamp(); + long passed = now - _lastDelta; + if (passed >= _delta) { + // update the last checkpoint time + _lastDelta = now + (passed - _delta); + + // notify our observer of the checkpoint + _obs.checkpoint(_name, _numTicks); + + // reset the tick count + _numTicks = 0; + } + } + + public String toString () + { + StringBuilder buf = new StringBuilder(); + buf.append("[obs=").append(_obs); + buf.append(", name=").append(_name); + buf.append(", delta=").append(_delta); + buf.append(", lastDelta=").append(_lastDelta); + buf.append(", numTicks=").append(_numTicks); + return buf.append("]").toString(); + } + + /** The performance observer. */ + protected PerformanceObserver _obs; + + /** The action name. */ + protected String _name; + + /** The number of milliseconds between each checkpoint. */ + protected long _delta; + + /** The time the last time a checkpoint was made. */ + protected long _lastDelta; + + /** The number of ticks since the last checkpoint. */ + protected int _numTicks; +} diff --git a/src/java/com/threerings/media/util/PerformanceObserver.java b/src/java/com/threerings/media/util/PerformanceObserver.java new file mode 100644 index 00000000..8b22f6b5 --- /dev/null +++ b/src/java/com/threerings/media/util/PerformanceObserver.java @@ -0,0 +1,39 @@ +// +// $Id: PerformanceObserver.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +/** + * This interface should be implemented by classes that wish to register + * actions to be monitored by the {@link PerformanceMonitor} class. + */ +public interface PerformanceObserver +{ + /** + * This method is called by the {@link PerformanceMonitor} class + * whenever an action's requested time interval between checkpoints + * has expired. + * + * @param name the action name. + * @param ticks the ticks since the last checkpoint. + */ + public void checkpoint (String name, int ticks); +} diff --git a/src/java/com/threerings/media/util/SingleFrameImageImpl.java b/src/java/com/threerings/media/util/SingleFrameImageImpl.java new file mode 100644 index 00000000..69f59454 --- /dev/null +++ b/src/java/com/threerings/media/util/SingleFrameImageImpl.java @@ -0,0 +1,75 @@ +// +// $Id: SingleFrameImageImpl.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Graphics2D; + +import com.threerings.media.image.Mirage; + +/** + * The single frame image class is a basic implementation of the {@link + * MultiFrameImage} interface intended to facilitate the creation of MFIs + * whose display frames consist of only a single image. + */ +public class SingleFrameImageImpl implements MultiFrameImage +{ + /** + * Constructs a single frame image object. + */ + public SingleFrameImageImpl (Mirage mirage) + { + _mirage = mirage; + } + + // documentation inherited + public int getFrameCount () + { + return 1; + } + + // documentation inherited from interface + public int getWidth (int index) + { + return _mirage.getWidth(); + } + + // documentation inherited from interface + public int getHeight (int index) + { + return _mirage.getHeight(); + } + + // documentation inherited from interface + public void paintFrame (Graphics2D g, int index, int x, int y) + { + _mirage.paint(g, x, y); + } + + // documentation inherited from interface + public boolean hitTest (int index, int x, int y) + { + return _mirage.hitTest(x, y); + } + + /** The frame image. */ + protected Mirage _mirage; +} diff --git a/src/java/com/threerings/media/util/SingleTileImageImpl.java b/src/java/com/threerings/media/util/SingleTileImageImpl.java new file mode 100644 index 00000000..fa9a1510 --- /dev/null +++ b/src/java/com/threerings/media/util/SingleTileImageImpl.java @@ -0,0 +1,75 @@ +// +// $Id: SingleTileImageImpl.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Graphics2D; + +import com.threerings.media.tile.Tile; + +/** + * The single frame image class is a basic implementation of the {@link + * MultiFrameImage} interface intended to facilitate the creation of MFIs + * whose display frames consist of only a single tile image. + */ +public class SingleTileImageImpl implements MultiFrameImage +{ + /** + * Constructs a single frame image object. + */ + public SingleTileImageImpl (Tile tile) + { + _tile = tile; + } + + // documentation inherited + public int getFrameCount () + { + return 1; + } + + // documentation inherited from interface + public int getWidth (int index) + { + return _tile.getWidth(); + } + + // documentation inherited from interface + public int getHeight (int index) + { + return _tile.getHeight(); + } + + // documentation inherited from interface + public void paintFrame (Graphics2D g, int index, int x, int y) + { + _tile.paint(g, x, y); + } + + // documentation inherited from interface + public boolean hitTest (int index, int x, int y) + { + return _tile.hitTest(x, y); + } + + /** The frame image. */ + protected Tile _tile; +} diff --git a/src/java/com/threerings/media/util/SmoothBobblePath.java b/src/java/com/threerings/media/util/SmoothBobblePath.java new file mode 100644 index 00000000..2ba20b56 --- /dev/null +++ b/src/java/com/threerings/media/util/SmoothBobblePath.java @@ -0,0 +1,86 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import com.samskivert.util.RandomUtil; + +/** + * Bobble a Pathable smoothly. + */ +public class SmoothBobblePath extends BobblePath +{ + public SmoothBobblePath (int dx, int dy, long duration) + { + this(dx, dy, duration, 1L); + } + + public SmoothBobblePath (int dx, int dy, long duration, long updateFreq) + { + super(dx, dy, duration, updateFreq); + } + + // documentation inherited + public void init (Pathable pable, long tickstamp) + { + super.init(pable, tickstamp); + + _newx = _sx; + _newy = _sy; + _oldx = _sx; + _oldy = _sy; + } + + // documentation inherited + public boolean tick (Pathable pable, long tickStamp) + { + // see if we need to stop + if (_stopTime <= tickStamp) { + boolean updated = updatePositionTo(pable, _sy, _sy); + pable.pathCompleted(tickStamp); + return updated; + } + + // see if it's time to update the position + if (_nextMove < tickStamp) { + _oldx = _newx; + _oldy = _newy; + do { + _newx = _sx + RandomUtil.getInt(_dx * 2 + 1) - _dx; + _newy = _sy + RandomUtil.getInt(_dy * 2 + 1) - _dy; + } while (_newx == _oldx && _newy == _oldy); + + _nextMove = tickStamp + _updateFreq; + } + + float movePerc = (float)(_nextMove - tickStamp) / (float)_updateFreq; + int x = _oldx + (int)((_newx - _oldx) * movePerc); + int y = _oldy + (int)((_newy - _oldy) * movePerc); + // update the position + return updatePositionTo(pable, x, y); + } + + /** The previous position. */ + protected int _oldx, _oldy; + + /** The next position. */ + protected int _newx, _newy; +} diff --git a/src/java/com/threerings/media/util/TiledArea.java b/src/java/com/threerings/media/util/TiledArea.java new file mode 100644 index 00000000..e53af78c --- /dev/null +++ b/src/java/com/threerings/media/util/TiledArea.java @@ -0,0 +1,61 @@ +// +// $Id: TiledArea.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import javax.swing.JComponent; + +/** + * A component that can be inserted into a user interface to fill a + * particular area using a {@link BackgroundTiler}. + */ +public class TiledArea extends JComponent +{ + public TiledArea (BufferedImage imgsrc) + { + this(new BackgroundTiler(imgsrc)); + } + + public TiledArea (BackgroundTiler tiler) + { + _tiler = tiler; + setOpaque(true); + } + + // documentation inherited + public void paintComponent (Graphics g) + { + super.paintComponent(g); + _tiler.paint(g, 0, 0, getWidth(), getHeight()); + } + + // documentation inherited + public Dimension getPreferredSize () + { + return new Dimension(_tiler.getNaturalWidth(), + _tiler.getNaturalHeight()); + } + + protected BackgroundTiler _tiler; +} diff --git a/src/java/com/threerings/media/util/TimeFunction.java b/src/java/com/threerings/media/util/TimeFunction.java new file mode 100644 index 00000000..3e4f75d7 --- /dev/null +++ b/src/java/com/threerings/media/util/TimeFunction.java @@ -0,0 +1,83 @@ +// +// $Id: TimeFunction.java 4191 2006-06-13 22:42:20Z ray $ + +package com.threerings.media.util; + +/** + * Used to vary a value over time where time is provided at discrete + * increments (on the frame tick) and the value is computed appropriately. + */ +public abstract class TimeFunction +{ + /** + * Every time function varies a value from some starting value to some + * ending value over some duration. The way in which it varies + * (linearly, for example) is up to the derived class. + * + *

Note: it is assumed that we will operate with + * relatively short durations such that integer arithmetic may be used + * rather than long arithmetic. + */ + public TimeFunction (int start, int end, int duration) + { + _start = start; + _end = end; + _duration = duration; + } + + /** + * Configures this function with a starting time. This method need not + * be called, and instead the first vall to {@link #getValue} will be + * used to obtain a starting time stamp. + */ + public void init (long tickStamp) + { + _startStamp = tickStamp; + } + + /** + * Called to fast forward our time stamps if we are ever paused and + * need to resume where we left off. + */ + public void fastForward (long timeDelta) + { + _startStamp += timeDelta; + } + + /** + * Returns the current value given the supplied time stamp. The value + * will be bounded to the originally supplied starting and ending + * values at times 0 (and below) and {@link #_duration} (and above) + * respectively. + */ + public int getValue (long tickStamp) + { + if (_startStamp == 0L) { + _startStamp = tickStamp; + } + + int dt = (int)(tickStamp - _startStamp); + if (dt <= 0) { + return _start; + } else if (dt >= _duration) { + return _end; + } else { + return computeValue(dt); + } + } + + /** + * This must be implemented by our derived class to compute our value + * given the specified elapsed time (in millis). + */ + protected abstract int computeValue (int dt); + + /** Our starting and ending values. */ + protected int _start, _end; + + /** The number of milliseconds over which we vary our value. */ + protected int _duration; + + /** The timestamp at which we began varying our value. */ + protected long _startStamp; +} diff --git a/src/java/com/threerings/media/util/TimedPath.java b/src/java/com/threerings/media/util/TimedPath.java new file mode 100644 index 00000000..944b0dd3 --- /dev/null +++ b/src/java/com/threerings/media/util/TimedPath.java @@ -0,0 +1,98 @@ +// +// $Id: TimedPath.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import com.threerings.media.Log; + +/** + * A base class for path implementations that endeavor to move their + * pathables along a path in a specified number of milliseconds. + */ +public abstract class TimedPath implements Path +{ + /** + * Configures the timed path with the duration in which it must + * traverse its path. + */ + public TimedPath (long duration) + { + // sanity check some things + if (duration <= 0) { + Log.warning("Requested path with illegal duration (<=0) " + + "[duration=" + duration + "]"); + Thread.dumpStack(); + duration = 1; // assume something short but non-zero + } + + _duration = duration; + } + + // documentation inherited + public void init (Pathable pable, long timestamp) + { + // give the pable a chance to perform any starting antics + pable.pathBeginning(); + + // make a note of when we started + _startStamp = timestamp; + + // we'll be ticked immediately following init() which will update + // our position to the start of our path + } + + // documentation inherited + public void fastForward (long timeDelta) + { + _startStamp += timeDelta; + } + + // documentation inherited from interface + public void wasRemoved (Pathable pable) + { + // nothing doing + } + + /** + * Generates a string representation of this instance. + */ + public String toString () + { + StringBuilder buf = new StringBuilder("["); + toString(buf); + return buf.append("]").toString(); + } + + /** + * An extensible method for generating a string representation of this + * instance. + */ + protected void toString (StringBuilder buf) + { + buf.append("duration=").append(_duration).append("ms"); + } + + /** The duration that we're to spend following the path. */ + protected long _duration; + + /** The time at which we started along the path. */ + protected long _startStamp; +} diff --git a/src/java/com/threerings/media/util/TrailingAverage.java b/src/java/com/threerings/media/util/TrailingAverage.java new file mode 100644 index 00000000..7e0d440b --- /dev/null +++ b/src/java/com/threerings/media/util/TrailingAverage.java @@ -0,0 +1,81 @@ +// +// $Id: TrailingAverage.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +/** + * Used to compute a trailing average of a value. + */ +public class TrailingAverage +{ + /** + * Creates a trailing average instance with the default number of + * values used to compute the average (10). + */ + public TrailingAverage () + { + this(10); + } + + /** + * Creates a trailing average instance with the specified number of + * values used to compute the average. + */ + public TrailingAverage (int history) + { + _history = new int[history]; + } + + /** + * Records a new value. + */ + public void record (int value) + { + _history[_index++%_history.length] = value; + } + + /** + * Returns the current averaged value. + */ + public int value () + { + int end = Math.min(_history.length, _index); + int value = 0; + for (int ii = 0; ii < end; ii++) { + value += _history[ii]; + } + return (end > 0) ? (value/end) : 0; + } + + /** + * Returns the current trailing average value as a string. + */ + public String toString () + { + return Integer.toString(value()); + } + + /** The history of values. */ + protected int[] _history; + + /** The index where we will next record a value. */ + protected int _index; +} diff --git a/src/java/com/threerings/miso/Log.java b/src/java/com/threerings/miso/Log.java new file mode 100644 index 00000000..c0bcce5f --- /dev/null +++ b/src/java/com/threerings/miso/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso; + +/** + * A placeholder class that contains a reference to the log object used by + * the miso package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("miso"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/miso/MisoConfig.java b/src/java/com/threerings/miso/MisoConfig.java new file mode 100644 index 00000000..f724f11f --- /dev/null +++ b/src/java/com/threerings/miso/MisoConfig.java @@ -0,0 +1,61 @@ +// +// $Id: MisoConfig.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso; + +import com.samskivert.util.Config; + +import com.threerings.miso.util.MisoSceneMetrics; + +/** + * Provides access to the Miso configuration. + */ +public class MisoConfig +{ + /** Provides access to configuration data for this package. */ + public static Config config = new Config("rsrc/config/miso/miso"); + + /** + * Creates scene metrics with information obtained from the deployed + * config file. + */ + public static MisoSceneMetrics getSceneMetrics () + { + return new MisoSceneMetrics( + config.getValue(TILE_WIDTH_KEY, DEF_TILE_WIDTH), + config.getValue(TILE_HEIGHT_KEY, DEF_TILE_HEIGHT), + config.getValue(FINE_GRAN_KEY, DEF_FINE_GRAN)); + } + + /** The config key for tile width in pixels. */ + protected static final String TILE_WIDTH_KEY = "tile_width"; + + /** The config key for tile height in pixels. */ + protected static final String TILE_HEIGHT_KEY = "tile_height"; + + /** The config key for tile fine coordinate granularity. */ + protected static final String FINE_GRAN_KEY = "fine_granularity"; + + /** Default scene view parameters. */ + protected static final int DEF_TILE_WIDTH = 64; + protected static final int DEF_TILE_HEIGHT = 48; + protected static final int DEF_FINE_GRAN = 4; +} diff --git a/src/java/com/threerings/miso/MisoPrefs.java b/src/java/com/threerings/miso/MisoPrefs.java new file mode 100644 index 00000000..0ba85f64 --- /dev/null +++ b/src/java/com/threerings/miso/MisoPrefs.java @@ -0,0 +1,35 @@ +// +// $Id: MisoPrefs.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso; + +import com.samskivert.util.Config; + +/** + * Provides access to runtime configuration parameters for the miso + * package and its subpackages. + */ +public class MisoPrefs +{ + /** Used to load our preferences from a properties file and map them + * to the persistent Java preferences repository. */ + public static Config config = new Config("rsrc/config/miso"); +} diff --git a/src/java/com/threerings/miso/client/DirtyItemList.java b/src/java/com/threerings/miso/client/DirtyItemList.java new file mode 100644 index 00000000..f58b01e0 --- /dev/null +++ b/src/java/com/threerings/miso/client/DirtyItemList.java @@ -0,0 +1,680 @@ +// +// $Id: DirtyItemList.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.Graphics2D; +import java.util.ArrayList; +import java.util.Comparator; + +import com.samskivert.util.SortableArrayList; + +import com.threerings.media.Log; +import com.threerings.media.sprite.Sprite; +import com.threerings.media.tile.ObjectTile; + +/** + * The dirty item list keeps track of dirty sprites and object tiles + * in a scene. + */ +public class DirtyItemList +{ + /** + * Creates a dirt item list that will handle dirty items for the + * specified view. + */ + public DirtyItemList () + { + } + + /** + * Appends the dirty sprite at the given coordinates to the dirty item + * list. + * + * @param sprite the dirty sprite itself. + * @param tx the sprite's x tile position. + * @param ty the sprite's y tile position. + */ + public void appendDirtySprite (Sprite sprite, int tx, int ty) + { + DirtyItem item = getDirtyItem(); + item.init(sprite, tx, ty); + _items.add(item); + } + + /** + * Appends the dirty object tile at the given coordinates to the dirty + * item list. + * + * @param scobj the scene object that is dirty. + */ + public void appendDirtyObject (SceneObject scobj) + { + DirtyItem item = getDirtyItem(); + item.init(scobj, scobj.info.x, scobj.info.y); + _items.add(item); + } + + /** + * Returns the dirty item at the given index in the list. + */ + public DirtyItem get (int idx) + { + return (DirtyItem)_items.get(idx); + } + + /** + * Returns an array of the {@link DirtyItem} objects in the list + * sorted in proper rendering order. + */ + public void sort () + { + int size = size(); + + if (DEBUG_SORT) { + Log.info("Sorting dirty item list [size=" + size + "]."); + } + + // if we've only got one item, we need to do no sorting + if (size > 1) { + // get items sorted by increasing origin x-coordinate + _xitems.addAll(_items); + _xitems.sort(ORIGIN_X_COMP); + if (DEBUG_SORT) { + Log.info("Sorted by x-origin " + + "[items=" + toString(_xitems) + "]."); + } + + // get items sorted by increasing origin y-coordinate + _yitems.addAll(_items); + _yitems.sort(ORIGIN_Y_COMP); + if (DEBUG_SORT) { + Log.info("Sorted by y-origin " + + "[items=" + toString(_yitems) + "]."); + } + + // sort the items according to the depth of the rear-most tile + _ditems.addAll(_items); + _ditems.sort(REAR_DEPTH_COMP); + + // now insertion sort the items from back to front into the + // render-sorted array + _items.clear(); + POS_LOOP: + for (int ii = 0; ii < size; ii++) { + DirtyItem item = (DirtyItem)_ditems.get(ii); + for (int rr = _items.size()-1; rr >= 0; rr--) { + DirtyItem pitem = (DirtyItem)_items.get(rr); + // if we render in front of this item, insert + // ourselves immediately following it + if (_rcomp.compare(item, pitem) > 0) { + _items.add(rr+1, item); + continue POS_LOOP; + } + } + // we don't render in front of anyone, so we go at the + // front of the list + _items.add(0, item); + } + + // clear out our temporary arrays + _xitems.clear(); + _yitems.clear(); + _ditems.clear(); + } + + if (DEBUG_SORT) { + Log.info("Sorted for render [items=" + toString(_items) + "]."); + for (int ii = 0, ll = _items.size()-1; ii < ll; ii++) { + DirtyItem a = (DirtyItem)_items.get(ii); + DirtyItem b = (DirtyItem)_items.get(ii+1); + if (_rcomp.compare(a, b) > 0) { + Log.warning("Invalid ordering [a=" + a + ", b=" + b + "]."); + } + } + } + } + + /** + * Paints all the dirty items in this list using the supplied graphics + * context. The items are removed from the dirty list after being + * painted and the dirty list ends up empty. + */ + public void paintAndClear (Graphics2D gfx) + { + int icount = _items.size(); + for (int ii = 0; ii < icount; ii++) { + DirtyItem item = (DirtyItem)_items.get(ii); + item.paint(gfx); + item.clear(); + _freelist.add(item); + } + _items.clear(); + } + + /** + * Clears out any items that were in this list. + */ + public void clear () + { + for (int icount = _items.size(); icount > 0; icount--) { + DirtyItem item = (DirtyItem)_items.remove(0); + item.clear(); + _freelist.add(item); + } + } + + /** + * Returns the number of items in the dirty item list. + */ + public int size () + { + return _items.size(); + } + + /** + * Obtains a new dirty item instance, reusing an old one if possible + * or creating a new one otherwise. + */ + protected DirtyItem getDirtyItem () + { + if (_freelist.size() > 0) { + return (DirtyItem)_freelist.remove(0); + } else { + return new DirtyItem(); + } + } + + /** + * Returns an abbreviated string representation of the given dirty + * item describing only its origin coordinates and render priority. + * Intended for debugging purposes. + */ + protected static String toString (DirtyItem a) + { + StringBuilder buf = new StringBuilder("["); + toString(buf, a); + return buf.append("]").toString(); + } + + /** + * Returns an abbreviated string representation of the two given dirty + * items. See {@link #toString(DirtyItem)}. + */ + protected static String toString (DirtyItem a, DirtyItem b) + { + StringBuilder buf = new StringBuilder("["); + toString(buf, a); + toString(buf, b); + return buf.append("]").toString(); + } + + /** + * Returns an abbreviated string representation of the given dirty + * items. See {@link #toString(DirtyItem)}. + */ + protected static String toString (SortableArrayList items) + { + StringBuilder buf = new StringBuilder(); + buf.append("["); + for (int ii = 0; ii < items.size(); ii++) { + DirtyItem item = (DirtyItem)items.get(ii); + toString(buf, item); + if (ii < (items.size() - 1)) { + buf.append(", "); + } + } + return buf.append("]").toString(); + } + + /** Helper function for {@link #toString(DirtyItem)}. */ + protected static void toString (StringBuilder buf, DirtyItem item) + { + buf.append("(o:+").append(item.ox).append("+").append(item.oy); + buf.append(" p:").append(item.getRenderPriority()).append(")"); + } + + /** + * A class to hold the items inserted in the dirty list along with + * all of the information necessary to render their dirty regions + * to the target graphics context when the time comes to do so. + */ + public class DirtyItem + { + /** The dirtied object; one of either a sprite or an object tile. */ + public Object obj; + + /** The origin tile coordinates. */ + public int ox, oy; + + /** The leftmost tile coordinates. */ + public int lx, ly; + + /** The rightmost tile coordinates. */ + public int rx, ry; + + /** + * Initializes a dirty item. + */ + public void init (Object obj, int x, int y) + { + this.obj = obj; + this.ox = x; + this.oy = y; + + // calculate the item's leftmost and rightmost tiles; note + // that sprites occupy only a single tile, so leftmost and + // rightmost tiles are equivalent + lx = rx = ox; + ly = ry = oy; + if (obj instanceof SceneObject) { + ObjectTile tile = ((SceneObject)obj).tile; + lx -= (tile.getBaseWidth() - 1); + ry -= (tile.getBaseHeight() - 1); + } + } + + /** + * Paints the dirty item to the given graphics context. Only + * the portion of the item that falls within the given dirty + * rectangle is actually drawn. + */ + public void paint (Graphics2D gfx) + { + if (obj instanceof Sprite) { + ((Sprite)obj).paint(gfx); + } else { + ((SceneObject)obj).paint(gfx); + } + } + + /** + * Returns the "depth" of our rear-most tile. + */ + public int getRearDepth () + { + return ry + lx; + } + + /** + * Returns the render priority for this dirty item. It will be + * zero unless this is a display object which may have a custom + * render priority. + */ + public int getRenderPriority () + { + if (obj instanceof SceneObject) { + return ((SceneObject)obj).getPriority(); + } else { + return 0; + } + } + + /** + * Releases all references held by this dirty item so that it + * doesn't inadvertently hold on to any objects while waiting to + * be reused. + */ + public void clear () + { + obj = null; + } + + // documentation inherited + public boolean equals (Object other) + { + // we're never equal to something that's not our kind + if (!(other instanceof DirtyItem)) { + return false; + } + + // sprites are equivalent if they're the same sprite + DirtyItem b = (DirtyItem)other; + return obj.equals(b.obj); + } + + // documentation inherited + public int hashCode () + { + return obj.hashCode(); + } + + /** + * Returns a string representation of the dirty item. + */ + public String toString () + { + StringBuilder buf = new StringBuilder(); + buf.append("[obj=").append(obj); + buf.append(", ox=").append(ox); + buf.append(", oy=").append(oy); + buf.append(", lx=").append(lx); + buf.append(", ly=").append(ly); + buf.append(", rx=").append(rx); + buf.append(", ry=").append(ry); + return buf.append("]").toString(); + } + } + + /** + * A comparator class for use in sorting dirty items in ascending + * origin x- or y-axis coordinate order. + */ + protected static class OriginComparator implements Comparator + { + /** + * Constructs an origin comparator that sorts dirty items in + * ascending order based on their origin coordinate on the given + * axis. + */ + public OriginComparator (int axis) + { + _axis = axis; + } + + // documentation inherited + public int compare (Object a, Object b) + { + DirtyItem da = (DirtyItem)a; + DirtyItem db = (DirtyItem)b; + + // if they don't overlap, sort them normally + if (_axis == X_AXIS) { + if (da.ox != db.ox) { + return da.ox - db.ox; + } + } else { + if (da.oy != db.oy) { + return da.oy - db.oy; + } + } + + // if they do overlap, incorporate render priority; assume + // non-display objects have a render priority of zero + return da.getRenderPriority() - db.getRenderPriority(); + } + + /** The axis this comparator sorts on. */ + protected int _axis; + } + + /** + * A comparator class for use in sorting the dirty sprites and + * objects in a scene in ascending x- and y-coordinate order + * suitable for rendering in the isometric view with proper visual + * results. + */ + protected class RenderComparator implements Comparator + { + // documentation inherited + public int compare (Object a, Object b) + { + DirtyItem da = (DirtyItem)a; + DirtyItem db = (DirtyItem)b; + + // if the two objects are scene objects and they overlap, we + // compare them solely based on their human assigned priority + if ((da.obj instanceof SceneObject) && + (db.obj instanceof SceneObject)) { + SceneObject soa = (SceneObject)da.obj; + SceneObject sob = (SceneObject)db.obj; + if (soa.objectFootprintOverlaps(sob)) { + int result = soa.getPriority() - sob.getPriority(); + if (DEBUG_COMPARE) { + String items = DirtyItemList.toString(da, db); + Log.info("compare: overlapping [result=" + result + + ", items=" + items + "]."); + } + return result; + } + } + + // check for partitioning objects on the y-axis + int result = comparePartitioned(Y_AXIS, da, db); + if (result != 0) { + if (DEBUG_COMPARE) { + String items = DirtyItemList.toString(da, db); + Log.info("compare: Y-partitioned " + + "[result=" + result + ", items=" + items + "]."); + } + return result; + } + + // check for partitioning objects on the x-axis + result = comparePartitioned(X_AXIS, da, db); + if (result != 0) { + if (DEBUG_COMPARE) { + String items = DirtyItemList.toString(da, db); + Log.info("compare: X-partitioned " + + "[result=" + result + ", items=" + items + "]."); + } + return result; + } + + // use normal iso-ordering check + result = compareNonPartitioned(da, db); + if (DEBUG_COMPARE) { + String items = DirtyItemList.toString(da, db); + Log.info("compare: non-partitioned " + + "[result=" + result + ", items=" + items + "]."); + } + + return result; + } + + /** + * Returns whether two dirty items have a partitioning object + * between them on the given axis. + */ + protected int comparePartitioned ( + int axis, DirtyItem da, DirtyItem db) + { + // prepare for the partitioning check + SortableArrayList sitems; + Comparator comp; + boolean swapped = false; + switch (axis) { + case X_AXIS: + if (da.ox == db.ox) { + // can't be partitioned if there's no space between + return 0; + } + + // order items for proper comparison + if (da.ox > db.ox) { + DirtyItem temp = da; + da = db; + db = temp; + swapped = true; + } + + // use the axis-specific sorted array + sitems = _xitems; + comp = ORIGIN_X_COMP; + break; + + case Y_AXIS: + default: + if (da.oy == db.oy) { + // can't be partitioned if there's no space between + return 0; + } + + // order items for proper comparison + if (da.oy > db.oy) { + DirtyItem temp = da; + da = db; + db = temp; + swapped = true; + } + + // use the axis-specific sorted array + sitems = _yitems; + comp = ORIGIN_Y_COMP; + break; + } + + // get the bounding item indices and the number of + // potentially-partitioning dirty items + int aidx = sitems.binarySearch(da, comp); + int bidx = sitems.binarySearch(db, comp); + int size = bidx - aidx - 1; + + // check each potentially partitioning item + int startidx = aidx + 1, endidx = startidx + size; + for (int pidx = startidx; pidx < endidx; pidx++) { + DirtyItem dp = (DirtyItem)sitems.get(pidx); + if (dp.obj instanceof Sprite) { + // sprites can't partition things + continue; + } else if ((dp.obj == da.obj) || + (dp.obj == db.obj)) { + // can't be partitioned by ourselves + continue; + } + + // perform the actual partition check for this object + switch (axis) { + case X_AXIS: + if (dp.ly >= da.ry && + dp.ry <= db.ly && + dp.lx >= da.rx && + dp.rx <= db.lx) { + return (swapped) ? 1 : -1; + } + + case Y_AXIS: + default: + if (dp.lx <= db.ox && + dp.rx >= da.lx && + dp.ry >= da.oy && + dp.oy <= db.ry) { + return (swapped) ? 1 : -1; + } + } + } + + // no partitioning object found + return 0; + } + + /** + * Compares the two dirty items assuming there are no partitioning + * objects between them. + */ + protected int compareNonPartitioned (DirtyItem da, DirtyItem db) + { + if (da.ox == db.ox && + da.oy == db.oy) { + if (da.equals(db)) { + // render level is equal if we're the same sprite + // or an object at the same location + return 0; + } + + boolean aIsSprite = (da.obj instanceof Sprite); + boolean bIsSprite = (db.obj instanceof Sprite); + + if (aIsSprite && bIsSprite) { + Sprite as = (Sprite)da.obj, bs = (Sprite)db.obj; + // we're comparing two sprites co-existing on the same + // tile, first check their render order + int rocomp = as.getRenderOrder() - bs.getRenderOrder(); + if (rocomp != 0) { + return rocomp; + } + // next sort them by y-position + int ydiff = as.getY() - bs.getY(); + if (ydiff != 0) { + return ydiff; + } + // if they're at the same height, just use hashCode() + // to establish a consistent arbitrary ordering + return (as.hashCode() - bs.hashCode()); + + // otherwise, always put a sprite on top of a non-sprite + } else if (aIsSprite) { + return 1; + + } else if (bIsSprite) { + return -1; + } + } + + // otherwise use a consistent ordering for non-overlappers; + // see narya/docs/miso/render_sort_diagram.png for more info + if (db.lx <= da.ox && db.ry <= da.oy) { + return 1; + } else if (db.rx >= da.lx && db.ly >= da.ry) { + return -1; + } else { + return da.oy - db.oy; + } + } + } + + /** The list of dirty items. */ + protected SortableArrayList _items = new SortableArrayList(); + + /** The list of dirty items sorted by x-position. */ + protected SortableArrayList _xitems = new SortableArrayList(); + + /** The list of dirty items sorted by y-position. */ + protected SortableArrayList _yitems = new SortableArrayList(); + + /** The list of dirty items sorted by rear-depth. */ + protected SortableArrayList _ditems = new SortableArrayList(); + + /** The render comparator we'll use for our final, magical sort. */ + protected Comparator _rcomp = new RenderComparator(); + + /** Unused dirty items. */ + protected ArrayList _freelist = new ArrayList(); + + /** Whether to log debug info when comparing pairs of dirty items. */ + protected static final boolean DEBUG_COMPARE = false; + + /** Whether to log debug info for the main dirty item sorting algorithm. */ + protected static final boolean DEBUG_SORT = false; + + /** Constants used to denote axis sorting constraints. */ + protected static final int X_AXIS = 0; + protected static final int Y_AXIS = 1; + + /** The comparator used to sort dirty items in ascending origin + * x-coordinate order. */ + protected static final Comparator ORIGIN_X_COMP = + new OriginComparator(X_AXIS); + + /** The comparator used to sort dirty items in ascending origin + * y-coordinate order. */ + protected static final Comparator ORIGIN_Y_COMP = + new OriginComparator(Y_AXIS); + + /** The comparator used to sort dirty items in ascending "rear-depth" + * order. */ + protected static final Comparator REAR_DEPTH_COMP = new Comparator() { + public int compare (Object o1, Object o2) { + return (((DirtyItem)o1).getRearDepth() - + ((DirtyItem)o2).getRearDepth()); + } + }; +} diff --git a/src/java/com/threerings/miso/client/MisoScenePanel.java b/src/java/com/threerings/miso/client/MisoScenePanel.java new file mode 100644 index 00000000..193e6dda --- /dev/null +++ b/src/java/com/threerings/miso/client/MisoScenePanel.java @@ -0,0 +1,1735 @@ +// +// $Id: MisoScenePanel.java 4007 2006-04-10 08:59:30Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Composite; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Graphics; +import java.awt.Point; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.Stroke; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; + +import javax.swing.Icon; +import javax.swing.JFrame; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; + +import com.samskivert.swing.Controller; +import com.samskivert.swing.RadialMenu; +import com.samskivert.swing.RuntimeAdjust; +import com.samskivert.swing.event.CommandEvent; +import com.samskivert.util.HashIntMap; +import com.samskivert.util.StringUtil; + +import com.threerings.media.VirtualMediaPanel; +import com.threerings.media.sprite.Sprite; +import com.threerings.media.tile.Tile; +import com.threerings.media.tile.TileManager; +import com.threerings.media.tile.TileSet; +import com.threerings.media.util.AStarPathUtil; +import com.threerings.media.util.MathUtil; +import com.threerings.media.util.Path; + +import com.threerings.miso.Log; +import com.threerings.miso.MisoPrefs; +import com.threerings.miso.client.DirtyItemList.DirtyItem; +import com.threerings.miso.data.MisoSceneModel; +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.tile.BaseTile; +import com.threerings.miso.util.MisoContext; +import com.threerings.miso.util.MisoSceneMetrics; +import com.threerings.miso.util.MisoUtil; + +/** + * Renders a Miso scene for all to see. + */ +public class MisoScenePanel extends VirtualMediaPanel + implements MouseListener, MouseMotionListener, AStarPathUtil.TraversalPred, + RadialMenu.Host +{ + /** Show flag that indicates we should show all tips. */ + public static final int SHOW_TIPS = (1 << 0); + + /** + * Creates a blank miso scene display. Configure it with a scene model + * via {@link #setSceneModel} to cause it to display something. + */ + public MisoScenePanel (MisoContext ctx, MisoSceneMetrics metrics) + { + super(ctx.getFrameManager()); + _ctx = ctx; + _metrics = metrics; + + // set ourselves up + setOpaque(true); + addMouseListener(this); + addMouseMotionListener(this); + + // handy rectangle + _tbounds = new Rectangle(0, 0, _metrics.tilewid, _metrics.tilehei); + + // create the resolver if it's not already around + if (_resolver == null) { + _resolver = new SceneBlockResolver(); + _resolver.setDaemon(true); + _resolver.setPriority(Thread.MIN_PRIORITY); + _resolver.start(); + } + } + + /** + * Configures this display with a scene model which will immediately + * be resolved and displayed. + */ + public void setSceneModel (MisoSceneModel model) + { + _model = model; + + // clear out old blocks and objects + clearScene(); + + centerOnTile(0, 0); + if (isShowing()) { + rethink(); + _remgr.invalidateRegion(_vbounds); + } + } + + /** + * Clears out our old scene business. + */ + protected void clearScene () + { + _blocks.clear(); + _vizobjs.clear(); + _masks.clear(); + if (_dpanel != null) { + _dpanel.newScene(); + } + } + + /** + * Completely invalidates our current resolved scene and re-resolves + * it from the ground up. + */ + public void refreshScene () + { + clearScene(); + rethink(); + _delayRepaint = true; + _remgr.invalidateRegion(_vbounds); + } + + /** + * Moves the scene such that the specified tile is in the center. + */ + public void centerOnTile (int tx, int ty) + { + Rectangle trect = MisoUtil.getTilePolygon(_metrics, tx, ty).getBounds(); + int nx = trect.x + trect.width/2 - _vbounds.width/2; + int ny = trect.y + trect.height/2 - _vbounds.height/2; +// Log.info("Centering on t:" + StringUtil.coordsToString(tx, ty) + +// " b:" + StringUtil.toString(trect) + +// " vb: " + StringUtil.toString(_vbounds) + +// ", n:" + StringUtil.coordsToString(nx, ny) + "."); + setViewLocation(nx, ny); + } + + /** + * Returns the scene model being displayed by this panel. Do + * not modify! + */ + public MisoSceneModel getSceneModel () + { + return _model; + } + + /** + * Returns the scene metrics in use by this panel. Do not + * modify! + */ + public MisoSceneMetrics getSceneMetrics () + { + return _metrics; + } + + /** + * Set whether or not to highlight object tooltips (and potentially + * other scene entities). + */ + public void setShowFlags (int flags, boolean on) + { + int oldshow = _showFlags; + + if (on) { + _showFlags |= flags; + } else { + _showFlags &= ~flags; + } + + if (oldshow != _showFlags) { + showFlagsDidChange(oldshow); + } + } + + /** + * Check to see if the specified show flag is on. + */ + public boolean checkShowFlag (int flag) + { + return (0 != (flag & _showFlags)); + } + + /** + * Called when our show flags have changed. + */ + protected void showFlagsDidChange (int oldflags) + { + if ((oldflags & SHOW_TIPS) != (_showFlags & SHOW_TIPS)) { + for (Iterator iter = _tips.values().iterator(); iter.hasNext(); ) { + dirtyTip((SceneObjectTip)iter.next()); + } + } + } + + /** + * Returns the top-most object over which the mouse is hovering; this + * may be a sprite or a {@link SceneObject}. + */ + public Object getHoverObject () + { + return _hobject; + } + + /** + * Returns the tile coordinates of the tile over which the mouse is + * hovering. + */ + public Point getHoverCoords () + { + return _hcoords; + } + + /** + * Returns an iterator over all resolved {@link SceneBlock} instances. + */ + public Iterator enumerateResolvedBlocks () + { + return _blocks.values().iterator(); + } + + /** + * Returns the resolved block that contains the specified tile + * coordinate or null if no block is resolved for that coordinate. + */ + public SceneBlock getBlock (int tx, int ty) + { + int bx = MathUtil.floorDiv(tx, _metrics.blockwid); + int by = MathUtil.floorDiv(ty, _metrics.blockhei); + return (SceneBlock)_blocks.get(compose(bx, by)); + } + + /** + * Computes a path for the specified sprite to the specified tile + * coordinates. + * + * @param loose if true, an approximate path will be returned if a + * complete path cannot be located. This path will navigate the sprite + * "legally" as far as possible and then walk the sprite in a straight + * line to its final destination. This is generally only useful if the + * the path goes "off screen". + */ + public Path getPath (Sprite sprite, int x, int y, boolean loose) + { + // sanity check + if (sprite == null) { + throw new IllegalArgumentException( + "Can't get path for null sprite [x=" + x + ", y=" + y + "."); + } + + // get the destination tile coordinates + Point src = MisoUtil.screenToTile( + _metrics, sprite.getX(), sprite.getY(), new Point()); + Point dest = MisoUtil.screenToTile(_metrics, x, y, new Point()); + + // compute our longest path from the screen size + int longestPath = 3 * (getWidth() / _metrics.tilewid); + + // get a reasonable tile path through the scene + long start = System.currentTimeMillis(); + List points = AStarPathUtil.getPath( + this, sprite, longestPath, src.x, src.y, dest.x, dest.y, loose); + long duration = System.currentTimeMillis() - start; + + // sanity check the number of nodes searched so that we can keep + // an eye out for bogosity + if (duration > 500L) { + int considered = AStarPathUtil.getConsidered(); + Log.warning("Considered " + considered + " nodes for path from " + + StringUtil.toString(src) + " to " + + StringUtil.toString(dest) + + " [duration=" + duration + "]."); + } + + // construct a path object to guide the sprite on its merry way + return (points == null) ? null : + new TilePath(_metrics, sprite, points, x, y); + } + + /** + * Converts the supplied full coordinates to screen coordinates. + */ + public Point getScreenCoords (int x, int y) + { + return MisoUtil.fullToScreen(_metrics, x, y, new Point()); + } + + /** + * Coverts the supplied screen coordinates to full coordinates. + */ + public Point getFullCoords (int x, int y) + { + return MisoUtil.screenToFull(_metrics, x, y, new Point()); + } + + /** + * Coverts the supplied screen coordinates to tile coordinates. + */ + public Point getTileCoords (int x, int y) + { + return MisoUtil.screenToTile(_metrics, x, y, new Point()); + } + + /** + * Clears any radial menu being displayed. + */ + public void clearRadialMenu () + { + if (_activeMenu != null) { + _activeMenu.deactivate(); + } + } + + /** + * Reports the memory usage of the resolved tiles in the current scene + * block. + */ + public void reportMemoryUsage () + { + HashMap base = new HashMap(); + HashSet fringe = new HashSet(); + HashMap object = new HashMap(); + long[] usage = new long[3]; + for (Iterator iter = _blocks.values().iterator(); iter.hasNext(); ) { + SceneBlock block = (SceneBlock)iter.next(); + block.computeMemoryUsage(base, fringe, object, usage); + } + Log.info("Scene tile memory usage " + + "[scene=" + StringUtil.shortClassName(this) + + ", base=" + base.size() + "->" + (usage[0] / 1024) + "k" + + ", fringe=" + fringe.size() + "->" + (usage[1] / 1024) + "k" + + ", fmasks=" + _masks.size() + ", obj=" + object.size() + + "->" + (usage[2] / 1024) + "k" + "]."); + } + + // documentation inherited + public void addNotify () + { + super.addNotify(); + + if (_resolveDebug.getValue()) { + _dpanel = new ResolutionView(this); + _dframe = new JFrame("Scene block resolver"); + _dframe.setContentPane(_dpanel); + _dframe.pack(); + _dframe.setVisible(true); + } + } + + // documentation inherited + public void removeNotify () + { + super.removeNotify(); + + if (_dpanel != null) { + _dframe.dispose(); + _dpanel = null; + _dframe = null; + } + } + + // documentation inherited from interface + public void mouseClicked (MouseEvent e) + { + // nothing doing + } + + // documentation inherited from interface + public void mousePressed (MouseEvent e) + { + // ignore mouse presses if we're not responsive + if (!isResponsive()) { + return; + } + + if (e.getButton() == MouseEvent.BUTTON1) { + if (_hobject instanceof Sprite) { + handleSpritePressed((Sprite)_hobject, e.getX(), e.getY()); + return; + } else if (_hobject instanceof SceneObject) { + handleObjectPressed((SceneObject)_hobject, e.getX(), e.getY()); + return; + } + } + // if not button1, or _hobject not Sprite or SceneObject... + handleMousePressed(_hobject, e); + } + + /** + * Programmatically "click" a scene object. This results in a call to + * {@link #handleObjectPressed} with click coordinates in the center + * of the object. + */ + public void pressObject (SceneObject scobj) + { + int px = scobj.bounds.x + scobj.bounds.width/2; + int py = scobj.bounds.y + scobj.bounds.height/2; + handleObjectPressed(scobj, px, py); + } + + /** + * Called when the user presses the mouse button over a sprite. + */ + protected void handleSpritePressed (Sprite sprite, int mx, int my) + { + } + + /** + * Called when the user presses the mouse button over an object. + */ + protected void handleObjectPressed (final SceneObject scobj, int mx, int my) + { + String action = scobj.info.action; + final ObjectActionHandler handler = ObjectActionHandler.lookup(action); + + // if there's no handler, just fire the action immediately + if (handler == null) { + fireObjectAction(null, scobj, new SceneObjectActionEvent( + this, 0, action, 0, scobj)); + return; + } + + // if the action's not allowed, pretend like we handled it + if (!handler.actionAllowed(action)) { + return; + } + + // if there's no menu for this object, fire the action immediately + RadialMenu menu = handler.handlePressed(scobj); + if (menu == null) { + fireObjectAction(handler, scobj, new SceneObjectActionEvent( + this, 0, action, 0, scobj)); + return; + } + + // make the menu surround the clicked object, but with consistent size + Rectangle mbounds = getRadialMenuBounds(scobj); + + _activeMenu = menu; + _activeMenu.addActionListener(new ActionListener() { + public void actionPerformed (ActionEvent e) { + if (e instanceof CommandEvent) { + fireObjectAction(handler, scobj, e); + } else { + SceneObjectActionEvent event = new SceneObjectActionEvent( + e.getSource(), e.getID(), e.getActionCommand(), + e.getModifiers(), scobj); + fireObjectAction(handler, scobj, event); + } + } + }); + _activeMenu.activate(this, mbounds, scobj); + } + + /** + * Returns an appropriate set of menu bounds for the specified object. + * Returns a rectangle of the size specified by + * {@link #getObjectRadialSize} centered around the object. + */ + protected Rectangle getRadialMenuBounds (SceneObject scobj) + { + Rectangle mbounds = new Rectangle(scobj.bounds); + Dimension radbox = getObjectRadialSize(); + if (mbounds.width != radbox.width) { + mbounds.x += (mbounds.width-radbox.width)/2; + mbounds.width = radbox.width; + } + if (mbounds.height != radbox.height) { + mbounds.y += (mbounds.height-radbox.height)/2; + mbounds.height = radbox.height; + } + return mbounds; + } + + /** + * Returns the size of the rectangle around which we create an + * object's radial menu. The default is a sensible size, but derived + * classes may wish to tune the value to make their menus lay out in a + * more aestetically pleasing manner. + */ + protected Dimension getObjectRadialSize () + { + return DEF_RADIAL_RECT; + } + + /** + * Called when an object or object menu item has been clicked. + */ + protected void fireObjectAction ( + ObjectActionHandler handler, SceneObject scobj, ActionEvent event) + { + if (handler == null) { + Controller.postAction(event); + } else { + handler.handleAction(scobj, event); + } + } + + /** + * Called when the mouse is pressed over an unknown or non-existent + * hover object. + * + * @param hobject the hover object at the time of the mouse press or + * null if no hover object is active. + * + * @return true if the mouse press was handled, false if not. + */ + protected boolean handleMousePressed (Object hobject, MouseEvent event) + { + return false; + } + + // documentation inherited from interface + public void mouseReleased (MouseEvent e) + { + // nothing doing; everything is handled on pressed + } + + // documentation inherited from interface + public void mouseEntered (MouseEvent e) + { + // nothing doing + } + + // documentation inherited from interface + public void mouseExited (MouseEvent e) + { + // clear the highlight tracking data + _hcoords.setLocation(-1, -1); + changeHoverObject(null); + _remgr.invalidateRegion(_vbounds); + } + + // documentation inherited from interface + public void mouseDragged (MouseEvent e) + { + // nothing doing + } + + // documentation inherited from interface + public void mouseMoved (MouseEvent e) + { + int x = e.getX(), y = e.getY(); + + // update the mouse's tile coordinates + updateTileCoords(x, y, _hcoords); + + // stop now if we're not responsive + if (!isResponsive()) { + return; + } + + // give derived classes a chance to start with a hover object + Object hobject = computeOverHover(x, y); + + // if they came up with nothing, compute the list of objects over + // which the mouse is hovering + if (hobject == null) { + // start with the sprites that contain the point + _spritemgr.getHitSprites(_hitSprites, x, y); + int hslen = _hitSprites.size(); + for (int i = 0; i < hslen; i++) { + Sprite sprite = (Sprite)_hitSprites.get(i); + appendDirtySprite(_hitList, sprite); + } + + // add the object tiles that contain the point + getHitObjects(_hitList, x, y); + + // sort the list of hit items by rendering order + _hitList.sort(); + + // the last element in the array is what we want (assuming + // there are any items in the array) + int icount = _hitList.size(); + if (icount > 0) { + DirtyItem item = (DirtyItem)_hitList.get(icount-1); + hobject = item.obj; + } + + // clear out the hitlists + _hitList.clear(); + _hitSprites.clear(); + } + + // if the user isn't hovering over a sprite or object with an + // action, allow derived classes to provide some other hover + if (hobject == null) { + hobject = computeUnderHover(x, y); + } + + changeHoverObject(hobject); + } + + /** + * Gives derived classes a chance to compute a hover object that takes + * precedence over sprites and actionable objects. If this method + * returns non-null, no sprite or object hover calculations will be + * performed and the object returned will become the new hover object. + */ + protected Object computeOverHover (int mx, int my) + { + return null; + } + + /** + * Gives derived classes a chance to compute a hover object that is + * used if the mouse is not hovering over a sprite or actionable + * object. If this method is called, it means that there are no + * sprites or objects under the mouse. Thus if it returns non-null, + * the object returned will become the new hover object. + */ + protected Object computeUnderHover (int mx, int my) + { + return null; + } + + // documentation inherited + public boolean canTraverse (Object traverser, int tx, int ty) + { + SceneBlock block = getBlock(tx, ty); + return (block == null) ? canTraverseUnresolved(traverser, tx, ty) : + block.canTraverse(traverser, tx, ty); + } + + /** + * Derived classes can control whether or not we consider unresolved + * tiles to be traversable or not. + */ + protected boolean canTraverseUnresolved (Object traverser, int tx, int ty) + { + return false; + } + + // documentation inherited from interface + public Rectangle getViewBounds () + { + return _vbounds; + } + + // documentation inherited from interface + public Component getComponent () + { + return this; + } + + // documentation inherited from interface + public void repaintRect (int x, int y, int width, int height) + { + // translate back into view coordinates + x -= _vbounds.x; + y -= _vbounds.y; + repaint(x, y, width, height); + } + + // documentation inherited from interface + public void menuDeactivated (RadialMenu menu) + { + _activeMenu = null; + } + + // documentation inherited + public void setBounds (int x, int y, int width, int height) + { + super.setBounds(x, y, width, height); + + // if we change size... + if (width != _rsize.width || height != _rsize.height) { + // ...adjust our view location to preserve the center of the + // screen... + int dx = (_rsize.width-width)/2, dy = (_rsize.height-height)/2; +// Log.info("Adjusting offset " + +// "rsize:" + StringUtil.toString(_rsize) + +// " nsize:" + width + "x" + height + +// " vb:" + StringUtil.toString(_vbounds) + +// " d:" + StringUtil.coordsToString(dx, dy) + "."); + setViewLocation(_nx+dx, _ny+dy); + _rsize.setSize(width, height); + + // ...and force a rethink on the next tick + _ulpos = null; + } + } + + // documentation inherited + protected void viewLocationDidChange (int dx, int dy) + { + super.viewLocationDidChange(dx, dy); + + // compute the tile coordinates of our upper left screen + // coordinate and request a rethink if they've changed + MisoUtil.screenToTile(_metrics, _vbounds.x, _vbounds.y, _tcoords); + if (_ulpos == null || !_tcoords.equals(_ulpos)) { + // if this is a forced rethink (_ulpos is null), we might + // delay paint as a result of it, but only if we queue up + // blocks for resolution in our rethink + boolean mightDelayPaint = false; + if (_ulpos == null) { + _ulpos = new Point(); + mightDelayPaint = true; + } + _ulpos.setLocation(_tcoords); + if (rethink() > 0) { + _delayRepaint = mightDelayPaint; + Log.info("Delaying repaint... " + + "[need=" + _visiBlocks.size() + + ", of=" + _pendingBlocks + + ", view=" + StringUtil.toString(_vbounds) + "]."); + } + } + } + + /** + * Derived classes can override this method and provide a colorizer + * that will be used to colorize the supplied scene object when + * rendering. + */ + protected TileSet.Colorizer getColorizer (ObjectInfo oinfo) + { + return null; + } + + /** + * Computes the tile coordinates of the supplied sprite and appends it + * to the supplied dirty item list. + */ + protected void appendDirtySprite (DirtyItemList list, Sprite sprite) + { + MisoUtil.screenToTile(_metrics, sprite.getX(), sprite.getY(), _tcoords); + list.appendDirtySprite(sprite, _tcoords.x, _tcoords.y); + } + + /** + * Returns the tile manager from which we load our tiles. + */ + protected TileManager getTileManager () + { + return _ctx.getTileManager(); + } + + /** + * This is called when our view position has changed by more than one + * tile in any direction. Herein we do a whole crapload of stuff: + * + *

  • Queue up loads for any new influential blocks. + *
  • Flush any blocks that are no longer influential. + *
  • Recompute the list of potentially visible scene objects. + *
+ * + * @return the count of blocks pending after this rethink. + */ + protected int rethink () + { + // recompute our "area of influence" + computeInfluentialBounds(); + +// Log.info("Rethinking vb:" + StringUtil.toString(_vbounds) + +// " ul:" + StringUtil.toString(_ulpos) + +// " ibounds: " + StringUtil.toString(_ibounds)); + + // not to worry if we presently have no scene model + if (_model == null) { + return 0; + } + + // compute the intersecting set of blocks + applyToTiles(_ibounds, _rethinkOp); +// Log.info("Influential blocks " + +// StringUtil.toString(_rethinkOp.blocks) + "."); + + // prune any blocks that are no longer influential + Point key = new Point(); + for (Iterator iter = _blocks.values().iterator(); iter.hasNext(); ) { + SceneBlock block = (SceneBlock)iter.next(); + key.x = block.getBounds().x; + key.y = block.getBounds().y; + if (!_rethinkOp.blocks.contains(key)) { + Log.debug("Flushing block " + block + "."); + if (_dpanel != null) { + _dpanel.blockCleared(block); + } + iter.remove(); + } + } + + for (Iterator iter = _rethinkOp.blocks.iterator(); iter.hasNext(); ) { + Point origin = (Point)iter.next(); + int bx = MathUtil.floorDiv(origin.x, _metrics.blockwid); + int by = MathUtil.floorDiv(origin.y, _metrics.blockhei); + int bkey = compose(bx, by); + if (!_blocks.containsKey(bkey)) { + SceneBlock block = new SceneBlock( + this, origin.x, origin.y, + _metrics.blockwid, _metrics.blockhei); + boolean visible = + block.getFootprint().getBounds().intersects(_vibounds); + block.setVisiBlock(visible); + _blocks.put(bkey, block); + + // queue the block up to be resolved + _pendingBlocks++; + if (visible) { + _visiBlocks.add(block); + } + _resolver.resolveBlock(block, visible); + if (_dpanel != null) { + _dpanel.queuedBlock(block); + } + } + } + _rethinkOp.blocks.clear(); + + // recompute our visible object set + recomputeVisible(); + + Log.debug("Rethunk [pending=" + _pendingBlocks + + ", visible=" + _visiBlocks.size() + "]."); + return _visiBlocks.size(); + } + + /** + * Called during the {@link #rethink} process, configures {@link + * #_ibounds} to contain the bounds of the potentially "influential" + * world and {@link #_vibounds} to contain bounds that are used to + * determine which blocks should be resolved before making the view + * visible. + * + *

Everything that intersects the influential area will be + * resolved on the expectation that it could be scrolled into view at + * any time. The influential bounds should be large enough that the + * time between a block becoming influential and the time at which it + * is resolved is longer than the expected time by which it will be + * scrolled into view, otherwise the users will see the man behind the + * curtain. + */ + protected void computeInfluentialBounds () + { + int infborx = 3*_vbounds.width/4; + int infbory = _vbounds.height/2; + _ibounds.setBounds(_vbounds.x-infborx, _vbounds.y-infbory, + _vbounds.width+2*infborx, + // we go extra on the height because objects + // below can influence fairly high up + _vbounds.height+3*infbory); + _vibounds.setBounds(_vbounds.x-_vbounds.width/4, _vbounds.y, + _vbounds.width+_vbounds.width/2, + _vbounds.height+infbory); + } + + /** + * Returns the bounds for which all intersecting scene blocks are kept + * resolved. Do not modify the rectangle returned by this method. + */ + protected Rectangle getInfluentialBounds () + { + return _ibounds; + } + + /** + * Called by the scene block when it has started its resolution. + */ + protected void blockResolving (SceneBlock block) + { + if (_dpanel != null) { + _dpanel.resolvingBlock(block); + } + } + + /** + * Called by the scene block if it has come up for resolution but is + * no longer influential. + */ + protected void blockAbandoned (SceneBlock block) + { + if (_dpanel != null) { + _dpanel.blockCleared(block); + } + } + + /** + * Called by a scene block when it has completed its resolution + * process. + */ + protected void blockResolved (SceneBlock block) + { + if (_dpanel != null) { + _dpanel.resolvedBlock(block); + } + + Rectangle sbounds = block.getScreenBounds(); + if (!_delayRepaint && sbounds != null && sbounds.intersects(_vbounds)) { +// warnVisible(block, sbounds); + // if we have yet further blocks to resolve, queue up a + // repaint now so that we get this data onscreen as quickly as + // possible + if (_pendingBlocks > 1) { + recomputeVisible(); + _remgr.invalidateRegion(sbounds); + } + } + --_pendingBlocks; +// if (_pendingBlocks == 0) { +// Log.info("Finished resolving pending blocks " + +// "[view=" + StringUtil.toString(_vbounds) + "]."); +// reportMemoryUsage(); +// } + + // once all the visible pending blocks have completed their + // resolution, recompute our visible object set and show ourselves + if (_visiBlocks.remove(block) && _visiBlocks.size() == 0) { + recomputeVisible(); + Log.info("Restoring repaint... [left=" + _pendingBlocks + + ", view=" + StringUtil.toString(_vbounds) + "]."); + _delayRepaint = false; + _remgr.invalidateRegion(_vbounds); + } + } + + /** + * Issues a warning to the error log that the specified block became + * visible prior to being resolved. Derived classes may wish to + * augment or inhibit this warning. + */ + protected void warnVisible (SceneBlock block, Rectangle sbounds) + { + Log.warning("Block visible during resolution " + block + + " sbounds:" + StringUtil.toString(sbounds) + + " vbounds:" + StringUtil.toString(_vbounds) + "."); + } + + /** + * Recomputes our set of visible objects and their tips. + */ + protected void recomputeVisible () + { + // flush our visible object set which we'll recreate later + _vizobjs.clear(); + + Rectangle vbounds = new Rectangle( + _vbounds.x-_metrics.tilewid, _vbounds.y-_metrics.tilehei, + _vbounds.width+2*_metrics.tilewid, + _vbounds.height+2*_metrics.tilehei); + + for (Iterator iter = _blocks.values().iterator(); iter.hasNext(); ) { + SceneBlock block = (SceneBlock)iter.next(); + if (!block.isResolved()) { + continue; + } + + // links this block to its neighbors; computes coverage + block.update(_blocks); + + // see which of this block's objects are visible + SceneObject[] objs = block.getObjects(); + for (int ii = 0; ii < objs.length; ii++) { + if (objs[ii].bounds != null && + vbounds.intersects(objs[ii].bounds)) { + _vizobjs.add(objs[ii]); + } + } + } + + // recompute our object tips + computeTips(); + +// Log.info("Computed " + _vizobjs.size() + " visible objects from " + +// _blocks.size() + " blocks."); + +// Log.info(StringUtil.listToString(_vizobjs, new StringUtil.Formatter() { +// public String toString (Object object) { +// SceneObject scobj = (SceneObject)object; +// return (TileUtil.getTileSetId(scobj.info.tileId) + ":" + +// TileUtil.getTileIndex(scobj.info.tileId)); +// } +// })); + } + + /** + * Masks off the lower 16 bits of the supplied integers and composes + * them into a single int. + */ + protected static int compose (int x, int y) + { + return (x << 16) | (y & 0xFFFF); + } + + /** + * Compute the tips for any objects in the scene. + */ + public void computeTips () + { + // clear any old tips + _tips.clear(); + + for (int ii = 0, nn = _vizobjs.size(); ii < nn; ii++) { + SceneObject scobj = (SceneObject)_vizobjs.get(ii); + String action = scobj.info.action; + + // if the object has no action, skip it + if (StringUtil.isBlank(action)) { + continue; + } + + // if we have an object action handler, possibly let them veto + // the display of this tooltip and action + ObjectActionHandler oah = ObjectActionHandler.lookup(action); + if (oah != null && !oah.isVisible(action)) { + continue; + } + + String tiptext = getTipText(scobj, action); + if (tiptext != null) { + Icon icon = getTipIcon(scobj, action); + SceneObjectTip tip = new SceneObjectTip(tiptext, icon); + _tips.put(scobj, tip); + } + } + + _tipsLaidOut = false; + } + + /** + * Derived classes can provide human readable object tips via this + * method. + */ + protected String getTipText (SceneObject scobj, String action) + { + ObjectActionHandler oah = ObjectActionHandler.lookup(action); + return (oah == null) ? action : oah.getTipText(action); + } + + /** + * Provides an icon for this tooltip, the default looks up an object + * action handler for the action and requests the icon from it. + */ + protected Icon getTipIcon (SceneObject scobj, String action) + { + ObjectActionHandler oah = ObjectActionHandler.lookup(action); + return (oah == null) ? null : oah.getTipIcon(action); + } + + /** + * Dirties the specified tip. + */ + protected void dirtyTip (SceneObjectTip tip) + { + if (tip != null) { + Rectangle r = tip.bounds; + if (r != null) { + _remgr.invalidateRegion(r); + } + } + } + + /** + * Change the hover object to the new object. + */ + protected void changeHoverObject (Object newHover) + { + if (newHover == _hobject) { + return; + } + Object oldHover = _hobject; + _hobject = newHover; + hoverObjectChanged(oldHover, newHover); + } + + /** + * A place for subclasses to react to the hover object changing. + * One of the supplied arguments may be null. + */ + protected void hoverObjectChanged (Object oldHover, Object newHover) + { + // deal with objects that care about being hovered over + if (oldHover instanceof SceneObject) { + SceneObject oldhov = (SceneObject)oldHover; + if (oldhov.setHovered(false)) { + _remgr.invalidateRegion(oldhov.bounds); + } + } + if (newHover instanceof SceneObject) { + SceneObject newhov = (SceneObject)newHover; + if (newhov.setHovered(true)) { + _remgr.invalidateRegion(newhov.bounds); + } + } + + // dirty the tips associated with the hover objects + dirtyTip((SceneObjectTip)_tips.get(oldHover)); + dirtyTip((SceneObjectTip)_tips.get(newHover)); + } + + /** + * Adds to the supplied dirty item list, all of the object tiles that + * are hit by the specified point (meaning the point is contained + * within their bounds and intersects a non-transparent pixel in the + * actual object image. + */ + protected void getHitObjects (DirtyItemList list, int x, int y) + { + for (int ii = 0, ll = _vizobjs.size(); ii < ll; ii++) { + SceneObject scobj = (SceneObject)_vizobjs.get(ii); + Rectangle pbounds = scobj.bounds; + if (!pbounds.contains(x, y)) { + continue; + } + + // see if we should skip it + if (skipHitObject(scobj)) { + continue; + } + + // now check that the pixel in the tile image is + // non-transparent at that point + if (!scobj.tile.hitTest(x - pbounds.x, y - pbounds.y)) { + continue; + } + + // we've passed the test, add the object to the list + list.appendDirtyObject(scobj); + } + } + + /** + * Determines whether we should skip the specified object when compiling + * the list of objects under a specified point using + * {@link #getHitObjects}. The default implementation returns + * true iff the object has no action. + */ + protected boolean skipHitObject (SceneObject scobj) + { + return StringUtil.isBlank(scobj.info.action); + } + + /** + * Converts the supplied screen coordinates into tile coordinates, + * writing the values into the supplied {@link Point} instance and + * returning true if the screen coordinates translated into a + * different set of tile coordinates than were already contained in + * the point (so that the caller can know to update a highlight, for + * example). + * + * @return true if the tile coordinates have changed. + */ + protected boolean updateTileCoords (int sx, int sy, Point tpos) + { + Point npos = MisoUtil.screenToTile(_metrics, sx, sy, new Point()); + if (!tpos.equals(npos)) { + tpos.setLocation(npos.x, npos.y); + return true; + } else { + return false; + } + } + + // documentation inherited + public void paint (Graphics g) + { + if (_delayRepaint) { + return; + } + super.paint(g); + } + + // documentation inherited + protected void paintInFront (Graphics2D gfx, Rectangle dirty) + { + super.paintInFront(gfx, dirty); + + // paint any active menu (this should in theory check to see if + // the active menu intersects one or more of the dirty rects) + if (_activeMenu != null) { + _activeMenu.render(gfx); + } + } + + // documentation inherited + protected void paintBetween (Graphics2D gfx, Rectangle dirty) + { + // render any intersecting tiles + paintTiles(gfx, dirty); + + // render anything that goes on top of the tiles + paintBaseDecorations(gfx, dirty); + + // render our dirty sprites and objects + paintDirtyItems(gfx, dirty); + + // draw sprite paths + if (_pathsDebug.getValue()) { + _spritemgr.renderSpritePaths(gfx); + } + + // paint any extra goodies + paintExtras(gfx, dirty); + } + + /** + * We don't want sprites rendered using the standard mechanism because + * we intersperse them with objects in our scene and need to manage + * their z-order. + */ + protected void paintBits (Graphics2D gfx, int layer, Rectangle dirty) + { + _animmgr.paint(gfx, layer, dirty); + } + + /** + * A function where derived classes can paint things after the base + * tiles have been rendered but before anything else has been rendered + * (so that whatever is painted appears to be on the ground). + */ + protected void paintBaseDecorations (Graphics2D gfx, Rectangle clip) + { + // nothing for now + } + + /** + * Renders the dirty sprites and objects in the scene to the given + * graphics context. + */ + protected void paintDirtyItems (Graphics2D gfx, Rectangle clip) + { + // add any sprites impacted by the dirty rectangle + _dirtySprites.clear(); + _spritemgr.getIntersectingSprites(_dirtySprites, clip); + int size = _dirtySprites.size(); + for (int ii = 0; ii < size; ii++) { + Sprite sprite = (Sprite)_dirtySprites.get(ii); + Rectangle bounds = sprite.getBounds(); + if (!bounds.intersects(clip)) { + continue; + } + appendDirtySprite(_dirtyItems, sprite); +// Log.info("Dirtied item: " + sprite); + } + + // add any objects impacted by the dirty rectangle + for (int ii = 0, ll = _vizobjs.size(); ii < ll; ii++) { + SceneObject scobj = (SceneObject)_vizobjs.get(ii); + if (!scobj.bounds.intersects(clip)) { + continue; + } + _dirtyItems.appendDirtyObject(scobj); +// Log.info("Dirtied item: " + scobj); + } + +// Log.info("paintDirtyItems [items=" + _dirtyItems.size() + "]."); + + // sort the dirty items so that we can paint them back-to-front + _dirtyItems.sort(); + _dirtyItems.paintAndClear(gfx); + } + + /** + * A function where derived classes can paint extra stuff while we've + * got the clipping region set up. + */ + protected void paintExtras (Graphics2D gfx, Rectangle clip) + { + if (isResponsive()) { + paintTips(gfx, clip); + } + } + + /** + * Paint all the appropriate tips for our scene objects. + */ + protected void paintTips (Graphics2D gfx, Rectangle clip) + { + // make sure the tips are ready + if (!_tipsLaidOut) { + ArrayList boxlist = new ArrayList(); + for (Iterator iter = _tips.keySet().iterator(); iter.hasNext(); ) { + SceneObject scobj = (SceneObject)iter.next(); + SceneObjectTip tip = (SceneObjectTip)_tips.get(scobj); + tip.layout(gfx, scobj, _vbounds, boxlist); + boxlist.add(tip.bounds); + } + _tipsLaidOut = true; + } + + if (checkShowFlag(SHOW_TIPS)) { + // show all the tips + for (Iterator iter = _tips.values().iterator(); iter.hasNext(); ) { + paintTip(gfx, clip, (SceneObjectTip)iter.next()); + } + + } else { + // show maybe one tip + SceneObjectTip tip = (SceneObjectTip)_tips.get(_hobject); + if (tip != null) { + paintTip(gfx, clip, tip); + } + } + } + + /** + * Paint the specified tip if it intersects the clipping rectangle. + */ + protected void paintTip (Graphics2D gfx, Rectangle clip, SceneObjectTip tip) + { + if (clip.intersects(tip.bounds)) { + tip.paint(gfx); + } + } + + /** + * Applies the supplied tile operation to all tiles that intersect the + * supplied screen rectangle. + */ + protected void applyToTiles (Rectangle bounds, TileOp op) + { + // determine which tiles intersect this region: this is going to + // be nearly incomprehensible without some sort of diagram; i'll + // do what i can to comment it, but you'll want to print out a + // scene diagram (docs/miso/scene.ps) and start making notes if + // you want to follow along + + // obtain our upper left tile + Point tpos = MisoUtil.screenToTile( + _metrics, bounds.x, bounds.y, new Point()); + + // determine which quadrant of the upper left tile we occupy + Point spos = MisoUtil.tileToScreen( + _metrics, tpos.x, tpos.y, new Point()); + boolean left = (bounds.x - spos.x < _metrics.tilehwid); + boolean top = (bounds.y - spos.y < _metrics.tilehhei); + + // set up our tile position counters + int dx, dy; + if (left) { + dx = 0; dy = 1; + } else { + dx = 1; dy = 0; + } + + // if we're in the top-half of the tile we need to move up a row, + // either forward or back depending on whether we're in the left + // or right half of the tile + if (top) { + if (left) { + tpos.x -= 1; + } else { + tpos.y -= 1; + } + // we'll need to start zig-zagging the other way as well + dx = 1 - dx; + dy = 1 - dy; + } + + // these will bound our loops + int rightx = bounds.x + bounds.width, + bottomy = bounds.y + bounds.height; + +// Log.info("Preparing to apply [tpos=" + StringUtil.toString(tpos) + +// ", left=" + left + ", top=" + top + +// ", bounds=" + StringUtil.toString(bounds) + +// ", spos=" + StringUtil.toString(spos) + +// "]."); + + // obtain the coordinates of the tile that starts the first row + // and loop through, applying to the intersecting tiles + MisoUtil.tileToScreen(_metrics, tpos.x, tpos.y, spos); + while (spos.y < bottomy) { + // set up our row counters + int tx = tpos.x, ty = tpos.y; + _tbounds.x = spos.x; + _tbounds.y = spos.y; + +// Log.info("Applying to row [tx=" + tx + ", ty=" + ty + "]."); + + // apply to the tiles in this row + while (_tbounds.x < rightx) { + op.apply(tx, ty, _tbounds); + + // move one tile to the right + tx += 1; ty -= 1; + _tbounds.x += _metrics.tilewid; + } + + // update our tile coordinates + tpos.x += dx; dx = 1-dx; + tpos.y += dy; dy = 1-dy; + + // obtain the screen coordinates of the next starting tile + MisoUtil.tileToScreen(_metrics, tpos.x, tpos.y, spos); + } + } + + /** + * Renders the base and fringe layer tiles that intersect the + * specified clipping rectangle. + */ + protected void paintTiles (Graphics2D gfx, Rectangle clip) + { + // go through rendering our tiles + _paintOp.setGraphics(gfx); + applyToTiles(clip, _paintOp); + _paintOp.setGraphics(null); + } + + /** + * Fills the specified tile with the given color at 50% alpha. + * Intended for debug-only tile highlighting purposes. + */ + protected void fillTile ( + Graphics2D gfx, int tx, int ty, Color color) + { + Composite ocomp = gfx.getComposite(); + gfx.setComposite(ALPHA_FILL_TILE); + Polygon poly = MisoUtil.getTilePolygon(_metrics, tx, ty); + gfx.setColor(color); + gfx.fill(poly); + gfx.setComposite(ocomp); + } + + /** Returns the base tile for the specified tile coordinate. */ + protected BaseTile getBaseTile (int tx, int ty) + { + SceneBlock block = getBlock(tx, ty); + return (block == null) ? null : block.getBaseTile(tx, ty); + } + + /** Returns the fringe tile for the specified tile coordinate. */ + protected BaseTile getFringeTile (int tx, int ty) + { + SceneBlock block = getBlock(tx, ty); + return (block == null) ? null : block.getFringeTile(tx, ty); + } + + /** Computes the fringe tile for the specified coordinate. */ + protected BaseTile computeFringeTile (int tx, int ty) + { + return _ctx.getTileManager().getAutoFringer().getFringeTile( + _model, tx, ty, _masks); + } + + /** + * Returns true if we're responding to user input. This is used to + * control the display of tooltips and other potential user + * interactions. By default we are always responsive. + */ + protected boolean isResponsive () + { + return true; + } + + /** Used with {@link #applyToTiles}. */ + protected static interface TileOp + { + public void apply (int tx, int ty, Rectangle tbounds); + } + + /** Used by {@link #paintTiles}. */ + protected class PaintTileOp implements TileOp + { + public void setGraphics (Graphics2D gfx) { + _gfx = gfx; + _thw = 0; + _thh = 0; + _fhei = 0; + _fm = null; + + // if we're showing coordinates, we need to do some setting up + if (gfx != null && _coordsDebug.getValue()) { + _fm = gfx.getFontMetrics(_font); + _fhei = _fm.getAscent(); + _thw = _metrics.tilehwid; + _thh = _metrics.tilehhei; + gfx.setFont(_font); + } + } + + public void apply (int tx, int ty, Rectangle tbounds) { + // draw the base and fringe tile images + try { + Tile tile; + boolean passable = true; + + if ((tile = getBaseTile(tx, ty)) != null) { + tile.paint(_gfx, tbounds.x, tbounds.y); + passable = ((BaseTile)tile).isPassable(); + + } else { + // draw black where there are no tiles + Polygon poly = MisoUtil.getTilePolygon(_metrics, tx, ty); + _gfx.setColor(Color.black); + _gfx.fill(poly); + } + + if ((tile = getFringeTile(tx, ty)) != null) { + tile.paint(_gfx, tbounds.x, tbounds.y); + passable = passable && ((BaseTile)tile).isPassable(); + } + + // highlight impassable tiles + if (_traverseDebug.getValue()) { + if (!passable) { + // highlight tiles blocked by base or fringe in yellow + fillTile(_gfx, tx, ty, Color.yellow); + + } else if (!canTraverse(null, tx, ty)) { + // highlight passable non-traversable tiles in green + fillTile(_gfx, tx, ty, Color.green); + } + } + + } catch (ArrayIndexOutOfBoundsException e) { + Log.warning("Whoops, booched it [tx=" + tx + + ", ty=" + ty + ", tb.x=" + tbounds.x + "]."); + e.printStackTrace(System.err); + } + + // if we're showing coordinates, do that + if (_coordsDebug.getValue()) { + // set the color according to the scene block + int bx = MathUtil.floorDiv(tx, _metrics.blockwid); + int by = MathUtil.floorDiv(ty, _metrics.blockhei); + if (((bx % 2) ^ (by % 2)) == 0) { + _gfx.setColor(Color.white); + } else { + _gfx.setColor(Color.yellow); + } + + // get the top-left screen coordinates of the tile + int sx = tbounds.x, sy = tbounds.y; + + // draw x-coordinate + String str = String.valueOf(tx); + int xpos = sx + _thw - (_fm.stringWidth(str) / 2); + _gfx.drawString(str, xpos, sy + _thh); + + // draw y-coordinate + str = String.valueOf(ty); + xpos = sx + _thw - (_fm.stringWidth(str) / 2); + _gfx.drawString(str, xpos, sy + _thh + _fhei); + + // draw the tile polygon as well + _gfx.draw(MisoUtil.getTilePolygon(_metrics, tx, ty)); + } + } + + protected Graphics2D _gfx; + protected FontMetrics _fm; + protected int _thw, _thh, _fhei; + protected Font _font = new Font("Arial", Font.PLAIN, 7); + } + + /** Used by {@link #rethink}. */ + protected class RethinkOp implements TileOp + { + public HashSet blocks = new HashSet(); + + public void apply (int tx, int ty, Rectangle tbounds) { + _key.x = MathUtil.floorDiv(tx, _metrics.blockwid) * + _metrics.blockwid; + _key.y = MathUtil.floorDiv(ty, _metrics.blockhei) * + _metrics.blockhei; + if (!blocks.contains(_key)) { + blocks.add(new Point(_key.x, _key.y)); + } + } + + protected Point _key = new Point(); + } + + /** Provides access to a few things. */ + protected MisoContext _ctx; + + /** Contains basic scene metrics like tile width and height. */ + protected MisoSceneMetrics _metrics; + + /** The scene model to be displayed. */ + protected MisoSceneModel _model; + + /** Tracks the size at which we were last "rethunk". */ + protected Dimension _rsize = new Dimension(); + + /** Contains the tile coords of our upper-left view coord. */ + protected Point _ulpos; + + /** Contains the bounds of our "area of influence" in screen coords. */ + protected Rectangle _ibounds = new Rectangle(); + + /** Contains the bounds of our visible "area of influence" in screen + * coords. */ + protected Rectangle _vibounds = new Rectangle(); + + /** Used by {@link #rethink}. */ + protected RethinkOp _rethinkOp = new RethinkOp(); + + /** Contains our scene blocks. See {@link #getBlock} for details. */ + protected HashIntMap _blocks = new HashIntMap(); + + /** A count of blocks in the process of being resolved. */ + protected int _pendingBlocks; + + /** Used to track visible blocks that are waiting to be resolved. */ + protected HashSet _visiBlocks = new HashSet(); + + /** Used to avoid repaints while we don't yet have resolved all the + * blocks needed to render the visible view. */ + protected boolean _delayRepaint = false; + + /** A list of the potentially visible objects in the scene. */ + protected ArrayList _vizobjs = new ArrayList(); + + /** For computing fringe tiles. */ + protected HashMap _masks = new HashMap(); + + /** The dirty sprites and objects that need to be re-painted. */ + protected DirtyItemList _dirtyItems = new DirtyItemList(); + + /** The working sprites list used when calculating dirty regions. */ + protected ArrayList _dirtySprites = new ArrayList(); + + /** Used when rendering tiles. */ + protected Rectangle _tbounds; + + /** Used to paint tiles. */ + protected PaintTileOp _paintOp = new PaintTileOp(); + + /** Temporary point used for intermediate calculations. */ + protected Point _tcoords = new Point(); + + /** Used to collect the list of sprites "hit" by a particular mouse + * location. */ + protected List _hitSprites = new ArrayList(); + + /** The list that we use to track and sort the items over which the + * mouse is hovering. */ + protected DirtyItemList _hitList = new DirtyItemList(); + + /** Info on the object that the mouse is currently hovering over. */ + protected Object _hobject; + + /** The item that the user has clicked on with the mouse. */ + protected Object _armedItem = null; + + /** The active radial menu (or null). */ + protected RadialMenu _activeMenu; + + /** Used to track the tile coordinates over which the mouse is hovering. */ + protected Point _hcoords = new Point(); + + /** Our object tips, indexed by the object that they tip for. */ + protected HashMap _tips = new HashMap(); + + /** Have the tips been laid out? */ + protected boolean _tipsLaidOut = false; + + /** Flags indicating which features we should show in the scene. */ + protected int _showFlags = 0; + + /** A scene block resolver shared by all scene panels. */ + protected static SceneBlockResolver _resolver; + + // used to display debugging information on scene block resolution + protected JFrame _dframe; + protected ResolutionView _dpanel; + + /** A debug hook that toggles debug rendering of traversable tiles. */ + protected static RuntimeAdjust.BooleanAdjust _traverseDebug = + new RuntimeAdjust.BooleanAdjust( + "Toggles debug rendering of traversable and impassable tiles in " + + "the iso scene view.", "narya.miso.iso_traverse_debug_render", + MisoPrefs.config, false); + + /** A debug hook that toggles debug rendering of tile coordinates. */ + protected static RuntimeAdjust.BooleanAdjust _coordsDebug = + new RuntimeAdjust.BooleanAdjust( + "Toggles debug rendering of tile coordinates in the iso scene " + + "view.", "narya.miso.iso_coords_debug_render", + MisoPrefs.config, false); + + /** A debug hook that toggles debug rendering of sprite paths. */ + protected static RuntimeAdjust.BooleanAdjust _pathsDebug = + new RuntimeAdjust.BooleanAdjust( + "Toggles debug rendering of sprite paths in the iso scene view.", + "narya.miso.iso_paths_debug_render", MisoPrefs.config, false); + + /** A debug hook that toggles the block resolution display. */ + protected static RuntimeAdjust.BooleanAdjust _resolveDebug = + new RuntimeAdjust.BooleanAdjust( + "Enables a view displaying the status of scene block resolution.", + "narya.miso.iso_paths_debug_resolve", MisoPrefs.config, false); + + /** The stroke used to draw dirty rectangles. */ + protected static final Stroke DIRTY_RECT_STROKE = new BasicStroke(2); + + /** The alpha used to fill tiles for debugging purposes. */ + protected static final Composite ALPHA_FILL_TILE = + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f); + + /** The default size of the "box" that defines the size of our radial + * menu circles. */ + protected static final Dimension DEF_RADIAL_RECT = new Dimension(80, 80); +} diff --git a/src/java/com/threerings/miso/client/ObjectActionHandler.java b/src/java/com/threerings/miso/client/ObjectActionHandler.java new file mode 100644 index 00000000..e8fea6df --- /dev/null +++ b/src/java/com/threerings/miso/client/ObjectActionHandler.java @@ -0,0 +1,151 @@ +// +// $Id: ObjectActionHandler.java 3917 2006-03-06 23:45:12Z mthomas $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.event.ActionEvent; +import java.util.HashMap; +import javax.swing.Icon; + +import com.samskivert.swing.RadialMenu; +import com.samskivert.util.StringUtil; + +import com.threerings.miso.Log; +import com.threerings.miso.client.SceneObject; + +/** + * Objects in scenes can be configured to generate action events. Those + * events are grouped into types and an object action handler can be + * registered to handle all actions of a particular type. + */ +public class ObjectActionHandler +{ + /** + * Returns true if we should allow this object action, false if we + * should not. + */ + public boolean actionAllowed (String action) + { + return true; + } + + /** + * Returns true if we should display the text for the action. By default + * this returns whether the action is allowed or not, but can be + * overridden by subclasses. This is used to completely hide actions that + * should not be visible without the proper privileges. + */ + public boolean isVisible (String action) + { + return actionAllowed(action); + } + + /** + * Get the human readable object tip for the specified action. + */ + public String getTipText (String action) + { + return action; + } + + /** + * Returns the tooltip icon for the specified action or null if the + * action has no tooltip icon. + */ + public Icon getTipIcon (String action) + { + return null; + } + + /** + * Return a {@link RadialMenu} or null if no menu needed. + */ + public RadialMenu handlePressed (SceneObject sourceObject) + { + return null; + } + + /** + * Called when an action is generated for an object. + */ + public void handleAction (SceneObject scobj, ActionEvent event) + { + Log.warning("Unknown object action [scobj=" + scobj + + ", action=" + event + "]."); + } + + /** + * Returns the type associated with this action command (which is + * mapped to a registered object action handler) or the empty string + * if it has no type. + */ + public static String getType (String command) + { + int cidx = StringUtil.isBlank(command) ? -1 : command.indexOf(':'); + return (cidx == -1) ? "" : command.substring(0, cidx); + } + + /** + * Returns the unqualified object action (minus the type, see {@link + * #getType}). + */ + public static String getAction (String command) + { + int cidx = StringUtil.isBlank(command) ? -1 : command.indexOf(':'); + return (cidx == -1) ? command : command.substring(cidx+1); + } + + /** + * Looks up the object action handler associated with the specified + * command. + */ + public static ObjectActionHandler lookup (String command) + { + return (ObjectActionHandler)_oahandlers.get(getType(command)); + } + + /** + * Registers an object action handler which will be called when a user + * clicks on an object in a scene that has an associated action. + */ + public static void register (String prefix, ObjectActionHandler handler) + { + // make sure we know about potential funny business + if (_oahandlers.containsKey(prefix)) { + Log.warning("Warning! Overwriting previous object action " + + "handler registration, all hell could shortly " + + "break loose [prefix=" + prefix + + ", handler=" + handler + "]."); + } + _oahandlers.put(prefix, handler); + } + + /** + * Removes an object action handler registration. + */ + public static void unregister (String prefix) + { + _oahandlers.remove(prefix); + } + + /** Our registered object action handlers. */ + protected static HashMap _oahandlers = new HashMap(); +} diff --git a/src/java/com/threerings/miso/client/ResolutionView.java b/src/java/com/threerings/miso/client/ResolutionView.java new file mode 100644 index 00000000..775b64af --- /dev/null +++ b/src/java/com/threerings/miso/client/ResolutionView.java @@ -0,0 +1,164 @@ +// +// $Id: ResolutionView.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Graphics; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; +import javax.swing.JPanel; + +import java.util.HashMap; +import java.util.Iterator; + +import com.samskivert.util.IntTuple; + +import com.threerings.media.util.MathUtil; +import com.threerings.miso.util.MisoSceneMetrics; +import com.threerings.miso.util.MisoUtil; + +/** + * Used to debug scene block resolution visually. + */ +public class ResolutionView extends JPanel +{ + public ResolutionView (MisoScenePanel panel) + { + _panel = panel; + _metrics = panel.getSceneMetrics(); + } + + public Dimension getPreferredSize () + { + return new Dimension(TILE_SIZE*MAX_WIDTH, TILE_SIZE*MAX_HEIGHT); + } + + public synchronized void queuedBlock (SceneBlock block) + { + assignColor(block, Color.yellow); + repaint(); + } + + public synchronized void resolvingBlock (SceneBlock block) + { + IntTuple key = blockKey(block); + if (_blocks.containsKey(key)) { + assignColor(block, Color.red); + repaint(); + } + } + + public synchronized void resolvedBlock (SceneBlock block) + { + IntTuple key = blockKey(block); + if (_blocks.containsKey(key)) { + assignColor(block, Color.green); + repaint(); + } + } + + public synchronized void blockCleared (SceneBlock block) + { + _blocks.remove(blockKey(block)); + repaint(); + } + + public synchronized void newScene () + { + _blocks.clear(); + repaint(); + } + + protected void assignColor (SceneBlock block, Color color) + { + IntTuple key = blockKey(block); + BlockGlyph glyph = (BlockGlyph)_blocks.get(key); + if (glyph == null) { + glyph = new BlockGlyph(_metrics, key.left, key.right); + _blocks.put(key, glyph); + } + glyph.color = color; + } + + public synchronized void paintComponent (Graphics g) + { + super.paintComponent(g); + Graphics2D gfx = (Graphics2D)g; + + Rectangle vbounds = _panel.getViewBounds(); + gfx.translate((getWidth()-vbounds.width/16)/2 - vbounds.x/16, + (getHeight()-vbounds.height/16)/2 - vbounds.y/16); + + AffineTransform xform = gfx.getTransform(); + gfx.scale(0.25, 0.25); + + // draw our block glyphs + for (Iterator iter = _blocks.values().iterator(); iter.hasNext(); ) { + ((BlockGlyph)iter.next()).paint(gfx); + } + + // draw the view bounds + gfx.scale(0.25, 0.25); + gfx.draw(vbounds); + gfx.setColor(Color.red); + gfx.draw(_panel.getInfluentialBounds()); + gfx.setTransform(xform); + } + + protected final IntTuple blockKey (SceneBlock block) + { + Rectangle bounds = block.getBounds(); + return new IntTuple(MathUtil.floorDiv(bounds.x, bounds.width), + MathUtil.floorDiv(bounds.y, bounds.height)); + } + + protected static class BlockGlyph + { + public Color color; + + public BlockGlyph (MisoSceneMetrics metrics, int bx, int by) + { + _bpoly = MisoUtil.getTilePolygon(metrics, bx, by); + } + + public void paint (Graphics2D gfx) + { + gfx.setColor(color); + gfx.fill(_bpoly); + gfx.setColor(Color.black); + gfx.draw(_bpoly); + } + + protected Polygon _bpoly; + } + + protected MisoScenePanel _panel; + protected MisoSceneMetrics _metrics; + protected HashMap _blocks = new HashMap(); + + protected static final int TILE_SIZE = 10; + protected static final int MAX_WIDTH = 30; + protected static final int MAX_HEIGHT = 30; +} diff --git a/src/java/com/threerings/miso/client/SceneBlock.java b/src/java/com/threerings/miso/client/SceneBlock.java new file mode 100644 index 00000000..97bcb906 --- /dev/null +++ b/src/java/com/threerings/miso/client/SceneBlock.java @@ -0,0 +1,596 @@ +// +// $Id: SceneBlock.java 3628 2005-06-28 17:42:55Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.Polygon; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; + +import com.samskivert.util.ArrayUtil; +import com.samskivert.util.HashIntMap; +import com.samskivert.util.StringUtil; + +import com.threerings.geom.GeomUtil; +import com.threerings.media.tile.NoSuchTileSetException; +import com.threerings.media.tile.ObjectTile; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileUtil; +import com.threerings.media.util.MathUtil; + +import com.threerings.miso.Log; +import com.threerings.miso.data.MisoSceneModel; +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.tile.BaseTile; +import com.threerings.miso.util.MisoUtil; +import com.threerings.miso.util.ObjectSet; + +/** + * Contains the base and object tile information on a particular + * rectangular region of a scene. + */ +public class SceneBlock +{ + /** + * Creates a scene block and resolves the base and object tiles that + * reside therein. + */ + public SceneBlock ( + MisoScenePanel panel, int tx, int ty, int width, int height) + { + _panel = panel; + _bounds = new Rectangle(tx, ty, width, height); + _base = new BaseTile[width*height]; + _fringe = new BaseTile[width*height]; + _covered = new boolean[width*height]; + + // compute our screen-coordinate footprint polygon + _footprint = MisoUtil.getFootprintPolygon( + panel.getSceneMetrics(), tx, ty, width, height); + + // the rest of our resolution will happen in resolve() + } + + /** + * Makes a note that this block was considered to be visible at the + * time it was created. This is purely for debugging purposes. + */ + public void setVisiBlock (boolean visi) + { + _visi = visi; + } + + /** + * This method is called by the {@link SceneBlockResolver} on the + * block resolution thread to allow us to load up our image data + * without blocking the AWT thread. + */ + protected boolean resolve () + { + // if we got canned before we were resolved, go ahead and bail now + if (_panel.getBlock(_bounds.x, _bounds.y) != this) { + // Log.info("Not resolving abandoned block " + this + "."); + _panel.blockAbandoned(this); + return false; + } + _panel.blockResolving(this); + + // start with the bounds of the footprint polygon + Rectangle sbounds = new Rectangle(_footprint.getBounds()); + Rectangle obounds = null; + + // resolve our base tiles + long now = System.currentTimeMillis(); + int baseCount = 0, fringeCount = 0; + MisoSceneModel model = _panel.getSceneModel(); + for (int yy = 0; yy < _bounds.height; yy++) { + for (int xx = 0; xx < _bounds.width; xx++) { + int x = _bounds.x + xx, y = _bounds.y + yy; + int fqTileId = model.getBaseTileId(x, y); + if (fqTileId <= 0) { + continue; + } + + // load up this base tile + updateBaseTile(fqTileId, x, y); + baseCount++; + + // if there's no tile here, we don't need no fringe + int tidx = index(x, y); + if (_base[tidx] == null) { + continue; + } + + // compute the fringe for this tile + _fringe[tidx] = _panel.computeFringeTile(x, y); + fringeCount++; + } + } + + // DEBUG: check for long resolution times + long stamp = System.currentTimeMillis(); + long elapsed = stamp - now; + if (elapsed > 500L) { + Log.warning("Base and fringe resolution took long time " + + "[block=" + this + ", baseCount=" + baseCount + + ", fringeCount=" + fringeCount + + ", elapsed=" + elapsed + "]."); + } + + // resolve our objects + ObjectSet set = new ObjectSet(); + model.getObjects(_bounds, set); + ArrayList scobjs = new ArrayList(); + now = System.currentTimeMillis(); + for (int ii = 0, ll = set.size(); ii < ll; ii++) { + SceneObject scobj = new SceneObject(_panel, set.get(ii)); + // ignore this object if it failed to resolve + if (scobj.bounds == null) { + continue; + } + sbounds.add(scobj.bounds); + obounds = GeomUtil.grow(obounds, scobj.bounds); + scobjs.add(scobj); + + // DEBUG: check for long resolution times + stamp = System.currentTimeMillis(); + elapsed = stamp - now; + now = stamp; + if (elapsed > 250L) { + Log.warning("Scene object took look time to resolve " + + "[block=" + this + ", scobj=" + scobj + + ", elapsed=" + elapsed + "]."); + } + } + _objects = (SceneObject[])scobjs.toArray( + new SceneObject[scobjs.size()]); + + // resolve our default tileset + int bsetid = model.getDefaultBaseTileSet(); + try { + if (bsetid > 0) { + _defset = _panel.getTileManager().getTileSet(bsetid); + } + } catch (Exception e) { + Log.warning("Unable to fetch default base tileset [tsid=" + bsetid + + ", error=" + e + "]."); + } + + // this both marks us as resolved and makes all our other updated + // fields visible + synchronized (this) { + _obounds = obounds; + _sbounds = sbounds; + } + + return true; + } + + /** + * This is called by the {@link SceneBlockResolver} on the AWT thread + * when our resolution has completed. We inform our containing panel. + */ + protected void wasResolved () + { + _panel.blockResolved(this); + } + + /** + * Returns true if this block has been resolved, false if not. + */ + protected synchronized boolean isResolved () + { + return _sbounds != null; + } + + /** + * Returns the bounds of this block, in tile coordinates. + */ + public Rectangle getBounds () + { + return _bounds; + } + + /** + * Returns the bounds of the screen coordinate rectangle that contains + * all pixels that are drawn on by all tiles and objects in this + * block. + */ + public Rectangle getScreenBounds () + { + return _sbounds; + } + + /** + * Returns the bounds of the screen coordinate rectangle that contains + * all pixels that are drawn on by all objects (but not base tiles) in + * this block. Note: this will return null if + * the block has no objects. + */ + public Rectangle getObjectBounds () + { + return _obounds; + } + + /** + * Returns the screen-coordinate polygon bounding the footprint of + * this block. + */ + public Polygon getFootprint () + { + return _footprint; + } + + /** + * Returns an array of all resolved scene objects in this block. + */ + public SceneObject[] getObjects () + { + return _objects; + } + + /** + * Returns the base tile at the specified coordinates or null if + * there's no tile at said coordinates. + */ + public BaseTile getBaseTile (int tx, int ty) + { + BaseTile tile = _base[index(tx, ty)]; + if (tile == null && _defset != null) { + tile = (BaseTile)_defset.getTile( + TileUtil.getTileHash(tx, ty) % _defset.getTileCount()); + } + return tile; + } + + /** + * Returns the fringe tile at the specified coordinates or null if + * there's no tile at said coordinates. + */ + public BaseTile getFringeTile (int tx, int ty) + { + return _fringe[index(tx, ty)]; + } + + /** + * Informs this scene block that the specified base tile has been + * changed. + */ + public void updateBaseTile (int fqTileId, int tx, int ty) + { + String errmsg = null; + int tidx = index(tx, ty); + + // this is a bit magical: we pass the fully qualified tile id to + // the tile manager which loads up from the configured tileset + // repository the appropriate tileset (which should be a + // BaseTileSet) and then extracts the appropriate base tile (the + // index of which is also in the fqTileId) + try { + if (fqTileId <= 0) { + _base[tidx] = null; + } else { + _base[tidx] = (BaseTile) + _panel.getTileManager().getTile(fqTileId); + } + // clear out the fringe (it must be recomputed by the caller) + _fringe[tidx] = null; + + } catch (ClassCastException cce) { + errmsg = "Scene contains non-base tile in base layer"; + } catch (NoSuchTileSetException nste) { + errmsg = "Scene contains non-existent tileset"; + } + + if (errmsg != null) { + Log.warning(errmsg + " [fqtid=" + fqTileId + + ", x=" + tx + ", y=" + ty + "]."); + } + } + + /** + * Instructs this block to recompute its fringe at the specified + * location. + */ + public void updateFringe (int tx, int ty) + { + int tidx = index(tx, ty); + if (_base[tidx] != null) { + _fringe[tidx] = _panel.computeFringeTile(tx, ty); + } + } + + /** + * Adds the supplied object to this block. Coverage is not computed + * for the added object, a subsequent call to {@link #update} will be + * needed. + * + * @return true if the object was added, false if it was not because + * another object of the same type already occupies that location. + */ + public boolean addObject (ObjectInfo info) + { + // make sure we don't already have this same object at these + // coordinates + for (int ii = 0; ii < _objects.length; ii++) { + if (_objects[ii].info.equals(info)) { + return false; + } + } + + _objects = (SceneObject[]) + ArrayUtil.append(_objects, new SceneObject(_panel, info)); + + // clear out our neighbors array so that the subsequent update + // causes us to recompute our coverage + Arrays.fill(_neighbors, null); + return true; + } + + /** + * Removes the specified object from this block. Coverage is not + * recomputed, so a subsequent call to {@link #update} will be needed. + * + * @return true if the object was deleted, false if it was not found + * in our object list. + */ + public boolean deleteObject (ObjectInfo info) + { + int oidx = -1; + for (int ii = 0; ii < _objects.length; ii++) { + if (_objects[ii].info.equals(info)) { + oidx = ii; + break; + } + } + if (oidx == -1) { + return false; + } + _objects = (SceneObject[])ArrayUtil.splice(_objects, oidx, 1); + + // clear out our neighbors array so that the subsequent update + // causes us to recompute our coverage + Arrays.fill(_neighbors, null); + return true; + } + + /** + * Returns true if the specified traverser can traverse the specified + * tile (which is assumed to be in the bounds of this scene block). + */ + public boolean canTraverse (Object traverser, int tx, int ty) + { + if (_covered[index(tx, ty)]) { + return false; + } + + // null base or impassable base kills traversal + BaseTile base = getBaseTile(tx, ty); + if ((base == null) || !base.isPassable()) { + return false; + } + + // fringe can only kill traversal if it is present + BaseTile fringe = getFringeTile(tx, ty); + return (fringe == null) || fringe.isPassable(); + } + + /** + * Computes the memory usage of the base and object tiles in this + * scene block; registering counted tiles in the hash map so that + * other blocks can be sure not to double count them. Base tile usage + * is placed into the zeroth array element, fringe tile usage into the + * first and object tile usage into the second. + */ + public void computeMemoryUsage ( + HashMap bases, HashSet fringes, HashMap objects, long[] usage) + { + // account for our base tiles + MisoSceneModel model = _panel.getSceneModel(); + for (int yy = 0; yy < _bounds.height; yy++) { + for (int xx = 0; xx < _bounds.width; xx++) { + int x = _bounds.x + xx, y = _bounds.y + yy; + int tidx = index(x, y); + BaseTile base = _base[tidx]; + if (base == null) { + continue; + } + + BaseTile sbase = (BaseTile)bases.get(base.key); + if (sbase == null) { + bases.put(base.key, base); + usage[0] += base.getEstimatedMemoryUsage(); + } else if (base != _base[tidx]) { + Log.warning("Multiple instances of same base tile " + + "[base=" + base + + ", x=" + xx + ", y=" + yy + "]."); + usage[0] += base.getEstimatedMemoryUsage(); + } + + // now account for the fringe + if (_fringe[tidx] == null) { + continue; + } else if (!fringes.contains(_fringe[tidx])) { + fringes.add(_fringe[tidx]); + usage[1] += _fringe[tidx].getEstimatedMemoryUsage(); + } + } + } + + // now get the object tiles + int ocount = (_objects == null) ? 0 : _objects.length; + for (int ii = 0; ii < ocount; ii++) { + SceneObject scobj = _objects[ii]; + ObjectTile tile = (ObjectTile)objects.get(scobj.tile.key); + if (tile == null) { + objects.put(scobj.tile.key, scobj.tile); + usage[2] += scobj.tile.getEstimatedMemoryUsage(); + } else if (tile != scobj.tile) { + Log.warning("Multiple instances of same object tile: " + + scobj.info + "."); + usage[2] += scobj.tile.getEstimatedMemoryUsage(); + } + } + } + + /** + * Returns a string representation of this instance. + */ + public String toString () + { + int bx = MathUtil.floorDiv(_bounds.x, _bounds.width); + int by = MathUtil.floorDiv(_bounds.y, _bounds.height); + return StringUtil.coordsToString(bx, by) + ":" + + StringUtil.toString(_bounds) + ":" + + ((_objects == null) ? 0 : _objects.length) + + (_visi ? ":v" : ":i"); + } + + /** + * Returns the index into our arrays of the specified tile. + */ + protected final int index (int tx, int ty) + { +// if (!_bounds.contains(tx, ty)) { +// String errmsg = "Coordinates out of bounds: +" + tx + "+" + ty + +// " not in " + StringUtil.toString(_bounds); +// throw new IllegalArgumentException(errmsg); +// } + return (ty-_bounds.y)*_bounds.width + (tx-_bounds.x); + } + + /** + * Links this block to its neighbors; informs neighboring blocks of + * object coverage. + */ + protected void update (HashIntMap blocks) + { + boolean recover = false; + + // link up to our neighbors + for (int ii = 0; ii < DX.length; ii++) { + SceneBlock neigh = (SceneBlock) + blocks.get(neighborKey(DX[ii], DY[ii])); + if (neigh != _neighbors[ii]) { + _neighbors[ii] = neigh; + // if we're linking up to a neighbor for the first time; + // we need to recalculate our coverage + recover = recover || (neigh != null); +// Log.info(this + " was introduced to " + neigh + "."); + } + } + + // if we need to regenerate the set of tiles covered by our + // objects, do so + if (recover) { + for (int ii = 0; ii < _objects.length; ii++) { + setCovered(blocks, _objects[ii]); + } + } + } + + /** Computes the key of our neighbor. */ + protected final int neighborKey (int dx, int dy) + { + int nx = MathUtil.floorDiv(_bounds.x, _bounds.width)+dx; + int ny = MathUtil.floorDiv(_bounds.y, _bounds.height)+dy; + return MisoScenePanel.compose(nx, ny); + } + + /** Computes the key for the block that holds the specified tile. */ + protected final int blockKey (int tx, int ty) + { + int bx = MathUtil.floorDiv(tx, _bounds.width); + int by = MathUtil.floorDiv(ty, _bounds.height); + return MisoScenePanel.compose(bx, by); + } + + /** + * Sets the footprint of this object tile + */ + protected void setCovered (HashIntMap blocks, SceneObject scobj) + { + int endx = scobj.info.x - scobj.tile.getBaseWidth() + 1; + int endy = scobj.info.y - scobj.tile.getBaseHeight() + 1; + + for (int xx = scobj.info.x; xx >= endx; xx--) { + for (int yy = scobj.info.y; yy >= endy; yy--) { + SceneBlock block = (SceneBlock)blocks.get(blockKey(xx, yy)); + if (block != null) { + block.setCovered(xx, yy); + } + } + } + +// Log.info("Updated coverage " + scobj.info + "."); + } + + /** + * Indicates that this tile is covered by an object footprint. + */ + protected void setCovered (int tx, int ty) + { + _covered[index(tx, ty)] = true; + } + + /** The panel for which we contain a block. */ + protected MisoScenePanel _panel; + + /** The bounds of (in tile coordinates) of this block. */ + protected Rectangle _bounds; + + /** The bounds (in screen coords) of all images rendered by this block. */ + protected Rectangle _sbounds; + + /** The bounds (in screen coords) of all objects rendered by this block. */ + protected Rectangle _obounds; + + /** A polygon bounding the footprint of this block. */ + protected Polygon _footprint; + + /** Used to return a tile where we have none. */ + protected TileSet _defset; + + /** Our base tiles. */ + protected BaseTile[] _base; + + /** Our fringe tiles. */ + protected BaseTile[] _fringe; + + /** Indicates whether our tiles are covered by an object. */ + protected boolean[] _covered; + + /** Info on our objects. */ + protected SceneObject[] _objects; + + /** Our neighbors in the eight cardinal directions. */ + protected SceneBlock[] _neighbors = new SceneBlock[DX.length]; + + /** A debug flag indicating whether we were visible at creation. */ + protected boolean _visi; + + // used to link up to our neighbors + protected static final int[] DX = { -1, -1, 0, 1, 1, 1, 0, -1 }; + protected static final int[] DY = { 0, -1, -1, -1, 0, 1, 1, 1 }; +} diff --git a/src/java/com/threerings/miso/client/SceneBlockResolver.java b/src/java/com/threerings/miso/client/SceneBlockResolver.java new file mode 100644 index 00000000..cb1aee0e --- /dev/null +++ b/src/java/com/threerings/miso/client/SceneBlockResolver.java @@ -0,0 +1,138 @@ +// +// $Id: SceneBlockResolver.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.EventQueue; + +import com.samskivert.util.Histogram; +import com.samskivert.util.LoopingThread; +import com.samskivert.util.Queue; + +import com.threerings.miso.Log; + +/** + * A separate thread for resolving miso scene blocks. + */ +public class SceneBlockResolver extends LoopingThread +{ + /** + * Queues up a scene block for resolution. + */ + public void resolveBlock (SceneBlock block, boolean hipri) + { + Log.debug("Queueing block for resolution " + block + + " (" + hipri + ")."); + if (hipri) { + _queue.prepend(block); + } else { + _queue.append(block); + } + } + + /** + * Temporarily suspends the scene block resolution thread. + */ + public synchronized void suspendResolution () + { + _resolving = false; + } + + /** + * Restores the operation of the scene block resolution thread after a + * previous call to {@link #suspendResolution}. + */ + public synchronized void restoreResolution () + { + _resolving = true; + notify(); + } + + /** + * Returns the number of scene blocks on the resolution queue. + */ + public int queueSize () + { + return _queue.size(); + } + + // documentation inherited + public void iterate () + { + final SceneBlock block = (SceneBlock)_queue.get(); + + while (!_resolving) { + synchronized (this) { + try { + wait(); + } catch (InterruptedException ie) { + Log.info("Resolver interrupted."); + } + } + } + + try { + long start = System.currentTimeMillis(); + Log.debug("Resolving block " + block + "."); + if (block.resolve()) { + Log.debug("Resolved block " + block + "."); + } + long elapsed = System.currentTimeMillis() - start; + _histo.addValue((int)elapsed); + + // warn if a block takes a long time to resolve + if (elapsed > LONG_RESOLVE_TIME) { + Log.warning("Block took long time to resolve [block=" + block + + ", elapsed=" + elapsed + "ms]."); + } + + // queue it up on the AWT thread to complete its resolution + final boolean report = (_queue.size() == 0); + EventQueue.invokeLater(new Runnable() { + public void run () { + // let the block's panel know that it is resolved + block.wasResolved(); + // report statistics +// if (report) { +// Log.info("Resolution histogram " + +// _histo.summarize() + "."); +// } + } + }); + + } catch (Exception e) { + Log.warning("Block failed during resolution " + block + "."); + Log.logStackTrace(e); + } + } + + /** The invoker's queue of units to be executed. */ + protected Queue _queue = new Queue(); + + /** Indicates whether or not we are resolving or suspended. */ + protected boolean _resolving = true; + + /** Used to time block loading. */ + protected Histogram _histo = new Histogram(0, 25, 100); + + /** Blocks shouldn't take too long to resolve. */ + protected static final long LONG_RESOLVE_TIME = 500L; +} diff --git a/src/java/com/threerings/miso/client/SceneObject.java b/src/java/com/threerings/miso/client/SceneObject.java new file mode 100644 index 00000000..b17df38e --- /dev/null +++ b/src/java/com/threerings/miso/client/SceneObject.java @@ -0,0 +1,358 @@ +// +// $Id: SceneObject.java 4007 2006-04-10 08:59:30Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Polygon; +import java.awt.Rectangle; + +import com.samskivert.swing.RuntimeAdjust; +import com.samskivert.util.StringUtil; + +import com.threerings.media.tile.NoSuchTileSetException; +import com.threerings.media.tile.ObjectTile; +import com.threerings.media.tile.TileUtil; + +import com.threerings.miso.Log; +import com.threerings.miso.MisoPrefs; +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.util.MisoSceneMetrics; +import com.threerings.miso.util.MisoUtil; + +/** + * Contains resolved information on an object in a scene. + */ +public class SceneObject +{ + /** The object's info record. */ + public ObjectInfo info; + + /** The object tile used to display this object. */ + public ObjectTile tile; + + /** The screen coordinate bounds of our object tile given its position + * in the scene. */ + public Rectangle bounds; + + /** + * Creates a scene object for display by the specified panel. The + * appropriate object tile is resolved and the object's in-situ bounds + * are computed. + */ + public SceneObject (MisoScenePanel panel, ObjectInfo info) + { + this.info = info; + + // resolve our object tile + refreshObjectTile(panel); + } + + /** + * Creates a scene object for display by the specified panel. + */ + public SceneObject (MisoScenePanel panel, ObjectInfo info, ObjectTile tile) + { + this(panel.getSceneMetrics(), info, tile); + } + + /** + * Creates a scene object for display according to the supplied metrics. + */ + public SceneObject ( + MisoSceneMetrics metrics, ObjectInfo info, ObjectTile tile) + { + this.info = info; + this.tile = tile; + computeInfo(metrics); + } + + /** + * Used to flag overlapping scene objects that have no resolving + * object priorities. + */ + public void setWarning (boolean warning) + { + _warning = warning; + } + + /** + * Requests that this scene object render itself. + */ + public void paint (Graphics2D gfx) + { + if (_hideObjects.getValue()) { + return; + } + + // if we're rendering footprints, paint that + boolean footpaint = _fprintDebug.getValue(); + if (footpaint) { + gfx.setColor(Color.black); + gfx.draw(_footprint); + } + + // if we have a warning, render an alpha'd red rectangle over our + // bounds + if (_warning) { + Composite ocomp = gfx.getComposite(); + gfx.setComposite(ALPHA_WARN); + gfx.fill(bounds); + gfx.setComposite(ocomp); + } + + // paint our tile + tile.paint(gfx, bounds.x, bounds.y); + + // and possibly paint the object's spot + if (footpaint && _sspot != null) { + // translate the origin to center on the portal + gfx.translate(_sspot.x, _sspot.y); + + // rotate to reflect the spot orientation + double rot = (Math.PI / 4.0f) * tile.getSpotOrient(); + gfx.rotate(rot); + + // draw the spot triangle + gfx.setColor(Color.green); + gfx.fill(_spotTri); + + // outline the triangle in black + gfx.setColor(Color.black); + gfx.draw(_spotTri); + + // restore the original transform + gfx.rotate(-rot); + gfx.translate(-_sspot.x, -_sspot.y); + } + } + + /** + * Returns the location associated with this object's "spot" in fine + * coordinates or null if it has no spot. + */ + public Point getObjectSpot () + { + return _fspot; + } + + /** + * Returns the location associated with this object's "spot" in screen + * coordinates or null if it has no spot. + */ + public Point getObjectScreenSpot () + { + return _sspot; + } + + /** + * Returns true if this object's footprint overlaps that of the + * specified other object. + */ + public boolean objectFootprintOverlaps (SceneObject so) + { + return _frect.intersects(so._frect); + } + + /** + * Returns true if this object's footprint overlaps the supplied tile + * coordinate rectangle. + */ + public boolean objectFootprintOverlaps (Rectangle rect) + { + return _frect.intersects(rect); + } + + /** + * Returns a polygon bounding all footprint tiles of this scene + * object. + * + * @return the bounding polygon. + */ + public Polygon getObjectFootprint () + { + return _footprint; + } + + /** + * Returns the render priority of this scene object. + */ + public int getPriority () + { + // if we have no overridden priority, return our object tile's + // default priority + return (info.priority == 0 && tile != null) ? + tile.getPriority() : info.priority; + } + + /** + * Overrides the render priority of this object. + */ + public void setPriority (byte priority) + { + info.priority = (byte)Math.max( + Byte.MIN_VALUE, Math.min(Byte.MAX_VALUE, priority)); + } + + /** + * Informs this scene object that the mouse is now hovering over it. + * Custom objects may wish to adjust some internal state and return + * true from this method indicating that they should be repainted. + */ + public boolean setHovered (boolean hovered) + { + return false; + } + + /** + * Updates this object's origin tile coordinate. It's bounds and other + * cached screen coordinate information are updated. + */ + public void relocateObject (MisoSceneMetrics metrics, int tx, int ty) + { +// Log.info("Relocating object " + this + " to " + +// StringUtil.coordsToString(tx, ty)); + info.x = tx; + info.y = ty; + computeInfo(metrics); + } + + /** + * Reloads and recolorizes our object tile. It is not intended for the + * actual object tile used by a scene object to change in its + * lifetime, only attributes of that object like its colorizations. So + * don't do anything crazy like change our {@link ObjectInfo}'s + * tileId and call this method or things might break. + */ + public void refreshObjectTile (MisoScenePanel panel) + { + int tsid = TileUtil.getTileSetId(info.tileId); + int tidx = TileUtil.getTileIndex(info.tileId); + try { + tile = (ObjectTile)panel.getTileManager().getTile( + tsid, tidx, panel.getColorizer(info)); + computeInfo(panel.getSceneMetrics()); + + } catch (NoSuchTileSetException te) { + Log.warning("Scene contains non-existent object tileset " + + "[info=" + info + "]."); + } + } + + /** + * Computes our screen bounds, tile footprint and other useful cached + * metrics. + */ + protected void computeInfo (MisoSceneMetrics metrics) + { + // start with the screen coordinates of our origin tile + Point tpos = MisoUtil.tileToScreen( + metrics, info.x, info.y, new Point()); + + // if the tile has an origin coordinate, use that, otherwise + // compute it from the tile footprint + int tox = tile.getOriginX(), toy = tile.getOriginY(); + if (tox == Integer.MIN_VALUE) { + tox = tile.getBaseWidth() * metrics.tilehwid; + } + if (toy == Integer.MIN_VALUE) { + toy = tile.getHeight(); + } + + bounds = new Rectangle(tpos.x + metrics.tilehwid - tox, + tpos.y + metrics.tilehei - toy, + tile.getWidth(), tile.getHeight()); + + // compute our object footprint as well + _frect = new Rectangle(info.x - tile.getBaseWidth() + 1, + info.y - tile.getBaseHeight() + 1, + tile.getBaseWidth(), tile.getBaseHeight()); + _footprint = MisoUtil.getFootprintPolygon( + metrics, _frect.x, _frect.y, _frect.width, _frect.height); + + // compute our object spot if we've got one + if (tile.hasSpot()) { + _fspot = MisoUtil.tilePlusFineToFull( + metrics, info.x, info.y, tile.getSpotX(), tile.getSpotY(), + new Point()); + _sspot = MisoUtil.fullToScreen( + metrics, _fspot.x, _fspot.y, new Point()); + } + +// Log.info("Computed object metrics " + +// "[tpos=" + StringUtil.coordsToString(tx, ty) + +// ", info=" + info + +// ", sbounds=" + StringUtil.toString(bounds) + "]."); + } + + /** + * Returns a string representation of this instance. + */ + public String toString () + { + return info + "[" + StringUtil.toString(bounds) + "]"; + } + + /** Our object as a tile coordinate rectangle. */ + protected Rectangle _frect; + + /** Our object footprint as a polygon. */ + protected Polygon _footprint; + + /** The full-coordinates of our object spot; or null if we have none. */ + protected Point _fspot; + + /** The screen-coordinates of our object spot; or null if we have none. */ + protected Point _sspot; + + /** Used to mark objects with errors. */ + protected boolean _warning; + + /** A debug hook that toggles rendering of objects. */ + protected static RuntimeAdjust.BooleanAdjust _hideObjects = + new RuntimeAdjust.BooleanAdjust( + "Toggles rendering of objects in the scene view.", + "narya.miso.hide_objects", MisoPrefs.config, false); + + /** A debug hook that toggles rendering of object footprints. */ + protected static RuntimeAdjust.BooleanAdjust _fprintDebug = + new RuntimeAdjust.BooleanAdjust( + "Toggles rendering of object footprints in the scene view.", + "narya.miso.iso_fprint_debug_render", MisoPrefs.config, false); + + /** The triangle used to render an object's spot. */ + protected static Polygon _spotTri; + + static { + _spotTri = new Polygon(); + _spotTri.addPoint(-3, -3); + _spotTri.addPoint(3, -3); + _spotTri.addPoint(0, 3); + }; + + /** The alpha used to fill our bounds for warning purposes. */ + protected static final Composite ALPHA_WARN = + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f); +} diff --git a/src/java/com/threerings/miso/client/SceneObjectActionEvent.java b/src/java/com/threerings/miso/client/SceneObjectActionEvent.java new file mode 100644 index 00000000..bd759a52 --- /dev/null +++ b/src/java/com/threerings/miso/client/SceneObjectActionEvent.java @@ -0,0 +1,48 @@ +// +// $Id: SceneObjectActionEvent.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.event.ActionEvent; + +/** + * An {@link ActionEvent} derivation that is fired when a scene object is + * clicked or menu item selected. + */ +public class SceneObjectActionEvent extends ActionEvent +{ + public SceneObjectActionEvent (Object source, int id, String action, + int modifiers, SceneObject scobj) + { + super(source, id, action, modifiers); + _scobj = scobj; + } + + /** + * Returns the scene object that was the source of this action. + */ + public SceneObject getSceneObject () + { + return _scobj; + } + + protected SceneObject _scobj; +} diff --git a/src/java/com/threerings/miso/client/SceneObjectTip.java b/src/java/com/threerings/miso/client/SceneObjectTip.java new file mode 100644 index 00000000..c394e895 --- /dev/null +++ b/src/java/com/threerings/miso/client/SceneObjectTip.java @@ -0,0 +1,206 @@ +// +// $Id: SceneObjectTip.java 4007 2006-04-10 08:59:30Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import java.util.Collection; + +import javax.swing.Icon; +import javax.swing.UIManager; + +import com.samskivert.swing.Label; +import com.samskivert.swing.LabelSausage; +import com.samskivert.util.ComparableArrayList; +import com.samskivert.util.StringUtil; + +/** + * A lightweight tooltip used by the {@link MisoScenePanel}. The tip + * foreground and background are controlled by the following {@link + * UIManager} properties: + * + *

+ * SceneObjectTip.background
+ * SceneObjectTip.foreground
+ * SceneObjectTip.font (falls back to Label.font)
+ * 
+ */ +public class SceneObjectTip extends LabelSausage +{ + /** + * Used to position a scene tip in relation to the object with which + * it is associated. + */ + public static interface TipLayout + { + /** + * Position the supplied tip relative to the supplied scene object. + */ + public void layout (Graphics2D gfx, Rectangle boundary, + SceneObject tipFor, SceneObjectTip tip); + } + + /** The bounding box of this tip, or null prior to layout(). */ + public Rectangle bounds; + + /** + * Construct a SceneObjectTip. + */ + public SceneObjectTip (String text, Icon icon) + { + super(new Label(text, _foreground, _font), icon); + } + + /** + * Called to initialize the tip so that it can be painted. + * + * @param tipFor the scene object that we're a tip for. + * @param boundary the boundary of all displayable space. + * @param othertips other tip boundaries that we should avoid. + */ + public void layout (Graphics2D gfx, SceneObject tipFor, Rectangle boundary, + Collection othertips) + { + layout(gfx, ICON_PAD, EXTRA_PAD); + bounds = new Rectangle(_size); + + // locate the most appropriate tip layout + for (int ii = 0, ll = _layouts.size(); ii < ll; ii++) { + LayoutReg reg = (LayoutReg)_layouts.get(ii); + String act = tipFor.info.action == null ? "" : tipFor.info.action; + if (act.startsWith(reg.prefix)) { + reg.layout.layout(gfx, boundary, tipFor, this); + break; + } + } + } + + /** + * Paint this tip at it's location. + */ + public void paint (Graphics2D gfx) + { + paint(gfx, bounds.x, bounds.y, _background, null); + } + + /** + * Generates a string representation of this instance. + */ + public String toString () + { + return _label.getText() + "[" + StringUtil.toString(bounds) + "]"; + } + + /** + * It may be desirable to layout object tips specially depending on + * what sort of actions they represent, so we allow different tip + * layout algorithms to be registered for particular object prefixes. + * The registration is simply a list searched from longest string to + * shortest string for the first match to an object's action. + */ + public static void registerTipLayout (String prefix, TipLayout layout) + { + LayoutReg reg = new LayoutReg(); + reg.prefix = prefix; + reg.layout = layout; + _layouts.insertSorted(reg); + } + + // documentation inherited + protected void drawBase (Graphics2D gfx, int x, int y) + { + Composite ocomp = gfx.getComposite(); + gfx.setComposite(ALPHA); + super.drawBase(gfx, x, y); + gfx.setComposite(ocomp); + } + + /** The alpha we use for our base. */ + protected static final Composite ALPHA = AlphaComposite.getInstance( + AlphaComposite.SRC_OVER, .75f); + + /** Colors to use when rendering the tip. */ + protected static Color _background, _foreground; + + /** The font to use when rendering the tip. */ + protected static Font _font; + + // initialize resources shared by all tips + static { + _background = UIManager.getColor("SceneObjectTip.background"); + _foreground = UIManager.getColor("SceneObjectTip.foreground"); + _font = UIManager.getFont("SceneObjectTip.font"); + if (_font == null) { + _font = UIManager.getFont("Label.font"); + } + } + + /** Used to store {@link TipLayout} registrations. */ + protected static class LayoutReg implements Comparable + { + /** The prefix that defines our applicability. */ + public String prefix; + + /** The layout to use for objects matching our prefix. */ + public TipLayout layout; + + // documentation inherited from interface + public int compareTo (Object o) { + LayoutReg or = (LayoutReg)o; + if (or.prefix.length() == prefix.length()) { + return or.prefix.compareTo(prefix); + } else { + return or.prefix.length() - prefix.length(); + } + } + } + + /** Our default tip layout algorithm which centers the tip in the + * bounds of the object in question. */ + protected static class DefaultLayout implements TipLayout + { + public void layout (Graphics2D gfx, Rectangle boundary, + SceneObject tipFor, SceneObjectTip tip) { + tip.bounds.setLocation( + tipFor.bounds.x + (tipFor.bounds.width-tip.bounds.width) / 2, + tipFor.bounds.y + (tipFor.bounds.height-tip.bounds.height) / 2); + } + } + + /** Contains a sorted list of layout registrations. */ + protected static ComparableArrayList _layouts = new ComparableArrayList(); + + /** The number of pixels to pad around the icon. */ + protected static final int ICON_PAD = 4; + + /** The number of pixels to pad between the icon and text. */ + protected static final int EXTRA_PAD = 2; + + static { + registerTipLayout("", new DefaultLayout()); + } +} diff --git a/src/java/com/threerings/miso/client/TilePath.java b/src/java/com/threerings/miso/client/TilePath.java new file mode 100644 index 00000000..78cced6e --- /dev/null +++ b/src/java/com/threerings/miso/client/TilePath.java @@ -0,0 +1,144 @@ +// +// $Id: TilePath.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.Point; +import java.util.List; + +import com.threerings.util.DirectionCodes; + +import com.threerings.media.sprite.Sprite; +import com.threerings.media.util.LineSegmentPath; +import com.threerings.media.util.MathUtil; + +import com.threerings.miso.util.MisoSceneMetrics; +import com.threerings.miso.util.MisoUtil; + +/** + * The tile path represents a path of tiles through a scene. The path is + * traversed by treating each pair of connected tiles as a line segment. + * Only ambulatory sprites can follow a tile path, and their tile + * coordinates are updated as the path is traversed. + */ +public class TilePath extends LineSegmentPath + implements DirectionCodes +{ + /** + * Constructs a tile path. + * + * @param metrics the metrics for the scene the with which the path is + * associated. + * @param sprite the sprite to follow the path. + * @param tiles the tiles to be traversed during the path. + * @param destx the destination x-position in screen pixel + * coordinates. + * @param desty the destination y-position in screen pixel + * coordinates. + */ + public TilePath (MisoSceneMetrics metrics, Sprite sprite, + List tiles, int destx, int desty) + { + // constrain destination pixels to fine coordinates + Point fpos = new Point(); + MisoUtil.screenToFull(metrics, destx, desty, fpos); + + // add the starting path node + int sx = sprite.getX(), sy = sprite.getY(); + Point ipos = MisoUtil.screenToTile(metrics, sx, sy, new Point()); + addNode(sx, sy, NORTH); + + // TODO: make more visually appealing path segments from start to + // second tile, and penultimate to ultimate tile + + // add all remaining path nodes excepting the last one + Point prev = new Point(ipos.x, ipos.y); + Point spos = new Point(); + int size = tiles.size(); + for (int ii = 1; ii < size - 1; ii++) { + Point next = (Point)tiles.get(ii); + + // determine the direction from previous to next node + int dir = MisoUtil.getIsoDirection(prev.x, prev.y, next.x, next.y); + + // determine the node's position in screen pixel coordinates + MisoUtil.tileToScreen(metrics, next.x, next.y, spos); + + // add the node to the path, wandering through the middle + // of each tile in the path for now + int dsx = spos.x + metrics.tilehwid; + int dsy = spos.y + metrics.tilehhei; + addNode(dsx, dsy, dir); + + prev = next; + } + + // get the final destination point's screen coordinates + // constrained to the closest full coordinate + MisoUtil.fullToScreen(metrics, fpos.x, fpos.y, spos); + + // get the tile coordinates for the final destination tile + int tdestx = MisoUtil.fullToTile(fpos.x); + int tdesty = MisoUtil.fullToTile(fpos.y); + + // get the facing direction for the final node + int dir; + if (prev.x == ipos.x && prev.y == ipos.y) { + // if destination is within starting tile, direction is + // determined by studying the fine coordinates + dir = MisoUtil.getDirection(metrics, sx, sy, spos.x, spos.y); + + } else { + // else it's based on the last tile we traversed + dir = MisoUtil.getIsoDirection(prev.x, prev.y, tdestx, tdesty); + } + + // add the final destination path node + addNode(spos.x, spos.y, dir); + } + + /** + * Returns the estimated number of millis that we'll be traveling + * along this path. + */ + public long getEstimTravelTime () + { + return (long)(_estimPixels / _vel); + } + + // documentation inherited + public void addNode (int x, int y, int dir) + { + super.addNode(x, y, dir); + if (_last == null) { + _last = new Point(); + } else { + _estimPixels += MathUtil.distance(_last.x, _last.y, x, y); + } + _last.setLocation(x, y); + } + + /** Used to compute estimated travel time. */ + protected Point _last; + + /** Estimated pixels traveled. */ + protected int _estimPixels; +} diff --git a/src/java/com/threerings/miso/data/MisoSceneModel.java b/src/java/com/threerings/miso/data/MisoSceneModel.java new file mode 100644 index 00000000..2a6422ff --- /dev/null +++ b/src/java/com/threerings/miso/data/MisoSceneModel.java @@ -0,0 +1,129 @@ +// +// $Id: MisoSceneModel.java 3726 2005-10-11 19:17:43Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.data; + +import java.awt.Rectangle; +import java.util.Random; + +import com.threerings.io.SimpleStreamableObject; + +import com.threerings.miso.util.ObjectSet; + +/** + * Contains basic information for a miso scene model that is shared among + * the specialized model implementations. + */ +public abstract class MisoSceneModel extends SimpleStreamableObject + implements Cloneable +{ + /** + * Creates a completely uninitialized model suitable for little more + * than unserialization. + */ + public MisoSceneModel () + { + } + + /** + * Returns the fully qualified tile id of the base tile at the + * specified coordinates. -1 will be returned if there is + * no tile at the specified coordinate. + */ + public abstract int getBaseTileId (int x, int y); + + /** + * Updates the tile at the specified location in the base layer. + * + *

Note that if there are fringe tiles associated with this scene, + * calling this method may result in the surrounding fringe tiles + * being cleared and subsequently recalculated. This should not be + * called on a displaying scene unless you know what you are doing. + * + * @param fqTileId the fully-qualified tile id (@see + * TileUtil#getFQTileId}) of the tile to set. + * @param x the x-coordinate of the tile to set. + * @param y the y-coordinate of the tile to set. + * + * @return false if the specified tile coordinates are outside of the + * scene and the tile was not saved, true otherwise. + */ + public abstract boolean setBaseTile (int fqTileId, int x, int y); + + /** + * Updates the default base tileset id for this scene. + */ + public void setDefaultBaseTileSet (int tileSetId) + { + // nothing doing + } + + /** + * Scene models can return a default tileset to be used when no base + * tile data exists for a particular tile. + */ + public int getDefaultBaseTileSet () + { + return 0; + } + + /** + * Populates the supplied object set with info on all objects whose + * origin falls in the requested region. + */ + public abstract void getObjects (Rectangle region, ObjectSet set); + + /** + * Adds an object to this scene. + * + * @return true if the object was added, false if the add was rejected + * due to being a duplicate. + */ + public abstract boolean addObject (ObjectInfo info); + + /** + * Updates an object in this scene. + */ + public abstract void updateObject (ObjectInfo info); + + /** + * Removes the specified object from the scene. + * + * @return true if it was removed, false if the object was not in the + * scene. + */ + public abstract boolean removeObject (ObjectInfo info); + + /** + * Creates a copy of this scene model. + */ + public Object clone () + { + try { + return (MisoSceneModel)super.clone(); + } catch (CloneNotSupportedException cnse) { + throw new RuntimeException("MisoSceneModel.clone: " + cnse); + } + } + + /** A random number generator for filling random base tiles. */ + protected transient Random _rando = new Random(); +} diff --git a/src/java/com/threerings/miso/data/ObjectInfo.java b/src/java/com/threerings/miso/data/ObjectInfo.java new file mode 100644 index 00000000..70c5ccb8 --- /dev/null +++ b/src/java/com/threerings/miso/data/ObjectInfo.java @@ -0,0 +1,168 @@ +// +// $Id: ObjectInfo.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.data; + +import com.samskivert.util.StringUtil; + +import com.threerings.io.SimpleStreamableObject; +import com.threerings.media.tile.TileUtil; + +/** + * Contains information about an object in a Miso scene. + */ +public class ObjectInfo extends SimpleStreamableObject + implements Cloneable +{ + /** The fully qualified object tile id. */ + public int tileId; + + /** The x and y tile coordinates of the object. */ + public int x, y; + + /** Don't access this directly unless you are serializing this + * instance. Use {@link #getPriority} instead. */ + public byte priority = 0; + + /** The action associated with this object or null if it has no + * action. */ + public String action; + + /** A "spot" associated with this object (specified as an offset from + * the fine coordinates of the object's origin tile). */ + public byte sx, sy; + + /** The orientation of the "spot" associated with this object. */ + public byte sorient; + + /** Up to two colorization assignments for this object. */ + public int zations; + + /** + * Convenience constructor. + */ + public ObjectInfo (int tileId, int x, int y) + { + this.tileId = tileId; + this.x = x; + this.y = y; + } + + /** + * Creates an object info that is a copy of the supplied info. + */ + public ObjectInfo (ObjectInfo other) + { + this.tileId = other.tileId; + this.x = other.x; + this.y = other.y; + this.priority = other.priority; + this.action = other.action; + this.sx = other.sx; + this.sy = other.sy; + this.sorient = other.sorient; + this.zations = other.zations; + } + + /** + * Zero argument constructor needed for unserialization. + */ + public ObjectInfo () + { + } + + /** + * Returns the render priority of this object tile. + */ + public int getPriority () + { + return priority; + } + + /** + * Returns the primary colorization assignment. + */ + public int getPrimaryZation () + { + return (zations & 0xFFFF); + } + + /** + * Returns the secondary colorization assignment. + */ + public int getSecondaryZation () + { + return ((zations >> 16) & 0xFFFF); + } + + /** + * Sets the primary and secondary colorization assignments. + */ + public void setZations (short primary, short secondary) + { + zations = ((secondary << 16) | primary); + } + + /** + * Returns true if this object info contains non-default data for + * anything other than the tile id and coordinates. + */ + public boolean isInteresting () + { + return (!StringUtil.isBlank(action) || priority != 0 || + sx != 0 || sy != 0 || zations != 0); + } + + // documentation inherited + public boolean equals (Object other) + { + if (other instanceof ObjectInfo) { + ObjectInfo ooi = (ObjectInfo)other; + return (x == ooi.x && y == ooi.y && tileId == ooi.tileId); + } else { + return false; + } + } + + // documentation inherited + public int hashCode () + { + return x ^ y ^ tileId; + } + + // documentation inherited + public Object clone () + { + try { + return super.clone(); + } catch (CloneNotSupportedException cnse) { + // notgunnahappen. + return null; + } + } + + /** Enhances our {@link SimpleStreamableObject#toString} output. */ + public String tileIdToString () + { + return (TileUtil.getTileSetId(tileId) + ":" + + TileUtil.getTileIndex(tileId)); + } +} diff --git a/src/java/com/threerings/miso/data/SimpleMisoSceneModel.java b/src/java/com/threerings/miso/data/SimpleMisoSceneModel.java new file mode 100644 index 00000000..79256f8b --- /dev/null +++ b/src/java/com/threerings/miso/data/SimpleMisoSceneModel.java @@ -0,0 +1,282 @@ +// +// $Id: SimpleMisoSceneModel.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.data; + +import java.awt.Rectangle; +import java.util.ArrayList; + +import com.samskivert.util.ArrayUtil; +import com.samskivert.util.IntListUtil; +import com.samskivert.util.ListUtil; + +import com.threerings.media.util.MathUtil; +import com.threerings.miso.util.ObjectSet; + +/** + * Contains miso scene data for a scene that is assumed to be reasonably + * simple and small, such that all base tile data for the entire scene can + * be stored in a single contiguous array. + * + *

Additionally, it makes the assumption that the single model will be + * used to display a scene on a rectangular screen (dimensions defined by + * {@link #vwidth} and {@link #vheight}) and further optimizes the base + * tile array to obviate the need to store tile data for things that fall + * outside the bounds of the screen. + */ +public class SimpleMisoSceneModel extends MisoSceneModel +{ + /** The width of this scene in tiles. */ + public short width; + + /** The height of this scene in tiles. */ + public short height; + + /** The viewport width in tiles. */ + public int vwidth; + + /** The viewport height in tiles. */ + public int vheight; + + /** The combined tile ids (tile set id and tile id) in compressed + * format. Don't go poking around in here, use the accessor + * methods. */ + public int[] baseTileIds; + + /** The combined tile ids (tile set id and tile id) of the + * "uninteresting" tiles in the object layer. */ + public int[] objectTileIds; + + /** The x coordinate of the "uninteresting" tiles in the object + * layer. */ + public short[] objectXs; + + /** The y coordinate of the "uninteresting" tiles in the object + * layer. */ + public short[] objectYs; + + /** Information records for the "interesting" objects in the object + * layer. */ + public ObjectInfo[] objectInfo; + + /** + * Creates a completely uninitialized model suitable for little more + * than unserialization. + */ + public SimpleMisoSceneModel () + { + } + + /** + * Creates a blank scene model with the specified dimensions. + */ + public SimpleMisoSceneModel (int width, int height, int vwidth, int vheight) + { + this.width = (short)MathUtil.bound( + Short.MIN_VALUE, width, Short.MAX_VALUE); + this.height = (short)MathUtil.bound( + Short.MIN_VALUE, height, Short.MAX_VALUE); + this.vwidth = vwidth; + this.vheight = vheight; + allocateBaseTileArray(); + + // start with zero-length object arrays + objectTileIds = new int[0]; + objectXs = new short[0]; + objectYs = new short[0]; + objectInfo = new ObjectInfo[0]; + } + + // documentation inherited + public int getBaseTileId (int col, int row) + { + int index = getIndex(col, row); + return (index == -1) ? 0 : baseTileIds[index]; + } + + // documentation inherited + public boolean setBaseTile (int fqBaseTileId, int col, int row) + { + int index = getIndex(col, row); + if (index == -1) { + return false; + } + baseTileIds[index] = fqBaseTileId; + return true; + } + + // documentation inherited + public void getObjects (Rectangle region, ObjectSet set) + { + // first look for intersecting interesting objects + for (int ii = 0; ii < objectInfo.length; ii++) { + ObjectInfo info = objectInfo[ii]; + if (region.contains(info.x, info.y)) { + set.insert(info); + } + } + + // now look for intersecting non-interesting objects + for (int ii = 0; ii < objectTileIds.length; ii++) { + int x = objectXs[ii], y = objectYs[ii]; + if (region.contains(x, y)) { + set.insert(new ObjectInfo(objectTileIds[ii], x, y)); + } + } + } + + // documentation inherited + public boolean addObject (ObjectInfo info) + { + if (info.isInteresting()) { + objectInfo = (ObjectInfo[])ArrayUtil.append(objectInfo, info); + } else { + objectTileIds = ArrayUtil.append(objectTileIds, info.tileId); + objectXs = ArrayUtil.append(objectXs, (short)info.x); + objectYs = ArrayUtil.append(objectYs, (short)info.y); + } + return true; + } + + // documentation inherited + public void updateObject (ObjectInfo info) + { + // not efficient, but this is only done in editing situations + removeObject(info); + addObject(info); + } + + // documentation inherited + public boolean removeObject (ObjectInfo info) + { + // look for it in the interesting info array + int oidx = ListUtil.indexOf(objectInfo, info); + if (oidx != -1) { + objectInfo = (ObjectInfo[])ArrayUtil.splice(objectInfo, oidx, 1); + return true; + } + + // look for it in the uninteresting arrays + oidx = IntListUtil.indexOf(objectTileIds, info.tileId); + if (oidx != -1) { + objectTileIds = ArrayUtil.splice(objectTileIds, oidx, 1); + objectXs = ArrayUtil.splice(objectXs, oidx, 1); + objectYs = ArrayUtil.splice(objectYs, oidx, 1); + return true; + } + + return false; + } + + // documentation inherited + public Object clone () + { + SimpleMisoSceneModel model = (SimpleMisoSceneModel)super.clone(); + model.baseTileIds = (int[])baseTileIds.clone(); + model.objectTileIds = (int[])objectTileIds.clone(); + model.objectXs = (short[])objectXs.clone(); + model.objectYs = (short[])objectYs.clone(); + model.objectInfo = (ObjectInfo[])objectInfo.clone(); + return model; + } + + /** + * Get the index into the baseTileIds[] for the specified + * x and y coordinates, or return -1 if the specified coordinates + * are outside of the viewable area. + * + * Assumption: The viewable area is centered and aligned as far + * to the top of the isometric scene as possible, such that + * the upper-left corner is at the point where the tiles + * (0, vwid) and (0, vwid-1) touch. The upper-right corner + * is at the point where the tiles (vwid-1, 0) and (vwid, 0) + * touch. + * + * The viewable area is made up of "fat" rows and "thin" rows. The + * fat rows display one more tile than the thin rows because their + * first and last tiles are halfway off the viewable area. The thin + * rows are fully contained within the viewable area except for the + * first and last thin rows, which display only their bottom and top + * halves, respectively. Note that #fatrows == #thinrows - 1; + */ + protected int getIndex (int x, int y) + { + // check to see if the index lies in one of the "fat" rows + if (((x + y) & 1) == (vwidth & 1)) { + + int col = (vwidth + x - y) >> 1; + int row = x - col; + if ((col < 0) || (col > vwidth) || + (row < 0) || (row >= vheight)) { + return -1; // out of view + } + + return (vwidth + 1) * row + col; + + } else { + // the index must be in a "thin" row + int col = (vwidth + x - y - 1) >> 1; + int row = x - col; + if ((col < 0) || (col >= vwidth) || + (row < 0) || (row > vheight)) { + return -1; // out of view + } + + // we store the all the fat rows first, then all the thin + // rows, the '(vwidth + 1) * vheight' is the size of all + // the fat rows. + return row * vwidth + col + (vwidth + 1) * vheight; + } + } + + /** + * Allocate the base tile array. + */ + protected void allocateBaseTileArray () + { + baseTileIds = new int[vwidth + vheight + ((vwidth * vheight) << 1)]; + } + + /** + * Populates the interesting and uninteresting parts of a miso scene + * model given lists of {@link ObjectInfo} records for each. + */ + public static void populateObjects (SimpleMisoSceneModel model, + ArrayList ilist, ArrayList ulist) + { + // set up the uninteresting arrays + int ucount = ulist.size(); + model.objectTileIds = new int[ucount]; + model.objectXs = new short[ucount]; + model.objectYs = new short[ucount]; + for (int ii = 0; ii < ucount; ii++) { + ObjectInfo info = (ObjectInfo)ulist.get(ii); + model.objectTileIds[ii] = info.tileId; + model.objectXs[ii] = (short)info.x; + model.objectYs[ii] = (short)info.y; + } + + // set up the interesting array + int icount = ilist.size(); + model.objectInfo = new ObjectInfo[icount]; + ilist.toArray(model.objectInfo); + } +} diff --git a/src/java/com/threerings/miso/data/SparseMisoSceneModel.java b/src/java/com/threerings/miso/data/SparseMisoSceneModel.java new file mode 100644 index 00000000..8617f8c8 --- /dev/null +++ b/src/java/com/threerings/miso/data/SparseMisoSceneModel.java @@ -0,0 +1,445 @@ +// +// $Id: SparseMisoSceneModel.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.data; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Iterator; + +import com.samskivert.util.ArrayUtil; +import com.samskivert.util.ListUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.io.SimpleStreamableObject; + +import com.threerings.media.util.MathUtil; +import com.threerings.util.StreamableHashIntMap; + +import com.threerings.miso.Log; +import com.threerings.miso.util.ObjectSet; + +/** + * Contains miso scene data that is broken up into NxN tile sections. + */ +public class SparseMisoSceneModel extends MisoSceneModel +{ + /** An interface that allows external entities to "visit" and inspect + * every object in this scene. */ + public static interface ObjectVisitor + { + /** Called for each object in the scene, interesting and not. */ + public void visit (ObjectInfo info); + } + + /** Contains information on a section of this scene. This is only + * public so that the scene model parser can do its job, so don't go + * poking around in here. */ + public static class Section extends SimpleStreamableObject + implements Cloneable + { + /** The tile coordinate of our upper leftmost tile. */ + public short x, y; + + /** The width of this section in tiles. */ + public int width; + + /** The combined tile ids (tile set id and tile id) for our + * section (in row major order). */ + public int[] baseTileIds; + + /** The combined tile ids (tile set id and tile id) of the + * "uninteresting" tiles in the object layer. */ + public int[] objectTileIds = new int[0]; + + /** The x coordinate of the "uninteresting" tiles in the object + * layer. */ + public short[] objectXs = new short[0]; + + /** The y coordinate of the "uninteresting" tiles in the object + * layer. */ + public short[] objectYs = new short[0]; + + /** Information records for the "interesting" objects in the + * object layer. */ + public ObjectInfo[] objectInfo = new ObjectInfo[0]; + + /** + * Creates a blank section instance, suitable for unserialization + * or configuration by the XML scene parser. + */ + public Section () + { + } + + /** + * Creates a new scene section with the specified dimensions. + */ + public Section (short x, short y, short width, short height) + { + this.x = x; + this.y = y; + this.width = width; + baseTileIds = new int[width*height]; + } + + public int getBaseTileId (int col, int row) { + if (col < x || col >= (x+width) || row < y || row >= (y+width)) { + Log.warning("Requested bogus tile +" + col + "+" + row + + " from " + this + "."); + return -1; + } else { + return baseTileIds[(row-y)*width+(col-x)]; + } + } + + public void setBaseTile (int col, int row, int fqBaseTileId) { + baseTileIds[(row-y)*width+(col-x)] = fqBaseTileId; + } + + public boolean addObject (ObjectInfo info) { + // sanity check: see if there is already an object of this + // type at these coordinates + int dupidx; + if ((dupidx = ListUtil.indexOf(objectInfo, info)) != -1) { + Log.warning("Refusing to add duplicate object [ninfo=" + info + + ", oinfo=" + objectInfo[dupidx] + "]."); + return false; + } + if ((dupidx = indexOfUn(info)) != -1) { + Log.warning("Refusing to add duplicate object " + + "[info=" + info + "]."); + return false; + } + + if (info.isInteresting()) { + objectInfo = (ObjectInfo[])ArrayUtil.append(objectInfo, info); + } else { + objectTileIds = ArrayUtil.append(objectTileIds, info.tileId); + objectXs = ArrayUtil.append(objectXs, (short)info.x); + objectYs = ArrayUtil.append(objectYs, (short)info.y); + } + return true; + } + + public boolean removeObject (ObjectInfo info) { + // look for it in the interesting info array + int oidx = ListUtil.indexOf(objectInfo, info); + if (oidx != -1) { + objectInfo = (ObjectInfo[]) + ArrayUtil.splice(objectInfo, oidx, 1); + return true; + } + + // look for it in the uninteresting arrays + oidx = indexOfUn(info); + if (oidx != -1) { + objectTileIds = ArrayUtil.splice(objectTileIds, oidx, 1); + objectXs = ArrayUtil.splice(objectXs, oidx, 1); + objectYs = ArrayUtil.splice(objectYs, oidx, 1); + return true; + } + + return false; + } + + /** + * Returns the index of the specified object in the uninteresting + * arrays or -1 if it is not in this section as an uninteresting + * object. + */ + protected int indexOfUn (ObjectInfo info) + { + for (int ii = 0; ii < objectTileIds.length; ii++) { + if (objectTileIds[ii] == info.tileId && + objectXs[ii] == info.x && objectYs[ii] == info.y) { + return ii; + } + } + return -1; + } + + public void getObjects (Rectangle region, ObjectSet set) { + // first look for intersecting interesting objects + for (int ii = 0; ii < objectInfo.length; ii++) { + ObjectInfo info = objectInfo[ii]; + if (region.contains(info.x, info.y)) { + set.insert(info); + } + } + + // now look for intersecting non-interesting objects + for (int ii = 0; ii < objectTileIds.length; ii++) { + int x = objectXs[ii], y = objectYs[ii]; + if (region.contains(x, y)) { + set.insert(new ObjectInfo(objectTileIds[ii], x, y)); + } + } + } + + /** + * Returns true if this section contains no data beyond the default. + * Used when saving a sparse scene: we omit blank sections. + */ + public boolean isBlank () + { + if ((objectTileIds.length != 0) || (objectInfo.length != 0)) { + return false; + } + for (int ii=0, nn=baseTileIds.length; ii < nn; ii++) { + if (baseTileIds[ii] != 0) { + return false; + } + } + + return true; + } + + public Object clone () { + try { + Section section = (Section)super.clone(); + section.baseTileIds = (int[])baseTileIds.clone(); + section.objectTileIds = (int[])objectTileIds.clone(); + section.objectXs = (short[])objectXs.clone(); + section.objectYs = (short[])objectYs.clone(); + section.objectInfo = (ObjectInfo[])objectInfo.clone(); + return section; + } catch (CloneNotSupportedException cnse) { + throw new RuntimeException( + "SparseMisoSceneModel.Section.clone: " + cnse); + } + } + + public String toString () { + return ((width == 0) ? "" : + (width + "x" + (baseTileIds.length/width))) + + "+" + x + "+" + y + + ":" + objectInfo.length + ":" + objectTileIds.length; + } + } + + /** The dimensions of a section of our scene. */ + public short swidth, sheight; + + /** The tileset to use when we have no tile data. */ + public int defTileSet = 0; + + /** + * Creates a scene model with the specified bounds. + * + * @param swidth the width of a single section (in tiles). + * @param sheight the height of a single section (in tiles). + */ + public SparseMisoSceneModel (int swidth, int sheight) + { + this.swidth = (short)swidth; + this.sheight = (short)sheight; + } + + /** + * Creates a blank model suitable for unserialization. + */ + public SparseMisoSceneModel () + { + } + + /** + * Adds all interesting {@link ObjectInfo} records in this scene to + * the supplied list. + */ + public void getInterestingObjects (ArrayList list) + { + for (Iterator iter = getSections(); iter.hasNext(); ) { + Section sect = (Section)iter.next(); + for (int oo = 0; oo < sect.objectInfo.length; oo++) { + list.add(sect.objectInfo[oo]); + } + } + } + + /** + * Informs the supplied visitor of each object in this scene. + */ + public void visitObjects (ObjectVisitor visitor) + { + visitObjects(visitor, false); + } + + /** + * Informs the supplied visitor of each object in this scene. + * + * @param interestingOnly if true, only the interesting objects will + * be visited. + */ + public void visitObjects (ObjectVisitor visitor, boolean interestingOnly) + { + for (Iterator iter = getSections(); iter.hasNext(); ) { + Section sect = (Section)iter.next(); + for (int oo = 0; oo < sect.objectInfo.length; oo++) { + ObjectInfo oinfo = sect.objectInfo[oo]; + visitor.visit(oinfo); + } + if (!interestingOnly) { + ObjectInfo info = new ObjectInfo(); + for (int oo = 0; oo < sect.objectTileIds.length; oo++) { + info.tileId = sect.objectTileIds[oo]; + info.x = sect.objectXs[oo]; + info.y = sect.objectYs[oo]; + visitor.visit(info); + } + } + } + } + + // documentation inherited + public int getBaseTileId (int col, int row) + { + Section sec = getSection(col, row, false); + return (sec == null) ? -1 : sec.getBaseTileId(col, row); + } + + // documentation inherited + public boolean setBaseTile (int fqBaseTileId, int col, int row) + { + getSection(col, row, true).setBaseTile(col, row, fqBaseTileId); + return true; + } + + // documentation inherited + public void setDefaultBaseTileSet (int tileSetId) + { + defTileSet = tileSetId; + } + + // documentation inherited + public int getDefaultBaseTileSet () + { + return defTileSet; + } + + // documentation inherited + public void getObjects (Rectangle region, ObjectSet set) + { + int minx = MathUtil.floorDiv(region.x, swidth)*swidth; + int maxx = MathUtil.floorDiv(region.x+region.width-1, swidth)*swidth; + int miny = MathUtil.floorDiv(region.y, sheight)*sheight; + int maxy = MathUtil.floorDiv(region.y+region.height-1, sheight)*sheight; + for (int yy = miny; yy <= maxy; yy += sheight) { + for (int xx = minx; xx <= maxx; xx += swidth) { + Section sec = getSection(xx, yy, false); + if (sec != null) { + sec.getObjects(region, set); + } + } + } + } + + // documentation inherited + public boolean addObject (ObjectInfo info) + { + return getSection(info.x, info.y, true).addObject(info); + } + + // documentation inherited + public void updateObject (ObjectInfo info) + { + // not efficient, but this is only done in editing situations + removeObject(info); + addObject(info); + } + + // documentation inherited + public boolean removeObject (ObjectInfo info) + { + Section sec = getSection(info.x, info.y, false); + if (sec != null) { + return sec.removeObject(info); + } else { + return false; + } + } + + /** + * Don't call this method! This is only public so that the scene + * parser can construct a scene from raw data. If only Java supported + * class friendship. + */ + public void setSection (Section section) + { + _sections.put(key(section.x, section.y), section); + } + + /** + * Don't call this method! This is only public so that the scene + * writer can generate XML from the raw scene data. + */ + public Iterator getSections () + { + return _sections.values().iterator(); + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", sections=" + + StringUtil.toString(_sections.values().iterator())); + } + + /** + * Returns the key for the specified section. + */ + protected final int key (int x, int y) + { + int sx = MathUtil.floorDiv(x, swidth); + int sy = MathUtil.floorDiv(y, sheight); + return (sx << 16) | (sy & 0xFFFF); + } + + /** Returns the section for the specified tile coordinate. */ + protected final Section getSection (int x, int y, boolean create) + { + int key = key(x, y); + Section sect = (Section)_sections.get(key); + if (sect == null && create) { + short sx = (short)(MathUtil.floorDiv(x, swidth)*swidth); + short sy = (short)(MathUtil.floorDiv(y, sheight)*sheight); + _sections.put(key, sect = new Section(sx, sy, swidth, sheight)); +// Log.info("Created new section " + sect + "."); + } + return sect; + } + + // documentation inherited + public Object clone () + { + SparseMisoSceneModel model = (SparseMisoSceneModel)super.clone(); + model._sections = new StreamableHashIntMap(); + for (Iterator iter = getSections(); iter.hasNext(); ) { + Section sect = (Section)iter.next(); + model.setSection((Section)sect.clone()); + } + return model; + } + + /** Contains our sections in row major order. */ + protected StreamableHashIntMap _sections = new StreamableHashIntMap(); +} diff --git a/src/java/com/threerings/miso/data/VirtualMisoSceneModel.java b/src/java/com/threerings/miso/data/VirtualMisoSceneModel.java new file mode 100644 index 00000000..299fdd65 --- /dev/null +++ b/src/java/com/threerings/miso/data/VirtualMisoSceneModel.java @@ -0,0 +1,58 @@ +// +// $Id: VirtualMisoSceneModel.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.data; + +/** + * A convenient base class for "virtual" scenes which do not allow editing + * and compute the base and object tiles rather than obtain them from some + * data structure. + */ +public abstract class VirtualMisoSceneModel extends MisoSceneModel +{ + public VirtualMisoSceneModel () + { + } + + // documentation inherited from interface + public boolean setBaseTile (int fqTileId, int x, int y) + { + throw new UnsupportedOperationException(); + } + + // documentation inherited from interface + public boolean addObject (ObjectInfo info) + { + throw new UnsupportedOperationException(); + } + + // documentation inherited from interface + public void updateObject (ObjectInfo info) + { + throw new UnsupportedOperationException(); + } + + // documentation inherited from interface + public boolean removeObject (ObjectInfo info) + { + throw new UnsupportedOperationException(); + } +} diff --git a/src/java/com/threerings/miso/tile/AutoFringer.java b/src/java/com/threerings/miso/tile/AutoFringer.java new file mode 100644 index 00000000..be302f13 --- /dev/null +++ b/src/java/com/threerings/miso/tile/AutoFringer.java @@ -0,0 +1,390 @@ +// +// $Id: AutoFringer.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tile; + +import java.awt.Graphics2D; +import java.awt.Transparency; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.HashMap; + +import com.samskivert.util.CheapIntMap; +import com.samskivert.util.QuickSort; + +import com.threerings.media.image.BufferedMirage; +import com.threerings.media.image.ImageManager; +import com.threerings.media.image.ImageUtil; + +import com.threerings.media.tile.NoSuchTileSetException; +import com.threerings.media.tile.Tile; +import com.threerings.media.tile.TileManager; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileUtil; + +import com.threerings.miso.Log; +import com.threerings.miso.data.MisoSceneModel; + +/** + * Automatically fringes a scene according to the rules in the supplied + * fringe configuration. + */ +public class AutoFringer +{ + /** + * Constructs an instance that will fringe according to the rules in + * the supplied fringe configuration. + */ + public AutoFringer (FringeConfiguration fringeconf, ImageManager imgr, + TileManager tmgr) + { + _fringeconf = fringeconf; + _imgr = imgr; + _tmgr = tmgr; + } + + /** + * Compute and return the fringe tile to be inserted at the specified + * location. + */ + public BaseTile getFringeTile (MisoSceneModel scene, int col, int row, + HashMap masks) + { + // get the tileset id of the base tile we are considering + int underset = scene.getBaseTileId(col, row) >> 16; + + // start with a clean temporary fringer map + _fringers.clear(); + boolean passable = true; + + // walk through our influence tiles + for (int y = row - 1, maxy = row + 2; y < maxy; y++) { + for (int x = col - 1, maxx = col + 2; x < maxx; x++) { + // we sensibly do not consider ourselves + if ((x == col) && (y == row)) { + continue; + } + + // determine the tileset for this tile + int btid = scene.getBaseTileId(x, y); + int baseset = (btid <= 0) ? + scene.getDefaultBaseTileSet() : (btid >> 16); + + // determine if it fringes on our tile + int pri = _fringeconf.fringesOn(baseset, underset); + if (pri == -1) { + continue; + } + + FringerRec fringer = (FringerRec)_fringers.get(baseset); + if (fringer == null) { + fringer = new FringerRec(baseset, pri); + _fringers.put(baseset, fringer); + } + + // now turn on the appropriate fringebits + fringer.bits |= FLAGMATRIX[y - row + 1][x - col + 1]; + + // See if a tile that fringes on us kills our passability, + // but don't count the default base tile against us, as + // we allow users to splash in the water. + if (passable && (btid > 0)) { + try { + BaseTile bt = (BaseTile) _tmgr.getTile(btid); + passable = bt.isPassable(); + } catch (NoSuchTileSetException nstse) { + Log.warning("Autofringer couldn't find a base " + + "set while attempting to figure passability " + + "[error=" + nstse + "]."); + } + } + } + } + + // if nothing fringed, we're done + int numfringers = _fringers.size(); + if (numfringers == 0) { + return null; + } + + // otherwise compose a FringeTile from the specified fringes + FringerRec[] frecs = new FringerRec[numfringers]; + for (int ii = 0, pp = 0; ii < 16; ii++) { + FringerRec rec = (FringerRec)_fringers.getValue(ii); + if (rec != null) { + frecs[pp++] = rec; + } + } + + BaseTile frTile = new BaseTile(); + frTile.setPassable(passable); + composeFringeTile(frTile, frecs, masks, TileUtil.getTileHash(col, row)); + return frTile; + } + + /** + * Compose a FringeTile out of the various fringe images needed. + */ + protected void composeFringeTile ( + Tile frTile, FringerRec[] fringers, HashMap masks, int hashValue) + { + // sort the array so that higher priority fringers get drawn first + QuickSort.sort(fringers); + + BufferedImage ftimg = null; + for (int ii = 0; ii < fringers.length; ii++) { + int[] indexes = getFringeIndexes(fringers[ii].bits); + for (int jj = 0; jj < indexes.length; jj++) { + try { + ftimg = getTileImage(ftimg, fringers[ii].baseset, + indexes[jj], masks, hashValue); + } catch (NoSuchTileSetException nstse) { + Log.warning("Autofringer couldn't find a needed tileset " + + "[error=" + nstse + "]."); + } + } + } + + frTile.setImage(new BufferedMirage(ftimg)); + } + + /** + * Retrieve or compose an image for the specified fringe. + */ + protected BufferedImage getTileImage ( + BufferedImage ftimg, int baseset, int index, + HashMap masks, int hashValue) + throws NoSuchTileSetException + { + FringeConfiguration.FringeTileSetRecord tsr = + _fringeconf.getFringe(baseset, hashValue); + int fringeset = tsr.fringe_tsid; + TileSet fset = _tmgr.getTileSet(fringeset); + + if (!tsr.mask) { + // oh good, this is easy + Tile stamp = fset.getTile(index); + return stampTileImage(stamp, ftimg, stamp.getWidth(), + stamp.getHeight()); + } + + // otherwise, it's a mask.. look for it in the cache.. + Long maskkey = Long.valueOf((((long) baseset) << 32) + + (fringeset << 16) + index); + BufferedImage img = (BufferedImage)masks.get(maskkey); + if (img == null) { + BufferedImage fsrc = fset.getRawTileImage(index); + BufferedImage bsrc = _tmgr.getTileSet(baseset).getRawTileImage(0); + img = ImageUtil.composeMaskedImage(_imgr, fsrc, bsrc); + masks.put(maskkey, img); + } + ftimg = stampTileImage(img, ftimg, img.getWidth(null), + img.getHeight(null)); + + return ftimg; + } + + /** Helper function for {@link #getTileImage}. */ + protected BufferedImage stampTileImage (Object stamp, BufferedImage ftimg, + int width, int height) + { + // create the target image if necessary + if (ftimg == null) { + ftimg = _imgr.createImage(width, height, Transparency.BITMASK); + } + Graphics2D gfx = (Graphics2D)ftimg.getGraphics(); + try { + if (stamp instanceof Tile) { + ((Tile)stamp).paint(gfx, 0, 0); + } else { + gfx.drawImage((BufferedImage)stamp, 0, 0, null); + } + } finally { + gfx.dispose(); + } + return ftimg; + } + + /** + * Get the fringe index specified by the fringebits. If no index + * is available, try breaking down the bits into contiguous regions of + * bits and look for indexes for those. + */ + protected int[] getFringeIndexes (int bits) + { + int index = BITS_TO_INDEX[bits]; + if (index != -1) { + int[] ret = new int[1]; + ret[0] = index; + return ret; + } + + // otherwise, split the bits into contiguous components + + // look for a zero and start our first split + int start = 0; + while ((((1 << start) & bits) != 0) && (start < NUM_FRINGEBITS)) { + start++; + } + + if (start == NUM_FRINGEBITS) { + // we never found an empty fringebit, and since index (above) + // was already -1, we have no fringe tile for these bits.. sad. + return new int[0]; + } + + ArrayList indexes = new ArrayList(); + int weebits = 0; + for (int ii=(start + 1) % NUM_FRINGEBITS; ii != start; + ii = (ii + 1) % NUM_FRINGEBITS) { + + if (((1 << ii) & bits) != 0) { + weebits |= (1 << ii); + } else if (weebits != 0) { + index = BITS_TO_INDEX[weebits]; + if (index != -1) { + indexes.add(Integer.valueOf(index)); + } + weebits = 0; + } + } + if (weebits != 0) { + index = BITS_TO_INDEX[weebits]; + if (index != -1) { + indexes.add(Integer.valueOf(index)); + } + } + + int[] ret = new int[indexes.size()]; + for (int ii=0; ii < ret.length; ii++) { + ret[ii] = ((Integer) indexes.get(ii)).intValue(); + } + return ret; + } + + /** + * A record for holding information about a particular fringe as we're + * computing what it will look like. + */ + static protected class FringerRec implements Comparable + { + int baseset; + int priority; + int bits; + + public FringerRec (int base, int pri) + { + baseset = base; + priority = pri; + } + + public int compareTo (Object o) + { + return priority - ((FringerRec) o).priority; + } + + public String toString () + { + return "[base=" + baseset + ", pri=" + priority + + ", bits=" + Integer.toString(bits, 16) + "]"; + } + } + + // fringe bits + // see docs/miso/fringebits.png + // + protected static final int NORTH = 1 << 0; + protected static final int NORTHEAST = 1 << 1; + protected static final int EAST = 1 << 2; + protected static final int SOUTHEAST = 1 << 3; + protected static final int SOUTH = 1 << 4; + protected static final int SOUTHWEST = 1 << 5; + protected static final int WEST = 1 << 6; + protected static final int NORTHWEST = 1 << 7; + + protected static final int NUM_FRINGEBITS = 8; + + // A matrix mapping adjacent tiles to which fringe bits + // they affect. + // (x and y are offset by +1, since we can't have -1 as an array index) + // again, see docs/miso/fringebits.png + // + protected static final int[][] FLAGMATRIX = { + { NORTHEAST, (NORTHEAST | EAST | SOUTHEAST), SOUTHEAST }, + { (NORTHWEST | NORTH | NORTHEAST), 0, (SOUTHEAST | SOUTH | SOUTHWEST) }, + { NORTHWEST, (NORTHWEST | WEST | SOUTHWEST), SOUTHWEST } + }; + + /** + * The fringe tiles we use. These are the 17 possible tiles made + * up of continuous fringebits sections. + * Huh? see docs/miso/fringebits.png + */ + protected static final int[] FRINGETILES = { + SOUTHEAST, + SOUTHWEST | SOUTH | SOUTHEAST, + SOUTHWEST, + NORTHEAST | EAST | SOUTHEAST, + NORTHWEST | WEST | SOUTHWEST, + NORTHEAST, + NORTHWEST | NORTH | NORTHEAST, + NORTHWEST, + + SOUTHWEST | WEST | NORTHWEST | NORTH | NORTHEAST, + NORTHWEST | NORTH | NORTHEAST | EAST | SOUTHEAST, + NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST, + SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST, + + NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST, + SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST, + SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST, + NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST, + + // all the directions! + NORTH | NORTHEAST | EAST | SOUTHEAST | + SOUTH | SOUTHWEST | WEST | NORTHWEST + }; + + // A reverse map of the above array, for quickly looking up which tile + // we want. + protected static final int[] BITS_TO_INDEX; + + // Construct the BITS_TO_INDEX array. + static { + int num = (1 << NUM_FRINGEBITS); + BITS_TO_INDEX = new int[num]; + + // first clear everything to -1 (meaning there is no tile defined) + for (int ii=0; ii < num; ii++) { + BITS_TO_INDEX[ii] = -1; + } + + // then fill in with the defined tiles. + for (int ii=0; ii < FRINGETILES.length; ii++) { + BITS_TO_INDEX[FRINGETILES[ii]] = ii; + } + } + + protected ImageManager _imgr; + protected TileManager _tmgr; + protected FringeConfiguration _fringeconf; + protected CheapIntMap _fringers = new CheapIntMap(16); +} diff --git a/src/java/com/threerings/miso/tile/BaseTile.java b/src/java/com/threerings/miso/tile/BaseTile.java new file mode 100644 index 00000000..b39107a5 --- /dev/null +++ b/src/java/com/threerings/miso/tile/BaseTile.java @@ -0,0 +1,52 @@ +// +// $Id: BaseTile.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tile; + +import com.threerings.media.tile.Tile; + +/** + * Extends the tile class to add support for tile passability. + * + * @see BaseTileSet + */ +public class BaseTile extends Tile +{ + /** + * Returns whether or not this tile can be walked upon by character + * sprites. + */ + public boolean isPassable () + { + return _passable; + } + + /** + * Configures this base tile as passable or impassable. + */ + public void setPassable (boolean passable) + { + _passable = passable; + } + + /** Whether the tile is passable. */ + protected boolean _passable = true; +} diff --git a/src/java/com/threerings/miso/tile/BaseTileSet.java b/src/java/com/threerings/miso/tile/BaseTileSet.java new file mode 100644 index 00000000..81ba4b97 --- /dev/null +++ b/src/java/com/threerings/miso/tile/BaseTileSet.java @@ -0,0 +1,81 @@ +// +// $Id: BaseTileSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tile; + +import com.samskivert.util.StringUtil; + +import com.threerings.media.image.Colorization; +import com.threerings.media.tile.SwissArmyTileSet; +import com.threerings.media.tile.Tile; + +/** + * The base tileset extends the swiss army tileset to add support for tile + * passability. Passability is used to determine whether traverser objects + * (generally sprites made to "walk" around the scene) can traverse a + * particular tile in a scene. + */ +public class BaseTileSet extends SwissArmyTileSet +{ + /** + * Sets the passability information for the tiles in this tileset. + * Each entry in the array corresponds to the tile at that tile index. + */ + public void setPassability (boolean[] passable) + { + _passable = passable; + } + + /** + * Returns the passability information for the tiles in this tileset. + */ + public boolean[] getPassability () + { + return _passable; + } + + // documentation inherited + protected Tile createTile () + { + return new BaseTile(); + } + + // documentation inherited + protected void initTile (Tile tile, int tileIndex, Colorization[] zations) + { + super.initTile(tile, tileIndex, zations); + ((BaseTile)tile).setPassable(_passable[tileIndex]); + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", passable=").append(StringUtil.toString(_passable)); + } + + /** Whether each tile is passable. */ + protected boolean[] _passable; + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/miso/tile/FringeConfiguration.java b/src/java/com/threerings/miso/tile/FringeConfiguration.java new file mode 100644 index 00000000..33747aaa --- /dev/null +++ b/src/java/com/threerings/miso/tile/FringeConfiguration.java @@ -0,0 +1,158 @@ +// +// $Id: FringeConfiguration.java 3403 2005-03-14 23:58:02Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tile; + +import java.io.Serializable; +import java.util.ArrayList; + +import com.samskivert.util.HashIntMap; +import com.samskivert.util.StringUtil; + +/** + * Used to manage data about which base tilesets fringe on which others + * and how they fringe. + */ +public class FringeConfiguration implements Serializable +{ + /** + * The path (relative to the resource directory) at which the fringe + * configuration should be loaded and stored. + */ + public static final String CONFIG_PATH = "config/miso/tile/fringeconf.dat"; + + public static class FringeRecord implements Serializable + { + /** The tileset id of the base tileset to which this applies. */ + public int base_tsid; + + /** The fringe priority of this base tileset. */ + public int priority; + + /** A list of the possible tilesets that can be used for fringing. */ + public ArrayList tilesets = new ArrayList(); + + /** Used when parsing the tilesets definitions. */ + public void addTileset (FringeTileSetRecord record) + { + tilesets.add(record); + } + + /** Did everything parse well? */ + public boolean isValid () + { + return ((base_tsid != 0) && (priority > 0)); + } + + /** Generates a string representation of this instance. */ + public String toString () + { + return "[base_tsid=" + base_tsid + ", priority=" + priority + + ", tilesets=" + StringUtil.toString(tilesets) + "]"; + } + + /** Increase this value when object's serialized state is impacted + * by a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; + } + + /** + * Used to parse the tileset fringe definitions. + */ + public static class FringeTileSetRecord implements Serializable + { + /** The tileset id of the fringe tileset. */ + public int fringe_tsid; + + /** Is this a mask? */ + public boolean mask; + + /** Did everything parse well? */ + public boolean isValid () + { + return (fringe_tsid != 0); + } + + /** Generates a string representation of this instance. */ + public String toString () + { + return "[fringe_tsid=" + fringe_tsid + ", mask=" + mask + "]"; + } + + /** Increase this value when object's serialized state is impacted + * by a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; + } + + /** + * Adds a parsed FringeRecord to this instance. This is used when parsing + * the fringerecords from xml. + */ + public void addFringeRecord (FringeRecord frec) + { + _frecs.put(frec.base_tsid, frec); + } + + /** + * If the first base tileset fringes upon the second, return the + * fringe priority of the first base tileset, otherwise return -1. + */ + public int fringesOn (int first, int second) + { + FringeRecord f1 = (FringeRecord) _frecs.get(first); + + // we better have a fringe record for the first + if (null != f1) { + + // it had better have some tilesets defined + if (f1.tilesets.size() > 0) { + + FringeRecord f2 = (FringeRecord) _frecs.get(second); + + // and we only fringe if second doesn't exist or has a lower + // priority + if ((null == f2) || (f1.priority > f2.priority)) { + return f1.priority; + } + } + } + + return -1; + } + + /** + * Get a random FringeTileSetRecord from amongst the ones + * listed for the specified base tileset. + */ + public FringeTileSetRecord getFringe (int baseset, int hashValue) + { + FringeRecord f = (FringeRecord) _frecs.get(baseset); + return (FringeTileSetRecord) f.tilesets.get( + hashValue % f.tilesets.size()); + } + + /** The mapping from base tileset id to fringerecord. */ + protected HashIntMap _frecs = new HashIntMap(); + + /** Increase this value when object's serialized state is impacted by + * a class change (modification of fields, inheritance). */ + private static final long serialVersionUID = 1; +} diff --git a/src/java/com/threerings/miso/tile/MisoTileManager.java b/src/java/com/threerings/miso/tile/MisoTileManager.java new file mode 100644 index 00000000..13bdd006 --- /dev/null +++ b/src/java/com/threerings/miso/tile/MisoTileManager.java @@ -0,0 +1,92 @@ +// +// $Id: MisoTileManager.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tile; + +import java.io.IOException; +import java.io.InputStream; + +import com.samskivert.io.StreamUtil; + +import com.threerings.resource.ResourceManager; +import com.threerings.util.CompiledConfig; + +import com.threerings.media.image.ImageManager; +import com.threerings.media.tile.TileManager; + +import com.threerings.miso.Log; + +/** + * Extends the basic tile manager and provides support for automatically + * generating fringes in between different types of base tiles in a scene. + */ +public class MisoTileManager extends TileManager +{ + /** + * Creates a tile manager and provides it with a reference to the + * image manager from which it will load tileset images. + * + * @param imgr the image manager via which the tile manager will + * decode and cache images. + */ + public MisoTileManager (ResourceManager rmgr, ImageManager imgr) + { + super(imgr); + + // look for a fringe configuration in the appropriate place + InputStream in = null; + try { + in = rmgr.getResource(FRINGE_CONFIG_PATH); + FringeConfiguration config = (FringeConfiguration) + CompiledConfig.loadConfig(in); + + // if we've found it, create our auto fringer with it + _fringer = new AutoFringer(config, imgr, this); + + } catch (IOException ioe) { + Log.warning("Unable to load fringe configuration " + + "[path=" + FRINGE_CONFIG_PATH + + ", error=" + ioe + "]."); + + } finally { + StreamUtil.close(in); + } + } + + /** + * Returns the auto fringer that has been configured for use by this + * tile manager. This will only be valid if this tile manager has been + * provided with a miso tileset repository via {@link + * #setTileSetRepository}. + */ + public AutoFringer getAutoFringer () + { + return _fringer; + } + + /** The entity that performs the automatic fringe layer generation. */ + protected AutoFringer _fringer; + + /** The path (in the classpath) to the serialized fringe + * configuration. */ + protected static final String FRINGE_CONFIG_PATH = + "config/miso/tile/fringeconf.dat"; +} diff --git a/src/java/com/threerings/miso/tile/tools/CompileFringeConfigurationTask.java b/src/java/com/threerings/miso/tile/tools/CompileFringeConfigurationTask.java new file mode 100644 index 00000000..8784efd2 --- /dev/null +++ b/src/java/com/threerings/miso/tile/tools/CompileFringeConfigurationTask.java @@ -0,0 +1,96 @@ +// +// $Id: CompileFringeConfigurationTask.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tile.tools; + +import java.io.File; +import java.io.Serializable; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Task; + +import com.samskivert.io.PersistenceException; + +import com.threerings.util.CompiledConfig; + +import com.threerings.media.tile.tools.MapFileTileSetIDBroker; + +import com.threerings.miso.tile.tools.xml.FringeConfigurationParser; + +/** + * Compile fringe configuration. + */ +public class CompileFringeConfigurationTask extends Task +{ + public void setTileSetMap (File tsetmap) + { + _tsetmap = tsetmap; + } + + public void setFringeDef (File fringedef) + { + _fringedef = fringedef; + } + + public void setTarget (File target) + { + _target = target; + } + + public void execute () throws BuildException + { + // make sure the source file exists + if (!_fringedef.exists()) { + throw new BuildException("Fringe definition file not found " + + "[path=" + _fringedef.getPath() + "]."); + } + + // set up the tileid broker + MapFileTileSetIDBroker broker; + try { + broker = new MapFileTileSetIDBroker(_tsetmap); + } catch (PersistenceException pe) { + throw new BuildException("Couldn't set up tileset mapping " + + "[path=" + _tsetmap.getPath() + + ", error=" + pe.getCause() + "]."); + } + + FringeConfigurationParser parser = new FringeConfigurationParser( + broker); + Serializable config; + try { + config = parser.parseConfig(_fringedef); + } catch (Exception e) { + throw new BuildException("Failure parsing config definition", e); + } + + try { + // and write it on out + CompiledConfig.saveConfig(_target, config); + } catch (Exception e) { + throw new BuildException("Failure writing serialized config", e); + } + } + + protected File _tsetmap; + protected File _fringedef; + protected File _target; +} diff --git a/src/java/com/threerings/miso/tile/tools/xml/BaseTileSetRuleSet.java b/src/java/com/threerings/miso/tile/tools/xml/BaseTileSetRuleSet.java new file mode 100644 index 00000000..f34bb431 --- /dev/null +++ b/src/java/com/threerings/miso/tile/tools/xml/BaseTileSetRuleSet.java @@ -0,0 +1,84 @@ +// +// $Id: BaseTileSetRuleSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tile.tools.xml; + +import org.apache.commons.digester.Digester; + +import com.samskivert.util.StringUtil; +import com.samskivert.xml.CallMethodSpecialRule; + +import com.threerings.media.tile.tools.xml.SwissArmyTileSetRuleSet; + +import com.threerings.miso.Log; +import com.threerings.miso.tile.BaseTileSet; + +/** + * Parses {@link BaseTileSet} instances from a tileset description. Base + * tilesets extend swiss army tilesets with the addition of a passability + * flag for each tile. + * + * @see SwissArmyTileSetRuleSet + */ +public class BaseTileSetRuleSet extends SwissArmyTileSetRuleSet +{ + // documentation inherited + public void addRuleInstances (Digester digester) + { + super.addRuleInstances(digester); + + digester.addRule( + _prefix + TILESET_PATH + "/passable", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + { + int[] values = StringUtil.parseIntArray(bodyText); + boolean[] passable = new boolean[values.length]; + for (int i = 0; i < values.length; i++) { + passable[i] = (values[i] != 0); + } + BaseTileSet starget = (BaseTileSet)target; + starget.setPassability(passable); + } + }); + } + + // documentation inherited + public boolean isValid (Object target) + { + BaseTileSet set = (BaseTileSet)target; + boolean valid = super.isValid(target); + + // check for a element + if (set.getPassability() == null) { + Log.warning("Tile set definition missing valid " + + "element [set=" + set + "]."); + valid = false; + } + + return valid; + } + + // documentation inherited + protected Class getTileSetClass () + { + return BaseTileSet.class; + } +} diff --git a/src/java/com/threerings/miso/tile/tools/xml/FringeConfigurationParser.java b/src/java/com/threerings/miso/tile/tools/xml/FringeConfigurationParser.java new file mode 100644 index 00000000..8450b037 --- /dev/null +++ b/src/java/com/threerings/miso/tile/tools/xml/FringeConfigurationParser.java @@ -0,0 +1,168 @@ +// +// $Id: FringeConfigurationParser.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tile.tools.xml; + +import java.io.Serializable; + +import org.xml.sax.Attributes; +import org.apache.commons.digester.Digester; + +import com.samskivert.util.StringUtil; +import com.samskivert.xml.SetPropertyFieldsRule; +import com.samskivert.xml.ValidatedSetNextRule; + +import com.threerings.tools.xml.CompiledConfigParser; + +import com.threerings.media.tile.TileSetIDBroker; + +import com.threerings.miso.Log; +import com.threerings.miso.tile.FringeConfiguration.FringeRecord; +import com.threerings.miso.tile.FringeConfiguration.FringeTileSetRecord; +import com.threerings.miso.tile.FringeConfiguration; + +/** + * Parses fringe config definitions. + */ +public class FringeConfigurationParser extends CompiledConfigParser +{ + public FringeConfigurationParser (TileSetIDBroker broker) + { + _idBroker = broker; + } + + + // documentation inherited + protected Serializable createConfigObject () + { + return new FringeConfiguration(); + } + + // documentation inherited + protected void addRules (Digester digest) + { + // configure top-level constraints + String prefix = "fringe"; + digest.addRule(prefix, new SetPropertyFieldsRule()); + + // create and configure fringe config instances + prefix += "/base"; + digest.addObjectCreate(prefix, FringeRecord.class.getName()); + + ValidatedSetNextRule.Validator val; + val = new ValidatedSetNextRule.Validator() { + public boolean isValid (Object target) { + if (((FringeRecord) target).isValid()) { + return true; + } else { + Log.warning("A FringeRecord was not added because it was " + + "improperly specified [rec=" + target + "]."); + return false; + } + } + }; + ValidatedSetNextRule vrule; + vrule = new ValidatedSetNextRule("addFringeRecord", val) { + // parse the fringe record, converting tileset names to + // tileset ids + public void begin (String namespace, String lname, Attributes attrs) + throws Exception + { + FringeRecord frec = (FringeRecord) digester.peek(); + + for (int ii=0; ii < attrs.getLength(); ii++) { + String name = attrs.getLocalName(ii); + if (StringUtil.isBlank(name)) { + name = attrs.getQName(ii); + } + String value = attrs.getValue(ii); + + if ("name".equals(name)) { + if (_idBroker.tileSetMapped(value)) { + frec.base_tsid = _idBroker.getTileSetID(value); + } else { + Log.warning("Skipping unknown base " + + "tileset [name=" + value + "]."); + } + + } else if ("priority".equals(name)) { + frec.priority = Integer.parseInt(value); + } else { + Log.warning("Skipping unknown attribute " + + "[name=" + name + "]."); + } + } + } + }; + digest.addRule(prefix, vrule); + + // create the tileset records in each fringe record + prefix += "/tileset"; + digest.addObjectCreate(prefix, FringeTileSetRecord.class.getName()); + + val = new ValidatedSetNextRule.Validator() { + public boolean isValid (Object target) { + if (((FringeTileSetRecord) target).isValid()) { + return true; + } else { + Log.warning("A FringeTileSetRecord was not added because " + + "it was improperly specified " + + "[rec=" + target + "]."); + return false; + } + } + }; + vrule = new ValidatedSetNextRule("addTileset", val) { + // parse the fringe tilesetrecord, converting tileset names to ids + public void begin (String namespace, String lname, Attributes attrs) + throws Exception + { + FringeTileSetRecord f = (FringeTileSetRecord) digester.peek(); + + for (int ii=0; ii < attrs.getLength(); ii++) { + String name = attrs.getLocalName(ii); + if (StringUtil.isBlank(name)) { + name = attrs.getQName(ii); + } + String value = attrs.getValue(ii); + + if ("name".equals(name)) { + if (_idBroker.tileSetMapped(value)) { + f.fringe_tsid = _idBroker.getTileSetID(value); + } else { + Log.warning("Skipping unknown fringe " + + "tileset [name=" + value + "]."); + } + + } else if ("mask".equals(name)) { + f.mask = Boolean.valueOf(value).booleanValue(); + } else { + Log.warning("Skipping unknown attribute " + + "[name=" + name + "]."); + } + } + } + }; + digest.addRule(prefix, vrule); + } + + protected TileSetIDBroker _idBroker; +} diff --git a/src/java/com/threerings/miso/tools/xml/SimpleMisoSceneParser.java b/src/java/com/threerings/miso/tools/xml/SimpleMisoSceneParser.java new file mode 100644 index 00000000..5b005dd6 --- /dev/null +++ b/src/java/com/threerings/miso/tools/xml/SimpleMisoSceneParser.java @@ -0,0 +1,90 @@ +// +// $Id: SimpleMisoSceneParser.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tools.xml; + +import java.io.IOException; +import java.io.FileInputStream; + +import org.xml.sax.SAXException; +import org.apache.commons.digester.Digester; + +import com.samskivert.util.StringUtil; + +import com.threerings.miso.data.SimpleMisoSceneModel; + +/** + * A simple class for parsing simple miso scene models. + */ +public class SimpleMisoSceneParser +{ + /** + * Constructs a scene parser that parses scenes with the specified XML + * path prefix. + */ + public SimpleMisoSceneParser (String prefix) + { + // create and configure our digester + _digester = new Digester(); + + // create our scene rule set + SimpleMisoSceneRuleSet set = new SimpleMisoSceneRuleSet(); + + // configure our top-level path prefix + if (StringUtil.isBlank(prefix)) { + _prefix = set.getOuterElement(); + } else { + _prefix = prefix + "/" + set.getOuterElement(); + } + + // add the scene rules + set.addRuleInstances(_prefix, _digester); + + // add a rule to grab the finished scene model + _digester.addSetNext( + _prefix, "setScene", SimpleMisoSceneModel.class.getName()); + } + + /** + * Parses the XML file at the specified path into a scene model + * instance. + */ + public SimpleMisoSceneModel parseScene (String path) + throws IOException, SAXException + { + _model = null; + _digester.push(this); + _digester.parse(new FileInputStream(path)); + return _model; + } + + /** + * Called by the parser once the scene is parsed. + */ + public void setScene (SimpleMisoSceneModel model) + { + _model = model; + } + + protected String _prefix; + protected Digester _digester; + protected SimpleMisoSceneModel _model; +} diff --git a/src/java/com/threerings/miso/tools/xml/SimpleMisoSceneRuleSet.java b/src/java/com/threerings/miso/tools/xml/SimpleMisoSceneRuleSet.java new file mode 100644 index 00000000..9210441e --- /dev/null +++ b/src/java/com/threerings/miso/tools/xml/SimpleMisoSceneRuleSet.java @@ -0,0 +1,108 @@ +// +// $Id: SimpleMisoSceneRuleSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tools.xml; + +import java.util.ArrayList; +import org.xml.sax.Attributes; + +import org.apache.commons.digester.Digester; +import org.apache.commons.digester.Rule; + +import com.samskivert.xml.CallMethodSpecialRule; +import com.samskivert.xml.SetFieldRule; +import com.samskivert.xml.SetPropertyFieldsRule; + +import com.threerings.tools.xml.NestableRuleSet; + +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.data.SimpleMisoSceneModel; + +/** + * Used to parse a {@link SimpleMisoSceneModel} from XML. + */ +public class SimpleMisoSceneRuleSet implements NestableRuleSet +{ + // documentation inherited from interface + public String getOuterElement () + { + return SimpleMisoSceneWriter.OUTER_ELEMENT; + } + + // documentation inherited from interface + public void addRuleInstances (String prefix, Digester dig) + { + // this creates the appropriate instance when we encounter our + // prefix tag + dig.addRule(prefix, new Rule() { + public void begin (String namespace, String name, + Attributes attributes) throws Exception { + digester.push(createMisoSceneModel()); + } + public void end (String namespace, String name) throws Exception { + digester.pop(); + } + }); + + // set up rules to parse and set our fields + dig.addRule(prefix + "/width", new SetFieldRule("width")); + dig.addRule(prefix + "/height", new SetFieldRule("height")); + dig.addRule(prefix + "/viewwidth", new SetFieldRule("vwidth")); + dig.addRule(prefix + "/viewheight", new SetFieldRule("vheight")); + dig.addRule(prefix + "/base", new SetFieldRule("baseTileIds")); + + dig.addObjectCreate(prefix + "/objects", ArrayList.class.getName()); + dig.addObjectCreate(prefix + "/objects/object", + ObjectInfo.class.getName()); + dig.addSetNext(prefix + "/objects/object", "add", + Object.class.getName()); + + dig.addRule(prefix + "/objects/object", new SetPropertyFieldsRule()); + + dig.addRule(prefix + "/objects", new CallMethodSpecialRule() { + public void parseAndSet (String bodyText, Object target) + throws Exception + { + ArrayList ilist = (ArrayList)target; + ArrayList ulist = new ArrayList(); + SimpleMisoSceneModel model = (SimpleMisoSceneModel) + digester.peek(1); + + // filter interesting and uninteresting into two lists + for (int ii = 0; ii < ilist.size(); ii++) { + ObjectInfo info = (ObjectInfo)ilist.get(ii); + if (!info.isInteresting()) { + ilist.remove(ii--); + ulist.add(info); + } + } + + // now populate the model + SimpleMisoSceneModel.populateObjects(model, ilist, ulist); + } + }); + } + + protected SimpleMisoSceneModel createMisoSceneModel () + { + return new SimpleMisoSceneModel(0, 0, 0, 0); + } +} diff --git a/src/java/com/threerings/miso/tools/xml/SimpleMisoSceneWriter.java b/src/java/com/threerings/miso/tools/xml/SimpleMisoSceneWriter.java new file mode 100644 index 00000000..1af46bba --- /dev/null +++ b/src/java/com/threerings/miso/tools/xml/SimpleMisoSceneWriter.java @@ -0,0 +1,116 @@ +// +// $Id: SimpleMisoSceneWriter.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tools.xml; + +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import com.megginson.sax.DataWriter; +import com.samskivert.util.StringUtil; +import com.threerings.tools.xml.NestableWriter; + +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.data.SimpleMisoSceneModel; + +/** + * Generates an XML representation of a {@link SimpleMisoSceneModel}. + */ +public class SimpleMisoSceneWriter implements NestableWriter +{ + /** The element used to enclose scene models written with this + * writer. */ + public static final String OUTER_ELEMENT = "miso"; + + // documentation inherited from interface + public void write (Object object, DataWriter writer) + throws SAXException + { + SimpleMisoSceneModel model = (SimpleMisoSceneModel)object; + writer.startElement(OUTER_ELEMENT); + writeSceneData(model, writer); + writer.endElement(OUTER_ELEMENT); + } + + /** + * Writes just the scene data which is handy for derived classes which + * may wish to add their own scene data to the scene output. + */ + protected void writeSceneData (SimpleMisoSceneModel model, + DataWriter writer) + throws SAXException + { + writer.dataElement("width", Integer.toString(model.width)); + writer.dataElement("height", Integer.toString(model.height)); + writer.dataElement("viewwidth", Integer.toString(model.vwidth)); + writer.dataElement("viewheight", Integer.toString(model.vheight)); + writer.dataElement("base", + StringUtil.toString(model.baseTileIds, "", "")); + + // write our uninteresting object tile information + writer.startElement("objects"); + for (int ii = 0; ii < model.objectTileIds.length; ii++) { + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute("", "tileId", "", "", + String.valueOf(model.objectTileIds[ii])); + attrs.addAttribute("", "x", "", "", + String.valueOf(model.objectXs[ii])); + attrs.addAttribute("", "y", "", "", + String.valueOf(model.objectYs[ii])); + writer.emptyElement("", "object", "", attrs); + } + + // write our uninteresting object tile information + for (int ii = 0; ii < model.objectInfo.length; ii++) { + ObjectInfo info = model.objectInfo[ii]; + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute("", "tileId", "", "", + String.valueOf(info.tileId)); + attrs.addAttribute("", "x", "", "", String.valueOf(info.x)); + attrs.addAttribute("", "y", "", "", String.valueOf(info.y)); + + if (!StringUtil.isBlank(info.action)) { + attrs.addAttribute("", "action", "", "", info.action); + } + + if (info.priority != 0) { + attrs.addAttribute("", "priority", "", "", + String.valueOf(info.priority)); + } + + if (info.sx != 0 || info.sy != 0) { + attrs.addAttribute("", "sx", "", "", + String.valueOf(info.sx)); + attrs.addAttribute("", "sy", "", "", + String.valueOf(info.sy)); + attrs.addAttribute("", "sorient", "", "", + String.valueOf(info.sorient)); + } + + if (info.zations != 0) { + attrs.addAttribute("", "zations", "", "", + String.valueOf(info.zations)); + } + writer.emptyElement("", "object", "", attrs); + } + writer.endElement("objects"); + } +} diff --git a/src/java/com/threerings/miso/tools/xml/SparseMisoSceneParser.java b/src/java/com/threerings/miso/tools/xml/SparseMisoSceneParser.java new file mode 100644 index 00000000..4151feb3 --- /dev/null +++ b/src/java/com/threerings/miso/tools/xml/SparseMisoSceneParser.java @@ -0,0 +1,97 @@ +// +// $Id: SparseMisoSceneParser.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tools.xml; + +import java.io.IOException; +import java.io.FileInputStream; + +import org.xml.sax.SAXException; +import org.apache.commons.digester.Digester; + +import com.samskivert.io.StreamUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.miso.data.SparseMisoSceneModel; + +/** + * A simple class for parsing simple miso scene models. + */ +public class SparseMisoSceneParser +{ + /** + * Constructs a scene parser that parses scenes with the specified XML + * path prefix. + */ + public SparseMisoSceneParser (String prefix) + { + // create and configure our digester + _digester = new Digester(); + + // create our scene rule set + SparseMisoSceneRuleSet set = new SparseMisoSceneRuleSet(); + + // configure our top-level path prefix + if (StringUtil.isBlank(prefix)) { + _prefix = set.getOuterElement(); + } else { + _prefix = prefix + "/" + set.getOuterElement(); + } + + // add the scene rules + set.addRuleInstances(_prefix, _digester); + + // add a rule to grab the finished scene model + _digester.addSetNext( + _prefix, "setScene", SparseMisoSceneModel.class.getName()); + } + + /** + * Parses the XML file at the specified path into a scene model + * instance. + */ + public SparseMisoSceneModel parseScene (String path) + throws IOException, SAXException + { + _model = null; + _digester.push(this); + FileInputStream stream = null; + try { + stream = new FileInputStream(path); + _digester.parse(stream); + } finally { + StreamUtil.close(stream); + } + return _model; + } + + /** + * Called by the parser once the scene is parsed. + */ + public void setScene (SparseMisoSceneModel model) + { + _model = model; + } + + protected String _prefix; + protected Digester _digester; + protected SparseMisoSceneModel _model; +} diff --git a/src/java/com/threerings/miso/tools/xml/SparseMisoSceneRuleSet.java b/src/java/com/threerings/miso/tools/xml/SparseMisoSceneRuleSet.java new file mode 100644 index 00000000..f3a1ba0f --- /dev/null +++ b/src/java/com/threerings/miso/tools/xml/SparseMisoSceneRuleSet.java @@ -0,0 +1,85 @@ +// +// $Id: SparseMisoSceneRuleSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tools.xml; + +import org.xml.sax.Attributes; + +import org.apache.commons.digester.Digester; +import org.apache.commons.digester.Rule; + +import com.samskivert.xml.SetFieldRule; +import com.samskivert.xml.SetPropertyFieldsRule; + +import com.threerings.tools.xml.NestableRuleSet; + +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.data.SparseMisoSceneModel.Section; +import com.threerings.miso.data.SparseMisoSceneModel; + +/** + * Used to parse a {@link SparseMisoSceneModel} from XML. + */ +public class SparseMisoSceneRuleSet implements NestableRuleSet +{ + // documentation inherited from interface + public String getOuterElement () + { + return SparseMisoSceneWriter.OUTER_ELEMENT; + } + + // documentation inherited from interface + public void addRuleInstances (String prefix, Digester dig) + { + // this creates the appropriate instance when we encounter our + // prefix tag + dig.addRule(prefix, new Rule() { + public void begin (String namespace, String name, + Attributes attributes) throws Exception { + digester.push(createMisoSceneModel()); + } + public void end (String namespace, String name) throws Exception { + digester.pop(); + } + }); + + // set up rules to parse and set our fields + dig.addRule(prefix + "/swidth", new SetFieldRule("swidth")); + dig.addRule(prefix + "/sheight", new SetFieldRule("sheight")); + dig.addRule(prefix + "/defTileSet", new SetFieldRule("defTileSet")); + + String sprefix = prefix + "/sections/section"; + dig.addObjectCreate(sprefix, Section.class.getName()); + dig.addRule(sprefix, new SetPropertyFieldsRule()); + dig.addRule(sprefix + "/base", new SetFieldRule("baseTileIds")); + dig.addObjectCreate(sprefix + "/objects/object", + ObjectInfo.class.getName()); + dig.addRule(sprefix + "/objects/object", new SetPropertyFieldsRule()); + dig.addSetNext(sprefix + "/objects/object", "addObject", + ObjectInfo.class.getName()); + dig.addSetNext(sprefix, "setSection", Section.class.getName()); + } + + protected SparseMisoSceneModel createMisoSceneModel () + { + return new SparseMisoSceneModel(); + } +} diff --git a/src/java/com/threerings/miso/tools/xml/SparseMisoSceneWriter.java b/src/java/com/threerings/miso/tools/xml/SparseMisoSceneWriter.java new file mode 100644 index 00000000..686de7a9 --- /dev/null +++ b/src/java/com/threerings/miso/tools/xml/SparseMisoSceneWriter.java @@ -0,0 +1,131 @@ +// +// $Id: SparseMisoSceneWriter.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.tools.xml; + +import java.util.Iterator; + +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import com.megginson.sax.DataWriter; +import com.samskivert.util.StringUtil; +import com.threerings.tools.xml.NestableWriter; + +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.data.SparseMisoSceneModel.Section; +import com.threerings.miso.data.SparseMisoSceneModel; + +/** + * Generates an XML representation of a {@link SparseMisoSceneModel}. + */ +public class SparseMisoSceneWriter implements NestableWriter +{ + /** The element used to enclose scene models written with this + * writer. */ + public static final String OUTER_ELEMENT = "miso"; + + // documentation inherited from interface + public void write (Object object, DataWriter writer) + throws SAXException + { + SparseMisoSceneModel model = (SparseMisoSceneModel)object; + writer.startElement(OUTER_ELEMENT); + writeSceneData(model, writer); + writer.endElement(OUTER_ELEMENT); + } + + /** + * Writes just the scene data which is handy for derived classes which + * may wish to add their own scene data to the scene output. + */ + protected void writeSceneData (SparseMisoSceneModel model, + DataWriter writer) + throws SAXException + { + writer.dataElement("swidth", Integer.toString(model.swidth)); + writer.dataElement("sheight", Integer.toString(model.sheight)); + writer.dataElement("defTileSet", Integer.toString(model.defTileSet)); + + writer.startElement("sections"); + for (Iterator iter = model.getSections(); iter.hasNext(); ) { + Section sect = (Section)iter.next(); + if (sect.isBlank()) { + continue; + } + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute("", "x", "", "", String.valueOf(sect.x)); + attrs.addAttribute("", "y", "", "", String.valueOf(sect.y)); + attrs.addAttribute("", "width", "", "", String.valueOf(sect.width)); + writer.startElement("", "section", "", attrs); + + writer.dataElement( + "base", StringUtil.toString(sect.baseTileIds, "", "")); + + // write our uninteresting object tile information + writer.startElement("objects"); + for (int ii = 0; ii < sect.objectTileIds.length; ii++) { + attrs = new AttributesImpl(); + attrs.addAttribute("", "tileId", "", "", + String.valueOf(sect.objectTileIds[ii])); + attrs.addAttribute("", "x", "", "", + String.valueOf(sect.objectXs[ii])); + attrs.addAttribute("", "y", "", "", + String.valueOf(sect.objectYs[ii])); + writer.emptyElement("", "object", "", attrs); + } + + // write our interesting object tile information + for (int ii = 0; ii < sect.objectInfo.length; ii++) { + ObjectInfo info = sect.objectInfo[ii]; + attrs = new AttributesImpl(); + attrs.addAttribute("", "tileId", "", "", + String.valueOf(info.tileId)); + attrs.addAttribute("", "x", "", "", String.valueOf(info.x)); + attrs.addAttribute("", "y", "", "", String.valueOf(info.y)); + + if (!StringUtil.isBlank(info.action)) { + attrs.addAttribute("", "action", "", "", info.action); + } + if (info.priority != 0) { + attrs.addAttribute("", "priority", "", "", + String.valueOf(info.priority)); + } + if (info.sx != 0 || info.sy != 0) { + attrs.addAttribute("", "sx", "", "", + String.valueOf(info.sx)); + attrs.addAttribute("", "sy", "", "", + String.valueOf(info.sy)); + attrs.addAttribute("", "sorient", "", "", + String.valueOf(info.sorient)); + } + if (info.zations != 0) { + attrs.addAttribute("", "zations", "", "", + String.valueOf(info.zations)); + } + writer.emptyElement("", "object", "", attrs); + } + writer.endElement("objects"); + writer.endElement("section"); + } + writer.endElement("sections"); + } +} diff --git a/src/java/com/threerings/miso/util/MisoContext.java b/src/java/com/threerings/miso/util/MisoContext.java new file mode 100644 index 00000000..267dfdc6 --- /dev/null +++ b/src/java/com/threerings/miso/util/MisoContext.java @@ -0,0 +1,43 @@ +// +// $Id: MisoContext.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.util; + +import com.threerings.media.FrameManager; +import com.threerings.miso.tile.MisoTileManager; + +/** + * Provides Miso code with access to the managers that it needs to do its + * thing. + */ +public interface MisoContext +{ + /** + * Returns the frame manager that our scene panel will interact with. + */ + public FrameManager getFrameManager (); + + /** + * Returns a reference to the tile manager. This reference is valid + * for the lifetime of the application. + */ + public MisoTileManager getTileManager (); +} diff --git a/src/java/com/threerings/miso/util/MisoSceneMetrics.java b/src/java/com/threerings/miso/util/MisoSceneMetrics.java new file mode 100644 index 00000000..b2f3facb --- /dev/null +++ b/src/java/com/threerings/miso/util/MisoSceneMetrics.java @@ -0,0 +1,101 @@ +// +// $Id: MisoSceneMetrics.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.util; + +import com.threerings.miso.client.MisoScenePanel; + +/** + * Contains information on the configuration of a particular isometric + * view. The member data are public to facilitate convenient referencing + * by the {@link MisoScenePanel} class, the values should not be modified + * once the metrics are constructed. + */ +public class MisoSceneMetrics +{ + /** Tile dimensions and half-dimensions in the view. */ + public int tilewid, tilehei, tilehwid, tilehhei; + + /** Fine coordinate dimensions. */ + public int finehwid, finehhei; + + /** Number of fine coordinates on each axis within a tile. */ + public int finegran; + + /** Dimensions of our scene blocks in tile count. */ + public short blockwid = 4, blockhei = 4; + + /** The length of a tile edge in pixels. */ + public float tilelen; + + /** The slope of the x- and y-axis lines. */ + public float slopeX, slopeY; + + /** The length between fine coordinates in pixels. */ + public float finelen; + + /** The y-intercept of the x-axis line within a tile. */ + public float fineBX; + + /** The slope of the x- and y-axis lines within a tile. */ + public float fineSlopeX, fineSlopeY; + + /** + * Constructs scene metrics by directly specifying the desired config + * parameters. + * + * @param tilewid the width in pixels of the tiles. + * @param tilehei the height in pixels of the tiles. + * @param finegran the number of sub-tile divisions to use for fine + * coordinates. + */ + public MisoSceneMetrics (int tilewid, int tilehei, int finegran) + { + // keep track of this stuff + this.tilewid = tilewid; + this.tilehei = tilehei; + this.finegran = finegran; + + // halve the dimensions + tilehwid = (tilewid / 2); + tilehhei = (tilehei / 2); + + // calculate the length of a tile edge in pixels + tilelen = (float) Math.sqrt( + (tilehwid * tilehwid) + (tilehhei * tilehhei)); + + // calculate the slope of the x- and y-axis lines + slopeX = (float)tilehei / (float)tilewid; + slopeY = -slopeX; + + // calculate the edge length separating each fine coordinate + finelen = tilelen / (float)finegran; + + // calculate the fine-coordinate x-axis line + fineSlopeX = (float)tilehei / (float)tilewid; + fineBX = -(fineSlopeX * (float)tilehwid); + fineSlopeY = -fineSlopeX; + + // calculate the fine coordinate dimensions + finehwid = (int)((float)tilehwid / (float)finegran); + finehhei = (int)((float)tilehhei / (float)finegran); + } +} diff --git a/src/java/com/threerings/miso/util/MisoUtil.java b/src/java/com/threerings/miso/util/MisoUtil.java new file mode 100644 index 00000000..64108264 --- /dev/null +++ b/src/java/com/threerings/miso/util/MisoUtil.java @@ -0,0 +1,518 @@ +// +// $Id: MisoUtil.java 3628 2005-06-28 17:42:55Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.util; + +import java.awt.Point; +import java.awt.Polygon; + +import com.samskivert.swing.SmartPolygon; + +import com.threerings.util.DirectionCodes; +import com.threerings.util.DirectionUtil; + +import com.threerings.media.util.MathUtil; + +/** + * Miscellaneous isometric-display-related utility routines. + */ +public class MisoUtil + implements DirectionCodes +{ + /** + * Given two points in screen pixel coordinates, return the + * compass direction that point B lies in from point A from an + * isometric perspective. + * + * @param ax the x-position of point A. + * @param ay the y-position of point A. + * @param bx the x-position of point B. + * @param by the y-position of point B. + * + * @return the direction specified as one of the Sprite + * class's direction constants. + */ + public static int getDirection ( + MisoSceneMetrics metrics, int ax, int ay, int bx, int by) + { + Point afpos = new Point(), bfpos = new Point(); + + // convert screen coordinates to full coordinates to get both + // tile coordinates and fine coordinates + screenToFull(metrics, ax, ay, afpos); + screenToFull(metrics, bx, by, bfpos); + + // pull out the tile coordinates for each point + int tax = fullToTile(afpos.x); + int tay = fullToTile(afpos.y); + + int tbx = fullToTile(bfpos.x); + int tby = fullToTile(bfpos.y); + + // compare tile coordinates to determine direction + int dir = getIsoDirection(tax, tay, tbx, tby); + if (dir != DirectionCodes.NONE) { + return dir; + } + + // destination point is in the same tile as the + // origination point, so consider fine coordinates + + // pull out the fine coordinates for each point + int fax = afpos.x - (tax * FULL_TILE_FACTOR); + int fay = afpos.y - (tay * FULL_TILE_FACTOR); + + int fbx = bfpos.x - (tbx * FULL_TILE_FACTOR); + int fby = bfpos.y - (tby * FULL_TILE_FACTOR); + + // compare fine coordinates to determine direction + dir = getIsoDirection(fax, fay, fbx, fby); + + // arbitrarily return southwest if fine coords were also equivalent + return (dir == -1) ? SOUTHWEST : dir; + } + + /** + * Given two points in an isometric coordinate system (in which {@link + * #NORTH} is in the direction of the negative x-axis and {@link + * #WEST} in the direction of the negative y-axis), return the compass + * direction that point B lies in from point A. This method is used + * to determine direction for both tile coordinates and fine + * coordinates within a tile, since the coordinate systems are the + * same. + * + * @param ax the x-position of point A. + * @param ay the y-position of point A. + * @param bx the x-position of point B. + * @param by the y-position of point B. + * + * @return the direction specified as one of the Sprite + * class's direction constants, or DirectionCodes.NONE if + * point B is equivalent to point A. + */ + public static int getIsoDirection (int ax, int ay, int bx, int by) + { + // head off a div by 0 at the pass.. + if (bx == ax) { + if (by == ay) { + return DirectionCodes.NONE; + } + return (by < ay) ? EAST : WEST; + } + + // figure direction base on the slope of the line + float slope = ((float) (ay - by)) / ((float) Math.abs(ax - bx)); + if (slope > 2f) { + return EAST; + } + if (slope > .5f) { + return (bx < ax) ? NORTHEAST : SOUTHEAST; + } + if (slope > -.5f) { + return (bx < ax) ? NORTH : SOUTH; + } + if (slope > -2f) { + return (bx < ax) ? NORTHWEST : SOUTHWEST; + } + return WEST; + } + + /** + * Given two points in screen coordinates, return the isometrically + * projected compass direction that point B lies in from point A. + * + * @param ax the x-position of point A. + * @param ay the y-position of point A. + * @param bx the x-position of point B. + * @param by the y-position of point B. + * + * @return the direction specified as one of the Sprite + * class's direction constants, or DirectionCodes.NONE if + * point B is equivalent to point A. + */ + public static int getProjectedIsoDirection (int ax, int ay, int bx, int by) + { + return toIsoDirection(DirectionUtil.getDirection(ax, ay, bx, by)); + } + + /** + * Converts a non-isometric orientation (where north points toward the + * top of the screen) to an isometric orientation where north points + * toward the upper-left corner of the screen. + */ + public static int toIsoDirection (int dir) + { + if (dir != DirectionCodes.NONE) { + // rotate the direction clockwise (ie. change SOUTHEAST to + // SOUTH) + dir = DirectionUtil.rotateCW(dir, 2); + } + return dir; + } + + /** + * Returns the tile coordinate of the given full coordinate. + */ + public static int fullToTile (int val) + { + return MathUtil.floorDiv(val, FULL_TILE_FACTOR); + } + + /** + * Returns the fine coordinate of the given full coordinate. + */ + public static int fullToFine (int val) + { + return (val - (fullToTile(val) * FULL_TILE_FACTOR)); + } + + /** + * Convert the given screen-based pixel coordinates to their + * corresponding tile-based coordinates. Converted coordinates + * are placed in the given point object. + * + * @param sx the screen x-position pixel coordinate. + * @param sy the screen y-position pixel coordinate. + * @param tpos the point object to place coordinates in. + * + * @return the point instance supplied via the tpos + * parameter. + */ + public static Point screenToTile ( + MisoSceneMetrics metrics, int sx, int sy, Point tpos) + { + // determine the upper-left of the quadrant that contains our + // point + int zx = (int)Math.floor((float)sx / metrics.tilewid); + int zy = (int)Math.floor((float)sy / metrics.tilehei); + + // these are the screen coordinates of the tile's top + int ox = (zx * metrics.tilewid), oy = (zy * metrics.tilehei); + + // these are the tile coordinates + tpos.x = zy + zx; tpos.y = zy - zx; + + // now determine which of the four tiles our point occupies + int dx = sx - ox, dy = sy - oy; + + if (Math.round(metrics.slopeY * dx + metrics.tilehei) <= dy) { + tpos.x += 1; + } + + if (Math.round(metrics.slopeX * dx) > dy) { + tpos.y -= 1; + } + +// Log.info("Converted [sx=" + sx + ", sy=" + sy + +// ", zx=" + zx + ", zy=" + zy + +// ", ox=" + ox + ", oy=" + oy + +// ", dx=" + dx + ", dy=" + dy + +// ", tpos.x=" + tpos.x + ", tpos.y=" + tpos.y + "]."); + return tpos; + } + + /** + * Convert the given tile-based coordinates to their corresponding + * screen-based pixel coordinates. The screen coordinate for a tile is + * the upper-left coordinate of the rectangle that bounds the tile + * polygon. Converted coordinates are placed in the given point + * object. + * + * @param x the tile x-position coordinate. + * @param y the tile y-position coordinate. + * @param spos the point object to place coordinates in. + * + * @return the point instance supplied via the spos + * parameter. + */ + public static Point tileToScreen ( + MisoSceneMetrics metrics, int x, int y, Point spos) + { + spos.x = (x - y - 1) * metrics.tilehwid; + spos.y = (x + y) * metrics.tilehhei; + return spos; + } + + /** + * Convert the given fine coordinates to pixel coordinates within + * the containing tile. Converted coordinates are placed in the + * given point object. + * + * @param x the x-position fine coordinate. + * @param y the y-position fine coordinate. + * @param ppos the point object to place coordinates in. + */ + public static void fineToPixel ( + MisoSceneMetrics metrics, int x, int y, Point ppos) + { + ppos.x = metrics.tilehwid + ((x - y) * metrics.finehwid); + ppos.y = (x + y) * metrics.finehhei; + } + + /** + * Convert the given pixel coordinates, whose origin is at the + * top-left of a tile's containing rectangle, to fine coordinates + * within that tile. Converted coordinates are placed in the + * given point object. + * + * @param x the x-position pixel coordinate. + * @param y the y-position pixel coordinate. + * @param fpos the point object to place coordinates in. + */ + public static void pixelToFine ( + MisoSceneMetrics metrics, int x, int y, Point fpos) + { + // calculate line parallel to the y-axis (from the given + // x/y-pos to the x-axis) + float bY = y - (metrics.fineSlopeY * x); + + // determine intersection of x- and y-axis lines + int crossx = (int)((bY - metrics.fineBX) / + (metrics.fineSlopeX - metrics.fineSlopeY)); + int crossy = (int)((metrics.fineSlopeY * crossx) + bY); + + // TODO: final position should check distance between our + // position and the surrounding fine coords and return the + // actual closest fine coord, rather than just dividing. + + // determine distance along the x-axis + float xdist = MathUtil.distance(metrics.tilehwid, 0, crossx, crossy); + fpos.x = (int)(xdist / metrics.finelen); + + // determine distance along the y-axis + float ydist = MathUtil.distance(x, y, crossx, crossy); + fpos.y = (int)(ydist / metrics.finelen); + +// Log.info("Pixel to fine " + StringUtil.coordsToString(x, y) + +// " -> " + StringUtil.toString(fpos) + "."); + } + + /** + * Convert the given screen-based pixel coordinates to full + * scene-based coordinates that include both the tile coordinates + * and the fine coordinates in each dimension. Converted + * coordinates are placed in the given point object. + * + * @param sx the screen x-position pixel coordinate. + * @param sy the screen y-position pixel coordinate. + * @param fpos the point object to place coordinates in. + * + * @return the point passed in to receive the coordinates. + */ + public static Point screenToFull ( + MisoSceneMetrics metrics, int sx, int sy, Point fpos) + { + // get the tile coordinates + Point tpos = new Point(); + screenToTile(metrics, sx, sy, tpos); + + // get the screen coordinates for the containing tile + Point spos = tileToScreen(metrics, tpos.x, tpos.y, new Point()); + +// Log.info("Screen to full " + +// "[screen=" + StringUtil.coordsToString(sx, sy) + +// ", tpos=" + StringUtil.toString(tpos) + +// ", spos=" + StringUtil.toString(spos) + +// ", fpix=" + StringUtil.coordsToString( +// sx-spos.x, sy-spos.y) + "]."); + + // get the fine coordinates within the containing tile + pixelToFine(metrics, sx - spos.x, sy - spos.y, fpos); + + // toss in the tile coordinates for good measure + fpos.x += (tpos.x * FULL_TILE_FACTOR); + fpos.y += (tpos.y * FULL_TILE_FACTOR); + + return fpos; + } + + /** + * Convert the given full coordinates to screen-based pixel + * coordinates. Converted coordinates are placed in the given + * point object. + * + * @param x the x-position full coordinate. + * @param y the y-position full coordinate. + * @param spos the point object to place coordinates in. + * + * @return the point passed in to receive the coordinates. + */ + public static Point fullToScreen ( + MisoSceneMetrics metrics, int x, int y, Point spos) + { + // get the tile screen position + int tx = fullToTile(x), ty = fullToTile(y); + Point tspos = tileToScreen(metrics, tx, ty, new Point()); + + // get the pixel position of the fine coords within the tile + Point ppos = new Point(); + int fx = x - (tx * FULL_TILE_FACTOR), fy = y - (ty * FULL_TILE_FACTOR); + fineToPixel(metrics, fx, fy, ppos); + + // final position is tile position offset by fine position + spos.x = tspos.x + ppos.x; + spos.y = tspos.y + ppos.y; + + return spos; + } + + /** + * Converts the given fine coordinate to a full coordinate (a tile + * coordinate plus a fine coordinate remainder). The fine coordinate + * is assumed to be relative to tile (0, 0). + */ + public static int fineToFull (MisoSceneMetrics metrics, int fine) + { + return toFull(fine / metrics.finegran, fine % metrics.finegran); + } + + /** + * Composes the supplied tile coordinate and fine coordinate offset + * into a full coordinate. + */ + public static int toFull (int tile, int fine) + { + return tile * FULL_TILE_FACTOR + fine; + } + + /** + * Return a polygon framing the specified tile. + * + * @param x the tile x-position coordinate. + * @param y the tile y-position coordinate. + */ + public static Polygon getTilePolygon ( + MisoSceneMetrics metrics, int x, int y) + { + return getFootprintPolygon(metrics, x, y, 1, 1); + } + + /** + * Return a screen-coordinates polygon framing the two specified + * tile-coordinate points. + */ + public static Polygon getMultiTilePolygon (MisoSceneMetrics metrics, + Point sp1, Point sp2) + { + int x = Math.min(sp1.x, sp2.x), y = Math.min(sp1.y, sp2.y); + int width = Math.abs(sp1.x-sp2.x)+1, height = Math.abs(sp1.y-sp2.y)+1; + return getFootprintPolygon(metrics, x, y, width, height); + } + + /** + * Returns a polygon framing the specified scene footprint. + * + * @param x the x tile coordinate of the "upper-left" of the footprint. + * @param y the y tile coordinate of the "upper-left" of the footprint. + * @param width the width in tiles of the footprint. + * @param height the height in tiles of the footprint. + */ + public static Polygon getFootprintPolygon ( + MisoSceneMetrics metrics, int x, int y, int width, int height) + { + SmartPolygon footprint = new SmartPolygon(); + Point tpos = MisoUtil.tileToScreen(metrics, x, y, new Point()); + + // start with top-center point + int rx = tpos.x + metrics.tilehwid, ry = tpos.y; + footprint.addPoint(rx, ry); + // right point + rx += width * metrics.tilehwid; + ry += width * metrics.tilehhei; + footprint.addPoint(rx, ry); + // bottom-center point + rx -= height * metrics.tilehwid; + ry += height * metrics.tilehhei; + footprint.addPoint(rx, ry); + // left point + rx -= width * metrics.tilehwid; + ry -= width * metrics.tilehhei; + footprint.addPoint(rx, ry); + // end with top-center point + rx += height * metrics.tilehwid; + ry -= height * metrics.tilehhei; + footprint.addPoint(rx, ry); + + return footprint; + } + + /** + * Adds the supplied fine coordinates to the supplied tile coordinates + * to compute full coordinates. + * + * @return the point object supplied as full. + */ + public static Point tilePlusFineToFull (MisoSceneMetrics metrics, + int tileX, int tileY, + int fineX, int fineY, + Point full) + { + int dtx = fineX / metrics.finegran; + int dty = fineY / metrics.finegran; + int fx = fineX - dtx * metrics.finegran; + if (fx < 0) { + dtx--; + fx += metrics.finegran; + } + int fy = fineY - dty * metrics.finegran; + if (fy < 0) { + dty--; + fy += metrics.finegran; + } + + full.x = toFull(tileX + dtx, fx); + full.y = toFull(tileY + dty, fy); + return full; + } + + /** + * Turns x and y scene coordinates into an integer key. + * + * @return the hash key, given x and y. + */ + public static final int coordsToKey (int x, int y) + { + return ((y << 16) & (0xFFFF0000)) | (x & 0xFFFF); + } + + /** + * Gets the x coordinate from an integer hash key. + * + * @return the x coordinate. + */ + public static final int xCoordFromKey (int key) + { + return (key & 0xFFFF); + } + + /** + * Gets the y coordinate from an integer hash key. + * + * @return the y coordinate from the hash key. + */ + public static final int yCoordFromKey (int key) + { + return ((key >> 16) & 0xFFFF); + } + + /** Multiplication factor to embed tile coords in full coords. */ + protected static final int FULL_TILE_FACTOR = 100; +} diff --git a/src/java/com/threerings/miso/util/ObjectSet.java b/src/java/com/threerings/miso/util/ObjectSet.java new file mode 100644 index 00000000..4734b259 --- /dev/null +++ b/src/java/com/threerings/miso/util/ObjectSet.java @@ -0,0 +1,182 @@ +// +// $Id: ObjectSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.util; + +import java.util.Arrays; +import java.util.Comparator; + +import com.samskivert.util.ArrayUtil; +import com.samskivert.util.ListUtil; + +import com.threerings.miso.Log; +import com.threerings.miso.data.ObjectInfo; + +/** + * Used to store an (arbitrarily) ordered, low-impact iteratable (doesn't + * require object creation), set of {@link ObjectInfo} instances. + */ +public class ObjectSet +{ + /** + * Inserts the supplied object into the set. + * + * @return true if it was inserted, false if the object was already in + * the set. + */ + public boolean insert (ObjectInfo info) + { + // bail if it's already in the set + int ipos = indexOf(info); + if (ipos >= 0) { + // log a warning because the caller shouldn't be doing this + Log.warning("Requested to add an object to a set that already " + + "contains such an object [ninfo=" + info + + ", oinfo=" + _objs[ipos] + "]."); + Thread.dumpStack(); + return false; + } + + // otherwise insert it + ipos = -(ipos+1); + _objs = ListUtil.insert(_objs, ipos, info); + _size++; + return true; + } + + /** + * Returns true if the specified object is in the set, false if it is + * not. + */ + public boolean contains (ObjectInfo info) + { + return (indexOf(info) >= 0); + } + + /** + * Returns the number of objects in this set. + */ + public int size () + { + return _size; + } + + /** + * Returns the object with the specified index. The index must & be + * between 0 and {@link #size}-1. + */ + public ObjectInfo get (int index) + { + return (ObjectInfo)_objs[index]; + } + + /** + * Removes the object at the specified index. + */ + public void remove (int index) + { + ListUtil.remove(_objs, index); + _size--; + } + + /** + * Removes the specified object from the set. + * + * @return true if it was removed, false if it was not in the set. + */ + public boolean remove (ObjectInfo info) + { + int opos = indexOf(info); + if (opos >= 0) { + remove(opos); + return true; + } else { + return false; + } + } + + /** + * Clears out the contents of this set. + */ + public void clear () + { + _size = 0; + Arrays.fill(_objs, null); + } + + /** + * Converts the contents of this object set to an array. + */ + public ObjectInfo[] toArray () + { + ObjectInfo[] info = new ObjectInfo[_size]; + System.arraycopy(_objs, 0, info, 0, _size); + return info; + } + + /** + * Returns a string representation of this instance. + */ + public String toString () + { + StringBuilder buf = new StringBuilder("["); + for (int ii = 0; ii < _size; ii++) { + if (ii > 0) { + buf.append(", "); + } + buf.append(_objs[ii]); + } + return buf.append("]").toString(); + } + + /** + * Returns the index of the object or it's insertion index if it is + * not in the set. + */ + protected final int indexOf (ObjectInfo info) + { + return ArrayUtil.binarySearch(_objs, 0, _size, info, INFO_COMP); + } + + /** Our sorted array of objects. */ + protected Object[] _objs = new Object[DEFAULT_SIZE]; + + /** The number of objects in the set. */ + protected int _size; + + /** We simply sort the objects in order of their hash code. We don't + * care about their order, it exists only to support binary search. */ + protected static final Comparator INFO_COMP = new Comparator() { + public int compare (Object o1, Object o2) { + ObjectInfo do1 = (ObjectInfo)o1; + ObjectInfo do2 = (ObjectInfo)o2; + if (do1.tileId == do2.tileId) { + return ((do1.x << 16) + do1.y) - ((do2.x << 16) + do2.y); + } else { + return do1.tileId - do2.tileId; + } + } + }; + + /** We start big because we know these will in general contain at + * least in the tens of objects. */ + protected static final int DEFAULT_SIZE = 16; +} diff --git a/src/java/com/threerings/openal/BlankSound.java b/src/java/com/threerings/openal/BlankSound.java new file mode 100644 index 00000000..97774428 --- /dev/null +++ b/src/java/com/threerings/openal/BlankSound.java @@ -0,0 +1,38 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.openal; + +/** + * A blank sound used when we cannot initialize the sound system. + */ +public class BlankSound extends Sound +{ + protected BlankSound () + { + super(null, null); + } + + protected boolean play (boolean allowDefer, boolean loop) + { + return false; // nothing doing + } +} diff --git a/src/java/com/threerings/openal/Clip.java b/src/java/com/threerings/openal/Clip.java new file mode 100644 index 00000000..bda9d8af --- /dev/null +++ b/src/java/com/threerings/openal/Clip.java @@ -0,0 +1,41 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.openal; + +import java.nio.ByteBuffer; + +import org.lwjgl.openal.AL10; + +/** + * Contains data for a single sampled sound. + */ +public class Clip +{ + /** The OpenAL format of this clip: {@link AL10#AL_FORMAT_MONO8}, etc. */ + public int format; + + /** The frequency of this clip in samples per second. */ + public int frequency; + + /** The audio data. */ + public ByteBuffer data; +} diff --git a/src/java/com/threerings/openal/ClipBuffer.java b/src/java/com/threerings/openal/ClipBuffer.java new file mode 100644 index 00000000..f78cebe7 --- /dev/null +++ b/src/java/com/threerings/openal/ClipBuffer.java @@ -0,0 +1,287 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.openal; + +import java.io.IOException; +import java.nio.IntBuffer; + +import org.lwjgl.BufferUtils; +import org.lwjgl.openal.AL10; + +import com.samskivert.util.ObserverList; + +/** + * Represents a sound that has been loaded into the OpenAL system. + */ +public class ClipBuffer +{ + /** Used to notify parties interested in when a clip is loaded. */ + public static interface Observer + { + /** Called when a clip has completed loading and is ready to be + * played. */ + public void clipLoaded (ClipBuffer buffer); + + /** Called when a clip has failed to prepare itself for one reason + * or other. */ + public void clipFailed (ClipBuffer buffer); + } + + /** + * Create a key that uniquely identifies this combination of clip + * provider and path. + */ + public static Comparable makeKey (ClipProvider provider, String path) + { + // we'll just use a string, amazing! + return provider + path; + } + + /** + * Creates a new clip buffer with the specified path that will obtain + * its clip data from the specified source. The clip will + * automatically queue itself up to be loaded into memory. + */ + public ClipBuffer (SoundManager manager, ClipProvider provider, String path) + { + _manager = manager; + _provider = provider; + _path = path; + } + + /** + * Returns the unique key for this clip buffer. + */ + public Comparable getKey () + { + return makeKey(_provider, _path); + } + + /** + * Returns the provider used to load this clip. + */ + public ClipProvider getClipProvider () + { + return _provider; + } + + /** + * Returns the path that identifies this sound clip. + */ + public String getPath () + { + return _path; + } + + /** + * Returns true if this buffer is loaded and ready to go. + */ + public boolean isPlayable () + { + return (_state == LOADED); + } + + /** + * Returns the identifier for this clip's buffer or -1 if it is not + * loaded. + */ + public int getBufferId () + { + return (_bufferId == null) ? -1 : _bufferId.get(0); + } + + /** + * Returns the size (in bytes) of this clip as reported by OpenAL. + * This value will not be valid until the clip is bound. + */ + public int getSize () + { + return _size; + } + + /** + * Instructs this buffer to resolve its underlying clip and be ready + * to be played ASAP. + */ + public void resolve (Observer observer) + { + // if we were waiting to unload, cancel that + if (_state == UNLOADING) { + _state = LOADED; + _manager.restoreClip(this); + } + + // if we're already loaded, this is easy + if (_state == LOADED) { + if (observer != null) { + observer.clipLoaded(this); + } + return; + } + + // queue up the observer + if (observer != null) { + _observers.add(observer); + } + + // if we're already loading, we can stop here + if (_state == LOADING) { + return; + } + + // create our OpenAL buffer and then queue ourselves up to have + // our clip data loaded + _bufferId = BufferUtils.createIntBuffer(1); + AL10.alGenBuffers(_bufferId); + int errno = AL10.alGetError(); + if (errno != AL10.AL_NO_ERROR) { + Log.warning("Failed to create buffer [key=" + getKey() + + ", errno=" + errno + "]."); + _bufferId = null; + // queue up a failure notification so that we properly return + // from this method and our sound has a chance to register + // itself as an observer before we jump up and declare failure + _manager.queueClipFailure(this); + + } else { + _state = LOADING; + _manager.queueClipLoad(this); + } + } + + /** + * Frees up the internal audio buffers associated with this clip. + */ + public void dispose () + { + if (_bufferId != null) { + // if there are sources bound to this buffer, we must wait + // for them to be unbound + if (_bound > 0) { + _state = UNLOADING; + return; + } + + // free up our buffer + AL10.alDeleteBuffers(_bufferId); + _bufferId = null; + _state = UNLOADED; + } + } + + /** + * This method is called by the background sound loading thread and + * actually loads the sound data from wherever it cometh. + */ + protected Clip load () + throws IOException + { + return _provider.loadClip(_path); + } + + /** + * This method is called back on the main thread and instructs this + * buffer to bind the clip data to this buffer's OpenAL buffer. + * + * @return true if the binding succeeded, false if we were unable to + * load the sound data into OpenAL. + */ + protected boolean bind (Clip clip) + { + AL10.alBufferData( + _bufferId.get(0), clip.format, clip.data, clip.frequency); + int errno = AL10.alGetError(); + if (errno != AL10.AL_NO_ERROR) { + Log.warning("Failed to bind clip [key=" + getKey() + + ", errno=" + errno + "]."); + failed(); + return false; + } + + _state = LOADED; + _size = AL10.alGetBufferi(_bufferId.get(0), AL10.AL_SIZE); + _observers.apply(new ObserverList.ObserverOp() { + public boolean apply (Object observer) { + ((Observer)observer).clipLoaded(ClipBuffer.this); + return true; + } + }); + _observers.clear(); + return true; + } + + /** + * Called when we fail in some part of the process in resolving our + * clip data. Notifies our observers and resets the clip to the + * UNLOADED state. + */ + protected void failed () + { + if (_bufferId != null) { + AL10.alDeleteBuffers(_bufferId); + _bufferId = null; + } + _state = UNLOADED; + + _observers.apply(new ObserverList.ObserverOp() { + public boolean apply (Object observer) { + ((Observer)observer).clipFailed(ClipBuffer.this); + return true; + } + }); + _observers.clear(); + } + + /** + * Notifies the buffer that a source has been bound to it. + */ + protected void sourceBound () + { + _bound++; + } + + /** + * Notifies the buffer that a source has been unbound from it. + */ + protected void sourceUnbound () + { + // dispose of the buffer when the last source is unbound + if (--_bound == 0 && _state == UNLOADING) { + dispose(); + } + } + + protected SoundManager _manager; + protected ClipProvider _provider; + protected String _path; + protected int _state; + protected IntBuffer _bufferId; + protected int _size; + protected ObserverList _observers = + new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY); + protected int _bound; + + protected static final int UNLOADED = 0; + protected static final int LOADING = 1; + protected static final int LOADED = 2; + protected static final int UNLOADING = 3; +} diff --git a/src/java/com/threerings/openal/ClipProvider.java b/src/java/com/threerings/openal/ClipProvider.java new file mode 100644 index 00000000..855be786 --- /dev/null +++ b/src/java/com/threerings/openal/ClipProvider.java @@ -0,0 +1,33 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.openal; + +import java.io.IOException; + +/** + * Provides a generic mechanism for loading sound data. + */ +public interface ClipProvider +{ + /** Loads the specified clip from the appropriate source. */ + public Clip loadClip (String path) throws IOException; +} diff --git a/src/java/com/threerings/openal/Log.java b/src/java/com/threerings/openal/Log.java new file mode 100644 index 00000000..ff125d44 --- /dev/null +++ b/src/java/com/threerings/openal/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.openal; + +/** + * A placeholder class that contains a reference to the log object used by + * this package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("narya.openal"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/openal/OggFileStream.java b/src/java/com/threerings/openal/OggFileStream.java new file mode 100644 index 00000000..562f4980 --- /dev/null +++ b/src/java/com/threerings/openal/OggFileStream.java @@ -0,0 +1,121 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.openal; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.nio.ByteBuffer; + +import java.util.ArrayList; + +import org.lwjgl.openal.AL10; + +import com.jmex.sound.openAL.objects.util.OggInputStream; + +/** + * An audio stream read from one or more Ogg Vorbis files. + */ +public class OggFileStream extends Stream +{ + /** + * Creates a new Ogg stream for the specified file. + * + * @param loop whether or not to play the file in a continuous loop + * if there's nothing on the queue + */ + public OggFileStream (SoundManager soundmgr, File file, boolean loop) + throws IOException + { + super(soundmgr); + _file = new OggFile(file, loop); + _istream = new OggInputStream(new FileInputStream(file)); + } + + /** + * Adds a file to the queue of files to play. + * + * @param loop if true, play this file in a loop if there's nothing else + * on the queue + */ + public void queueFile (File file, boolean loop) + { + _queue.add(new OggFile(file, loop)); + } + + // documentation inherited + protected int getFormat () + { + return (_istream.getFormat() == OggInputStream.FORMAT_MONO16) ? + AL10.AL_FORMAT_MONO16 : AL10.AL_FORMAT_STEREO16; + } + + // documentation inherited + protected int getFrequency () + { + return _istream.getRate(); + } + + // documentation inherited + protected int populateBuffer (ByteBuffer buf) + throws IOException + { + int read = _istream.read(buf, buf.position(), buf.remaining()); + while (buf.hasRemaining() && (!_queue.isEmpty() || _file.loop)) { + if (!_queue.isEmpty()) { + _file = _queue.remove(0); + } + _istream = new OggInputStream(new FileInputStream(_file.file)); + read = Math.max(0, read); + read += _istream.read(buf, buf.position(), buf.remaining()); + } + return read; + } + + /** The file currently being played. */ + protected OggFile _file; + + /** The underlying Ogg input stream for the current file. */ + protected OggInputStream _istream; + + /** The queue of files to play after the current one. */ + protected ArrayList _queue = new ArrayList(); + + /** A file queued for play. */ + protected class OggFile + { + /** The file to play. */ + public File file; + + /** Whether or not to play the file in a loop when there's nothing + * in the queue. */ + public boolean loop; + + public OggFile (File file, boolean loop) + { + this.file = file; + this.loop = loop; + } + } +} diff --git a/src/java/com/threerings/openal/Sound.java b/src/java/com/threerings/openal/Sound.java new file mode 100644 index 00000000..fbe67ca7 --- /dev/null +++ b/src/java/com/threerings/openal/Sound.java @@ -0,0 +1,298 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.openal; + +import java.nio.FloatBuffer; + +import org.lwjgl.BufferUtils; +import org.lwjgl.openal.AL10; + +/** + * Represents an instance of a sound clip which can be positioned in 3D + * space, gain and pitch adjusted and played or looped. + */ +public class Sound +{ + /** Used to await notification of the starting of a sound which may be + * delayed in loading. */ + public interface StartObserver + { + /** Called when the specified sound has started playing. If + * sound is null then the sound failed to play but soundStarted + * was called anyway to perform whatever actions were waiting + * on the sound. */ + public void soundStarted (Sound sound); + } + + /** + * Returns the buffer of audio data associated with this sound. + */ + public ClipBuffer getBuffer () + { + return _buffer; + } + + /** + * Configures the location of this sound in 3D space. This will not + * affect an already playing sound but will take effect the next time + * it is played. + */ + public void setLocation (float x, float y, float z) + { + if (_position == null) { + _position = BufferUtils.createFloatBuffer(3); + } + _position.put(x).put(y).put(z); + _position.flip(); + } + + /** + * Configures the velocity of this sound in 3D space, in delta + * location per second (see {@link #setLocation}). This will not + * affect an already playing sound but will take effect the next time + * it is played. + */ + public void setVelocity (float dx, float dy, float dz) + { + if (_velocity == null) { + _velocity = BufferUtils.createFloatBuffer(3); + } + _velocity.put(dx).put(dy).put(dz); + _velocity.flip(); + } + + /** + * Configures the sounds's pitch. This value can range from 0.5 to + * 2.0. This will not affect an already playing sound but will take + * effect the next time it is played. + */ + public void setPitch (float pitch) + { + _pitch = pitch; + } + + /** + * Configures the sound's gain (volume). This value can range from 0.0 + * to 1.0 with 1.0 meaning no attenuation, each division by two + * corresponding to a -6db attentuation and each multiplication by two + * corresponding to +6db amplification. This will not affect an + * already playing sound but will take effect the next time it is + * played. + * + *

Note: this value is multiplied by the base gain configured + * in the sound manager. + */ + public void setGain (float gain) + { + _gain = gain; + } + + /** + * Plays this sound from the beginning. While the sound is playing, an + * audio channel will be locked and then freed when the sound + * completes. + * + * @param allowDefer if false, the sound will be played immediately or + * not at all. If true, the sound will be queued up for loading if it + * is currently flushed from the cache and played once loaded. + * + * @return true if the sound could be played and was started (or + * queued up to be loaded and played ASAP if it was specified as + * deferrable) or false if the sound could not be played either + * because it was not ready and deferral was not allowed or because + * too many other sounds were playing concurrently. + */ + public boolean play (boolean allowDefer) + { + return play(allowDefer, false, null); + } + + /** + * Loops this sound, starting from the beginning of the audio data. It + * will continue to loop until {@link #pause}d or {@link #stop}ped. + * While the sound is playing an audio channel will be locked. + * + * @return true if a channel could be obtained to play the sound (and + * the sound was thus started) or false if no channels were available. + */ + public boolean loop (boolean allowDefer) + { + return play(allowDefer, true, null); + } + + /** + * Plays this sound from the beginning, notifying the supplied observer + * when the audio starts. + * + * @param loop whether or not to loop the sampe until {@link #stop}ped. + */ + public void play (StartObserver obs, boolean loop) + { + play(true, loop, obs); + } + + /** + * Pauses this sound. A subsequent call to {@link #play} will resume + * the sound from the precise position that it left off. While the + * sound is paused, its audio channel will remain locked. + */ + public void pause () + { + if (_sourceId != -1) { + AL10.alSourcePause(_sourceId); + } + } + + /** + * Stops this sound and rewinds to its beginning. The audio channel + * being used to play the sound will be released. + */ + public void stop () + { + if (_sourceId != -1) { + AL10.alSourceStop(_sourceId); + } + } + + protected Sound (SoundGroup group, ClipBuffer buffer) + { + _group = group; + _buffer = buffer; + } + + protected boolean play ( + boolean allowDefer, final boolean loop, final StartObserver obs) + { + // if we were unable to get our buffer, fail immediately + if (_buffer == null) { + if (obs != null) { + obs.soundStarted(null); + } + return false; + } + + // if we're not ready to go... + if (!_buffer.isPlayable()) { + if (allowDefer) { + // resolve the buffer and instruct it to play once it is + // resolved + _buffer.resolve(new ClipBuffer.Observer() { + public void clipLoaded (ClipBuffer buffer) { + play(false, loop, obs); + } + public void clipFailed (ClipBuffer buffer) { + // well, let's pretend like the sound started so that + // the observer isn't left hanging + if (obs != null) { + obs.soundStarted(Sound.this); + } + } + }); + return true; + } else { + // sorry charlie... + if (obs != null) { + obs.soundStarted(null); + } + return false; + } + } + + // let the observer know that (as far as they're concerned), we're + // started + if (obs != null) { + obs.soundStarted(this); + } + + // if we do not already have a source, obtain one + if (_sourceId == -1) { + _sourceId = _group.acquireSource(this); + if (_sourceId == -1) { + return false; + } + + // bind our clip buffer to the source and notify it + AL10.alSourcei(_sourceId, AL10.AL_BUFFER, _buffer.getBufferId()); + _buffer.sourceBound(); + } + + // configure the source with our ephemera + AL10.alSourcef(_sourceId, AL10.AL_PITCH, _pitch); + AL10.alSourcef(_sourceId, AL10.AL_GAIN, _gain * _group.getBaseGain()); + if (_position != null) { + AL10.alSource(_sourceId, AL10.AL_POSITION, _position); + } + if (_velocity != null) { + AL10.alSource(_sourceId, AL10.AL_VELOCITY, _velocity); + } + + // configure whether or not we should loop + AL10.alSourcei(_sourceId, AL10.AL_LOOPING, + loop ? AL10.AL_TRUE : AL10.AL_FALSE); + + // and start that damned thing up! + AL10.alSourcePlay(_sourceId); + + return true; + } + + /** + * Called by the {@link SoundGroup} when it wants to reclaim our + * source. + * + * @return false if we have no source to reclaim or if we're still busy + * playing our sound, true if we gave up our source. + */ + protected boolean reclaim () + { + if (_sourceId != -1 && + AL10.alGetSourcei(_sourceId, AL10.AL_SOURCE_STATE) == + AL10.AL_STOPPED) { + AL10.alSourcei(_sourceId, AL10.AL_BUFFER, 0); + _buffer.sourceUnbound(); + _sourceId = -1; + return true; + } + return false; + } + + /** The sound group with which we are associated. */ + protected SoundGroup _group; + + /** The OpenAL buffer from which we get our sound data. */ + protected ClipBuffer _buffer; + + /** The source via which we are playing our sound currently. */ + protected int _sourceId = -1; + + /** The pitch adjustment. */ + protected float _pitch = 1; + + /** The gain adjustment. */ + protected float _gain = 1; + + /** The starting position in 3D space. */ + protected FloatBuffer _position; + + /** The velocity vector. */ + protected FloatBuffer _velocity; +} diff --git a/src/java/com/threerings/openal/SoundGroup.java b/src/java/com/threerings/openal/SoundGroup.java new file mode 100644 index 00000000..efd2ec21 --- /dev/null +++ b/src/java/com/threerings/openal/SoundGroup.java @@ -0,0 +1,155 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.openal; + +import java.nio.IntBuffer; +import java.util.ArrayList; + +import org.lwjgl.BufferUtils; +import org.lwjgl.openal.AL10; + +/** + * Manages a group of sounds, binding them to OpenAL sources as they are + * played and freeing up those sources for use by other sounds when the + * sounds are finished. + */ +public class SoundGroup +{ + /** + * Queues up the specified sound clip for pre-loading into the cache. + */ + public void preloadClip (String path) + { + if (_manager.isInitialized()) { + _manager.getClip(_provider, path); + } + } + + /** + * Obtains an "instance" of the specified sound which can be + * positioned, played, looped and otherwise used to make noise. + */ + public Sound getSound (String path) + { + ClipBuffer buffer = null; + if (_manager.isInitialized()) { + buffer = _manager.getClip(_provider, path); + } + return (buffer == null) ? new BlankSound() : new Sound(this, buffer); + } + + /** + * Disposes this sound group, freeing up the OpenAL sources with which + * it is associated. All sounds obtained from this group will no + * longer be usable and should be discarded. + */ + public void dispose () + { + if (_sourceIds != null) { + // make sure any bound sources are released + for (Source source : _sources) { + if (source.holder != null) { + source.holder.stop(); + source.holder.reclaim(); + } + } + _sources.clear(); + AL10.alDeleteSources(_sourceIds); + _sourceIds = null; + } + } + + protected SoundGroup ( + SoundManager manager, ClipProvider provider, int sources) + { + _manager = manager; + _provider = provider; + + // if we were unable to initialize the sound system at all, just + // stop here and we'll behave as if we have no available sources + if (!_manager.isInitialized()) { + return; + } + + // create our sources + _sourceIds = BufferUtils.createIntBuffer(sources); + AL10.alGenSources(_sourceIds); + int errno = AL10.alGetError(); + if (errno != AL10.AL_NO_ERROR) { + Log.warning("Failed to create sources [cprov=" + provider + + ", sources=" + sources + ", errno=" + errno + "]."); + _sourceIds = null; + // we'll have no sources which means all requests to play + // sounds will silently fail as if all sources were in use + + } else { + for (int ii = 0; ii < sources; ii++) { + Source source = new Source(); + source.sourceId = _sourceIds.get(ii); + _sources.add(source); + } + } + } + + /** + * Called by a {@link Sound} when it wants to obtain a source on which + * to play its clip. + */ + protected int acquireSource (Sound acquirer) + { + // start at the beginning of the list looking for an available + // source + for (int ii = 0, ll = _sources.size(); ii < ll; ii++) { + Source source = _sources.get(ii); + if (source.holder == null || source.holder.reclaim()) { + // note this source's new holder + source.holder = acquirer; + // move this source to the end of the list + _sources.remove(ii); + _sources.add(source); + return source.sourceId; + } + } + return -1; + } + + /** + * Used to pass the base gain through to sound effects. + */ + protected float getBaseGain () + { + return _manager.getBaseGain(); + } + + /** Used to track which sources are in use. */ + protected static class Source + { + public int sourceId; + public Sound holder; + } + + protected SoundManager _manager; + protected ClipProvider _provider; + + protected IntBuffer _sourceIds; + protected ArrayList _sources = new ArrayList(); +} diff --git a/src/java/com/threerings/openal/SoundManager.java b/src/java/com/threerings/openal/SoundManager.java new file mode 100644 index 00000000..d7841264 --- /dev/null +++ b/src/java/com/threerings/openal/SoundManager.java @@ -0,0 +1,347 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.openal; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.WeakHashMap; + +import org.lwjgl.openal.AL; +import org.lwjgl.openal.AL10; + +import com.samskivert.util.LRUHashMap; +import com.samskivert.util.Queue; +import com.samskivert.util.RunQueue; + +/** + * An interface to the OpenAL library that provides a number of additional + * services: + * + *

    + *
  • an object oriented interface to the OpenAL system + *
  • a mechanism for loading a group of sounds and freeing their + * resources all at once + *
  • a mechanism for loading sounds in a background thread and + * preloading sounds that are likely to be needed soon + *
+ * + *

Note: the sound manager is not thread safe (other than + * during its interactions with its internal background loading + * thread). It assumes that all sound loading and play requests will be + * made from a single thread. + */ +public class SoundManager +{ + /** + * Creates, initializes and returns the singleton sound manager + * instance. + * + * @param rqueue a queue that the sound manager can use to post short + * runnables that must be executed on the same thread from which all + * other sound methods will be called. + */ + public static SoundManager createSoundManager (RunQueue rqueue) + { + if (_soundmgr != null) { + throw new IllegalStateException( + "A sound manager has already been created."); + } + _soundmgr = new SoundManager(rqueue); + return _soundmgr; + } + + /** + * Returns true if we were able to initialize the sound system. + */ + public boolean isInitialized () + { + return (_toLoad != null); + } + + /** + * Configures the size of our sound cache. If this value is larger + * than memory available to the underlying sound system, it will be + * reduced when OpenAL first tells us we're out of memory. + */ + public void setCacheSize (int bytes) + { + _clips.setMaxSize(bytes); + } + + /** + * Configures the base gain (which must be a value between 0 and 1.0) which + * is multiplied to the individual gain assigned to sound effects (but not + * music). + */ + public void setBaseGain (float gain) + { + _baseGain = gain; + } + + /** + * Returns the base gain used for sound effects (not music). + */ + public float getBaseGain () + { + return _baseGain; + } + + /** + * Creates an object that can be used to manage and play a group of + * sounds. Note: the sound group must be disposed + * when it is no longer needed via a call to {@link + * SoundGroup#dispose}. + * + * @param provider indicates from where the sound group will load its + * sounds. + * @param sources indicates the maximum number of simultaneous sounds + * that can play in this group. + */ + public SoundGroup createGroup (ClipProvider provider, int sources) + { + return new SoundGroup(this, provider, sources); + } + + /** + * Returns a reference to the list of active streams. + */ + public ArrayList getStreams () + { + return _streams; + } + + /** + * Updates all of the streams controlled by the manager. This should be + * called once per frame by the application. + * + * @param time the number of seconds elapsed since the last update + */ + public void updateStreams (float time) + { + // iterate backwards through the list so that streams can dispose of + // themselves during their update + for (int ii = _streams.size() - 1; ii >= 0; ii--) { + _streams.get(ii).update(time); + } + } + + /** + * Creates a sound manager and initializes the OpenAL sound subsystem. + */ + protected SoundManager (RunQueue rqueue) + { + _rqueue = rqueue; + + // initialize the OpenAL sound system + try { + AL.create("", 44100, 15, false); + } catch (Exception e) { + Log.warning("Failed to initialize sound system."); + Log.logStackTrace(e); + // don't start the background loading thread + return; + } + + int errno = AL10.alGetError(); + if (errno != AL10.AL_NO_ERROR) { + Log.warning("Failed to initialize sound system " + + "[errno=" + errno + "]."); + // don't start the background loading thread + return; + } + + // configure our LRU map with a removal observer + _clips.setRemovalObserver( + new LRUHashMap.RemovalObserver() { + public void removedFromMap (LRUHashMap map, + final ClipBuffer item) { + _rqueue.postRunnable(new Runnable() { + public void run () { + Log.debug("Flushing " + item.getKey()); + item.dispose(); + } + }); + } + }); + + // create our loading queue + _toLoad = new Queue(); + + // start up the background loader thread + _loader.setDaemon(true); + _loader.start(); + } + + /** + * Creates a clip buffer for the sound clip loaded via the specified + * provider with the specified path. The clip buffer may come from teh + * cache, and it will immediately be queued for loading if it is not + * already loaded. + */ + protected ClipBuffer getClip (ClipProvider provider, String path) + { + Comparable ckey = ClipBuffer.makeKey(provider, path); + ClipBuffer buffer = _clips.get(ckey); + try { + if (buffer == null) { + // check to see if this clip is currently loading + buffer = _loading.get(ckey); + if (buffer == null) { + buffer = new ClipBuffer(this, provider, path); + _loading.put(ckey, buffer); + } + } + buffer.resolve(null); + return buffer; + + } catch (Throwable t) { + Log.warning("Failure resolving buffer [key=" + ckey + "]."); + Log.logStackTrace(t); + return null; + } + } + + /** + * Queues the supplied clip buffer up for resolution. The {@link Clip} + * will be loaded into memory and then bound into OpenAL on the + * background thread. + */ + protected void queueClipLoad (ClipBuffer buffer) + { + if (_toLoad != null) { + _toLoad.append(buffer); + } + } + + /** + * Queues the supplied clip buffer up using our {@link RunQueue} to + * notify its observers that it failed to load. + */ + protected void queueClipFailure (final ClipBuffer buffer) + { + _rqueue.postRunnable(new Runnable() { + public void run () { + _loading.remove(buffer.getKey()); + buffer.failed(); + } + }); + } + + /** + * Adds the supplied clip buffer back to the cache after it has been marked + * for disposal and subsequently re-requested. + */ + protected void restoreClip (ClipBuffer buffer) + { + _clips.put(buffer.getKey(), buffer); + } + + /** + * Adds a stream to the list maintained by the manager. Called by streams + * when they are created. + */ + protected void addStream (Stream stream) + { + _streams.add(stream); + } + + /** + * Removes a stream from the list maintained by the manager. Called by + * streams when they are disposed. + */ + protected void removeStream (Stream stream) + { + _streams.remove(stream); + } + + /** The thread that loads up sound clips in the background. */ + protected Thread _loader = new Thread("SoundManager.Loader") { + public void run () { + while (true) { + final ClipBuffer buffer = (ClipBuffer)_toLoad.get(); + try { + Log.info("Loading " + buffer.getKey() + "."); + final Clip clip = buffer.load(); + _rqueue.postRunnable(new Runnable() { + public void run () { + Comparable ckey = buffer.getKey(); + Log.debug("Loaded " + ckey + "."); + _loading.remove(ckey); + if (buffer.bind(clip)) { + _clips.put(ckey, buffer); + } else { + // TODO: shrink the cache size if the bind + // failed due to OUT_OF_MEMORY + } + } + }); + + } catch (Throwable t) { + Log.warning("Failed to load clip " + + "[key=" + buffer.getKey() + "]."); + Log.logStackTrace(t); + + // let the clip and its observers know that we are a + // miserable failure + queueClipFailure(buffer); + } + } + } + }; + + /** Used to get back from the background thread to our "main" thread. */ + protected RunQueue _rqueue; + + /** A base gain that is multiplied by the individual gain assigned to + * sounds. */ + protected float _baseGain = 1; + + /** Contains a mapping of all currently-loading clips. */ + protected HashMap _loading = + new HashMap(); + + /** Contains a mapping of all loaded clips. */ + protected LRUHashMap _clips = + new LRUHashMap(DEFAULT_CACHE_SIZE, _sizer); + + /** Contains a queue of clip buffers waiting to be loaded. */ + protected Queue _toLoad; + + /** The list of active streams. */ + protected ArrayList _streams = new ArrayList(); + + /** The one and only sound manager, here for an exclusive performance + * by special request. Available for all your sound playing needs. */ + protected static SoundManager _soundmgr; + + /** Used to compute the in-memory size of sound samples. */ + protected static LRUHashMap.ItemSizer _sizer = + new LRUHashMap.ItemSizer() { + public int computeSize (ClipBuffer item) { + return item.getSize(); + } + }; + + /** Default to a cache size of one megabyte. */ + protected static final int DEFAULT_CACHE_SIZE = 8 * 1024 * 1024; +} diff --git a/src/java/com/threerings/openal/Stream.java b/src/java/com/threerings/openal/Stream.java new file mode 100644 index 00000000..92a06155 --- /dev/null +++ b/src/java/com/threerings/openal/Stream.java @@ -0,0 +1,350 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.openal; + +import java.io.IOException; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +import org.lwjgl.BufferUtils; +import org.lwjgl.openal.AL10; + +/** + * Represents a streaming source of sound data. + */ +public abstract class Stream +{ + /** + * Creates a new stream. Call {@link #dispose} when finished with the + * stream. + * + * @param soundmgr a reference to the sound manager that will update the + * stream + */ + public Stream (SoundManager soundmgr) + { + _soundmgr = soundmgr; + + // create the source and buffers + _nbuf = BufferUtils.createIntBuffer(NUM_BUFFERS); + _nbuf.limit(1); + AL10.alGenSources(_nbuf); + _sourceId = _nbuf.get(); + _nbuf.clear(); + AL10.alGenBuffers(_nbuf); + _nbuf.get(_bufferIds); + + // register with sound manager + _soundmgr.addStream(this); + } + + /** + * Sets the base gain of the stream. + */ + public void setGain (float gain) + { + _gain = gain; + if (_fadeMode == FadeMode.NONE) { + AL10.alSourcef(_sourceId, AL10.AL_GAIN, _gain); + } + } + + /** + * Sets the pitch of the stream. + */ + public void setPitch (float pitch) + { + _pitch = pitch; + AL10.alSourcef(_sourceId, AL10.AL_PITCH, _pitch); + } + + /** + * Determines whether this stream is currently playing. + */ + public boolean isPlaying () + { + return _state == AL10.AL_PLAYING; + } + + /** + * Starts playing this stream. + */ + public void play () + { + if (_state == AL10.AL_PLAYING) { + Log.warning("Tried to play stream already playing."); + return; + } + if (_state == AL10.AL_INITIAL) { + _qidx = _qlen = 0; + queueBuffers(NUM_BUFFERS); + } + AL10.alSourcePlay(_sourceId); + _state = AL10.AL_PLAYING; + } + + /** + * Pauses this stream. + */ + public void pause () + { + if (_state != AL10.AL_PLAYING) { + Log.warning("Tried to pause stream that wasn't playing."); + return; + } + AL10.alSourcePause(_sourceId); + _state = AL10.AL_PAUSED; + } + + /** + * Stops this stream. + */ + public void stop () + { + if (_state == AL10.AL_STOPPED) { + Log.warning("Tried to stop stream that was already stopped."); + return; + } + AL10.alSourceStop(_sourceId); + _state = AL10.AL_STOPPED; + } + + /** + * Fades this stream in over the specified interval. If the stream isn't + * playing, it will be started. + */ + public void fadeIn (float interval) + { + if (_state != AL10.AL_PLAYING) { + play(); + } + AL10.alSourcef(_sourceId, AL10.AL_GAIN, 0f); + _fadeMode = FadeMode.IN; + _fadeInterval = interval; + _fadeElapsed = 0f; + } + + /** + * Fades this stream out over the specified interval. + * + * @param dispose if true, dispose of the stream when done fading out + */ + public void fadeOut (float interval, boolean dispose) + { + _fadeMode = dispose ? FadeMode.OUT_DISPOSE : FadeMode.OUT; + _fadeInterval = interval; + _fadeElapsed = 0f; + } + + /** + * Releases the resources held by this stream and removes it from + * the manager. + */ + public void dispose () + { + // make sure the stream is stopped + if (_state != AL10.AL_STOPPED) { + stop(); + } + + // delete the source and buffers + _nbuf.clear(); + _nbuf.put(_sourceId).flip(); + AL10.alDeleteSources(_nbuf); + _nbuf.clear(); + _nbuf.put(_bufferIds).flip(); + AL10.alDeleteBuffers(_nbuf); + + // remove from manager + _soundmgr.removeStream(this); + } + + /** + * Updates the state of this stream, loading data into buffers and + * adjusting gain as necessary. Called periodically by the + * {@link SoundManager}. + * + * @param time the amount of time elapsed since the last update + */ + protected void update (float time) + { + // update fade, which may stop playing + updateFade(time); + if (_state != AL10.AL_PLAYING) { + return; + } + + // find out how many buffers have been played and unqueue them + int played = AL10.alGetSourcei(_sourceId, AL10.AL_BUFFERS_PROCESSED); + if (played == 0) { + return; + } + _nbuf.clear(); + for (int ii = 0; ii < played; ii++) { + _nbuf.put(_bufferIds[_qidx]); + _qidx = (_qidx + 1) % NUM_BUFFERS; + _qlen--; + } + _nbuf.flip(); + AL10.alSourceUnqueueBuffers(_sourceId, _nbuf); + + // enqueue up to the number of buffers played + queueBuffers(played); + + // find out if we're still playing; if not and we have buffers queued, + // we must restart + _state = AL10.alGetSourcei(_sourceId, AL10.AL_SOURCE_STATE); + if (_qlen > 0 && _state != AL10.AL_PLAYING) { + play(); + } + } + + /** + * Updates the gain of the stream according to the fade state. + */ + protected void updateFade (float time) + { + if (_fadeMode == FadeMode.NONE) { + return; + } + float alpha = Math.min((_fadeElapsed += time) / _fadeInterval, 1f); + AL10.alSourcef(_sourceId, AL10.AL_GAIN, _gain * + (_fadeMode == FadeMode.IN ? alpha : (1f - alpha))); + if (alpha == 1f) { + if (_fadeMode == FadeMode.OUT) { + stop(); + } else if (_fadeMode == FadeMode.OUT_DISPOSE) { + dispose(); + } + _fadeMode = FadeMode.NONE; + } + } + + /** + * Queues (up to) the specified number of buffers. + */ + protected void queueBuffers (int buffers) + { + _nbuf.clear(); + for (int ii = 0; ii < buffers; ii++) { + int bufferId = _bufferIds[(_qidx + _qlen) % NUM_BUFFERS]; + if (populateBuffer(bufferId)) { + _nbuf.put(bufferId); + _qlen++; + } else { + break; + } + } + _nbuf.flip(); + AL10.alSourceQueueBuffers(_sourceId, _nbuf); + } + + /** + * Populates the identified buffer with as much data as it can hold. + * + * @return true if data was read into the buffer and it should be enqueued, + * false if the end of the stream has been reached and no data was read + * into the buffer + */ + protected boolean populateBuffer (int bufferId) + { + if (_abuf == null) { + _abuf = ByteBuffer.allocateDirect(BUFFER_SIZE); + } + _abuf.clear(); + int read = 0; + try { + read = Math.max(populateBuffer(_abuf), 0); + } catch (IOException e) { + Log.warning("Error reading audio stream [error=" + e + "]."); + } + if (read <= 0) { + return false; + } + _abuf.rewind().limit(read); + AL10.alBufferData(bufferId, getFormat(), _abuf, getFrequency()); + return true; + } + + /** + * Returns the OpenAL audio format of the stream. + */ + protected abstract int getFormat (); + + /** + * Returns the stream's playback frequency in samples per second. + */ + protected abstract int getFrequency (); + + /** + * Populates the given buffer with audio data. + * + * @return the total number of bytes read into the buffer, or -1 if the + * end of the stream has been reached + */ + protected abstract int populateBuffer (ByteBuffer buf) + throws IOException; + + /** The manager to which the stream was added. */ + protected SoundManager _soundmgr; + + /** The source through which the stream plays. */ + protected int _sourceId; + + /** The buffers through which we cycle. */ + protected int[] _bufferIds = new int[NUM_BUFFERS]; + + /** The starting index and length of the current queue in + * {@link #_bufferIds}. */ + protected int _qidx, _qlen; + + /** The pitch of the stream. */ + protected float _pitch = 1f; + + /** The gain of the stream. */ + protected float _gain = 1f; + + /** The interval and elapsed time for fading. */ + protected float _fadeInterval, _fadeElapsed; + + /** The type of fading being performed. */ + protected FadeMode _fadeMode = FadeMode.NONE; + + /** The buffer used to store names. */ + protected IntBuffer _nbuf; + + /** The buffer used to store audio data temporarily. */ + protected ByteBuffer _abuf; + + /** The OpenAL state of the stream. */ + protected int _state = AL10.AL_INITIAL; + + /** The size of the buffers in bytes. */ + protected static final int BUFFER_SIZE = 131072; + + /** The number of buffers to use. */ + protected static final int NUM_BUFFERS = 4; + + /** Fading modes. */ + protected enum FadeMode { NONE, IN, OUT, OUT_DISPOSE }; +} diff --git a/src/java/com/threerings/openal/WaveDataClipProvider.java b/src/java/com/threerings/openal/WaveDataClipProvider.java new file mode 100644 index 00000000..9630e1a6 --- /dev/null +++ b/src/java/com/threerings/openal/WaveDataClipProvider.java @@ -0,0 +1,46 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.openal; + +import java.io.IOException; + +import org.lwjgl.util.WaveData; + +/** + * Loads clips via the class loader using LWJGL's {@link WaveData} utility + * class. + */ +public class WaveDataClipProvider implements ClipProvider +{ + public Clip loadClip (String path) throws IOException + { + Clip clip = new Clip(); + WaveData file = WaveData.create(path); + if (file == null) { + throw new IOException("Error loading " + path); + } + clip.format = file.format; + clip.frequency = file.samplerate; + clip.data = file.data; + return clip; + } +} diff --git a/src/java/com/threerings/resource/Handler.java b/src/java/com/threerings/resource/Handler.java new file mode 100644 index 00000000..a30aec2b --- /dev/null +++ b/src/java/com/threerings/resource/Handler.java @@ -0,0 +1,194 @@ +// +// $Id: Handler.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.resource; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import java.security.Permission; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; + +import com.samskivert.io.ByteArrayOutInputStream; +import com.samskivert.net.AttachableURLFactory; +import com.samskivert.util.StringUtil; + +import com.threerings.geom.GeomUtil; + +/** + * This class is not used directly, except by a registering ResourceManager + * so that we can load data from the resource manager using URLs of the form + * resource://<resourceSet>/<path>. ResourceSet may + * be the empty string to load from the default resource sets. + */ +public class Handler extends URLStreamHandler +{ + /** + * Register this class to handle "resource" urls + * ("resource://resourceSet/path") with the specified + * ResourceManager. + */ + public static void registerHandler (ResourceManager rmgr) + { + // if we already have a resource manager registered; don't + // register another one + if (_rmgr != null) { + Log.warning("Refusing duplicate resource handler registration."); + return; + } + _rmgr = rmgr; + + // wire up our handler with the handy dandy attachable URL factory + AttachableURLFactory.attachHandler("resource", Handler.class); + } + + // documentation inherited + protected int hashCode (URL url) + { + return String.valueOf(url).hashCode(); + } + + // documentation inherited + protected boolean equals (URL u1, URL u2) + { + return String.valueOf(u1).equals(String.valueOf(u2)); + } + + // documentation inherited + protected URLConnection openConnection (URL url) + throws IOException + { + return new URLConnection(url) { + // documentation inherited + public void connect () + throws IOException + { + // the host is the bundle name + String bundle = this.url.getHost(); + // and we need to remove the leading '/' from path; + String path = this.url.getPath().substring(1); + try { + // if there are query parameters, we need special magic + String query = url.getQuery(); + if (!StringUtil.isBlank(query)) { + _stream = getStream(bundle, path, query); + } else if (StringUtil.isBlank(bundle)) { + _stream = _rmgr.getResource(path); + } else { + _stream = _rmgr.getResource(bundle, path); + } + this.connected = true; + + } catch (IOException ioe) { + Log.warning("Could not find resource [url=" + this.url + + ", error=" + ioe.getMessage() + "]."); + throw ioe; // rethrow + } + } + + // documentation inherited + public InputStream getInputStream () + throws IOException + { + if (!this.connected) { + connect(); + } + return _stream; + } + + // documentation inherited + public Permission getPermission () + throws IOException + { + // We allow anything in the resource bundle to be loaded + // without any permission restrictions. + return null; + } + + protected InputStream _stream; + }; + } + + /** + * Does some magic to allow a subset of an image to be extracted, + * reencoded as a PNG and then spat back out to the Java content + * handler system for inclusion in internal documentation. + */ + protected InputStream getStream (String bundle, String path, String query) + throws IOException + { + // we can only do this with PNGs + if (!path.endsWith(".png")) { + Log.warning("Requested sub-tile of non-PNG resource " + + "[bundle=" + bundle + ", path=" + path + + ", dims=" + query + "]."); + return _rmgr.getResource(bundle, path); + } + + // parse the query string + String[] bits = StringUtil.split(query, "&"); + int width = -1, height = -1, tidx = -1; + try { + for (int ii = 0; ii < bits.length; ii++) { + if (bits[ii].startsWith("width=")) { + width = Integer.parseInt(bits[ii].substring(6)); + } else if (bits[ii].startsWith("height=")) { + height = Integer.parseInt(bits[ii].substring(7)); + } else if (bits[ii].startsWith("tile=")) { + tidx = Integer.parseInt(bits[ii].substring(5)); + } + } + } catch (NumberFormatException nfe) { + } + if (width <= 0 || height <= 0 || tidx < 0) { + Log.warning("Bogus sub-image dimensions [bundle=" + bundle + + ", path=" + path + ", dims=" + query + "]."); + throw new FileNotFoundException(path); + } + + // locate the tile image, then write that subimage back out in PNG + // format into memory and return an input stream for that + ImageInputStream stream = + StringUtil.isBlank(bundle) ? _rmgr.getImageResource(path) + : _rmgr.getImageResource(bundle, path); + BufferedImage src = ImageIO.read(stream); + Rectangle trect = GeomUtil.getTile( + src.getWidth(), src.getHeight(), width, height, tidx); + BufferedImage tile = src.getSubimage( + trect.x, trect.y, trect.width, trect.height); + ByteArrayOutInputStream data = new ByteArrayOutInputStream(); + ImageIO.write(tile, "PNG", data); + return data.getInputStream(); + } + + /** Our singleton resource manager. */ + protected static ResourceManager _rmgr; +} diff --git a/src/java/com/threerings/resource/Log.java b/src/java/com/threerings/resource/Log.java new file mode 100644 index 00000000..926f844d --- /dev/null +++ b/src/java/com/threerings/resource/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.resource; + +/** + * A placeholder class that contains a reference to the log object used by + * the resource management package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("resource"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/resource/ResourceBundle.java b/src/java/com/threerings/resource/ResourceBundle.java new file mode 100644 index 00000000..1df76039 --- /dev/null +++ b/src/java/com/threerings/resource/ResourceBundle.java @@ -0,0 +1,418 @@ +// +// $Id: ResourceBundle.java 3998 2006-04-04 18:46:42Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.resource; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import com.samskivert.util.FileUtil; +import com.samskivert.util.StringUtil; + +import org.apache.commons.io.CopyUtils; + +/** + * A resource bundle provides access to the resources in a jar file. + */ +public class ResourceBundle +{ + /** + * Constructs a resource bundle with the supplied jar file. + * + * @param source a file object that references our source jar file. + */ + public ResourceBundle (File source) + { + this(source, false, false); + } + + /** + * Constructs a resource bundle with the supplied jar file. + * + * @param source a file object that references our source jar file. + * @param delay if true, the bundle will wait until someone calls + * {@link #sourceIsReady} before allowing access to its resources. + * @param unpack if true the bundle will unpack itself into a + * temporary directory + */ + public ResourceBundle (File source, boolean delay, boolean unpack) + { + _source = source; + if (unpack) { + String root = stripSuffix(source.getPath()); + _unpacked = new File(root + ".stamp"); + _cache = new File(root); + } + + if (!delay) { + sourceIsReady(); + } + } + + /** + * Returns the {@link File} from which resources are fetched for this + * bundle. + */ + public File getSource () + { + return _source; + } + + /** + * @return true if the bundle is fully downloaded and successfully + * unpacked. + */ + public boolean isUnpacked () + { + return (_source.exists() && _unpacked != null && + _unpacked.lastModified() == _source.lastModified()); + } + + /** + * Called by the resource manager once it has ensured that our + * resource jar file is up to date and ready for reading. + * + * @return true if we successfully unpacked our resources, false if we + * encountered errors in doing so. + */ + public boolean sourceIsReady () + { + // make a note of our source's last modification time + _sourceLastMod = _source.lastModified(); + + // if we are unpacking files, the time to do so is now + if (_unpacked != null && _unpacked.lastModified() != _sourceLastMod) { + try { + resolveJarFile(); + } catch (IOException ioe) { + Log.warning("Failure resolving jar file '" + _source + + "': " + ioe + "."); + wipeBundle(true); + return false; + } + + Log.info("Unpacking into " + _cache + "..."); + if (!_cache.exists()) { + if (!_cache.mkdir()) { + Log.warning("Failed to create bundle cache directory '" + + _cache + "'."); + closeJar(); + // we are hopelessly fucked + return false; + } + } else { + FileUtil.recursiveClean(_cache); + } + + // unpack the jar file (this will close the jar when it's done) + if (!FileUtil.unpackJar(_jarSource, _cache)) { + // if something went awry, delete everything in the hopes + // that next time things will work + wipeBundle(true); + return false; + } + + // if everything unpacked smoothly, create our unpack stamp + try { + _unpacked.createNewFile(); + if (!_unpacked.setLastModified(_sourceLastMod)) { + Log.warning("Failed to set last mod on stamp file '" + + _unpacked + "'."); + } + } catch (IOException ioe) { + Log.warning("Failure creating stamp file '" + _unpacked + + "': " + ioe + "."); + // no need to stick a fork in things at this point + } + } + + return true; + } + + /** + * Clears out everything associated with this resource bundle in the + * hopes that we can download it afresh and everything will work the + * next time around. + */ + public void wipeBundle (boolean deleteJar) + { + // clear out our cache directory + if (_cache != null) { + FileUtil.recursiveClean(_cache); + } + + // delete our unpack stamp file + if (_unpacked != null) { + _unpacked.delete(); + } + + // clear out any .jarv file that Getdown might be maintaining so + // that we ensure that it is revalidated + File vfile = new File(FileUtil.resuffix(_source, ".jar", ".jarv")); + if (vfile.exists() && !vfile.delete()) { + Log.warning("Failed to delete " + vfile + "."); + } + + // close and delete our source jar file + if (deleteJar && _source != null) { + closeJar(); + if (!_source.delete()) { + Log.warning("Failed to delete " + _source + + " [exists=" + _source.exists() + "]."); + } + } + } + + /** + * Fetches the named resource from this bundle. The path should be + * specified as a relative, platform independent path (forward + * slashes). For example sounds/scream.au. + * + * @param path the path to the resource in this jar file. + * + * @return an input stream from which the resource can be loaded or + * null if no such resource exists. + * + * @exception IOException thrown if an error occurs locating the + * resource in the jar file. + */ + public InputStream getResource (String path) + throws IOException + { + // unpack our resources into a temp directory so that we can load + // them quickly and the file system can cache them sensibly + File rfile = getResourceFile(path); + return (rfile == null) ? null : new FileInputStream(rfile); + } + + /** + * Returns a file from which the specified resource can be loaded. + * This method will unpack the resource into a temporary directory and + * return a reference to that file. + * + * @param path the path to the resource in this jar file. + * + * @return a file from which the resource can be loaded or null if no + * such resource exists. + */ + public File getResourceFile (String path) + throws IOException + { + if (resolveJarFile()) { + return null; + } + + // if we have been unpacked, return our unpacked file + if (_cache != null) { + File cfile = new File(_cache, path); + if (cfile.exists()) { + return cfile; + } else { + return null; + } + } + + // otherwise, we unpack resources as needed into a temp directory + String tpath = StringUtil.md5hex(_source.getPath() + "%" + path); + File tfile = new File(getCacheDir(), tpath); + if (tfile.exists() && (tfile.lastModified() > _sourceLastMod)) { + return tfile; + } + + JarEntry entry = _jarSource.getJarEntry(path); + if (entry == null) { +// Log.info("Couldn't locate " + path + " in " + _jarSource + "."); + return null; + } + + // copy the resource into the temporary file + BufferedOutputStream fout = + new BufferedOutputStream(new FileOutputStream(tfile)); + InputStream jin = _jarSource.getInputStream(entry); + CopyUtils.copy(jin, fout); + jin.close(); + fout.close(); + + return tfile; + } + + /** + * Returns true if this resource bundle contains the resource with the + * specified path. This avoids actually loading the resource, in the + * event that the caller only cares to know that the resource exists. + */ + public boolean containsResource (String path) + { + try { + if (resolveJarFile()) { + return false; + } + return (_jarSource.getJarEntry(path) != null); + } catch (IOException ioe) { + return false; + } + } + + /** + * Returns a string representation of this resource bundle. + */ + public String toString () + { + try { + resolveJarFile(); + return (_jarSource == null) ? "[file=" + _source + "]" : + "[path=" + _jarSource.getName() + "]"; + + } catch (IOException ioe) { + return "[file=" + _source + ", ioe=" + ioe + "]"; + } + } + + /** + * Creates the internal jar file reference if we've not already got + * it; we do this lazily so as to avoid any jar- or zip-file-related + * antics until and unless doing so is required, and because the + * resource manager would like to be able to create bundles before the + * associated files have been fully downloaded. + * + * @return true if the jar file could not yet be resolved because we + * haven't yet heard from the resource manager that it is ready for us + * to access, false if all is cool. + */ + protected boolean resolveJarFile () + throws IOException + { + // if we don't yet have our resource bundle's last mod time, we + // have not yet been notified that it is ready + if (_sourceLastMod == -1) { + return true; + } + + if (!_source.exists()) { + throw new IOException("Missing jar file for resource bundle: " + + _source + "."); + } + + try { + if (_jarSource == null) { + _jarSource = new JarFile(_source); + } + return false; + + } catch (IOException ioe) { + String msg = "Failed to resolve resource bundle jar file '" + + _source + "'"; + Log.warning(msg + "."); + Log.logStackTrace(ioe); + throw (IOException) new IOException(msg).initCause(ioe); + } + } + + /** + * Closes our (possibly opened) jar file. + */ + protected void closeJar () + { + try { + if (_jarSource != null) { + _jarSource.close(); + } + } catch (Exception ioe) { + Log.warning("Failed to close jar file [path=" + _source + + ", error=" + ioe + "]."); + } + } + + /** + * Returns the cache directory used for unpacked resources. + */ + public static File getCacheDir () + { + if (_tmpdir == null) { + String tmpdir = System.getProperty("java.io.tmpdir"); + if (tmpdir == null) { + Log.info("No system defined temp directory. Faking it."); + tmpdir = System.getProperty("user.home"); + } + setCacheDir(new File(tmpdir, ".narcache")); + } + return _tmpdir; + } + + /** + * Specifies the directory in which our temporary resource files + * should be stored. + */ + public static void setCacheDir (File tmpdir) + { + String rando = Long.toHexString((long)(Math.random() * Long.MAX_VALUE)); + _tmpdir = new File(tmpdir, rando); + if (!_tmpdir.exists()) { + Log.info("Creating narya temp cache directory '" + _tmpdir + "'."); + _tmpdir.mkdirs(); + } + + // add a hook to blow away the temp directory when we exit + Runtime.getRuntime().addShutdownHook(new Thread() { + public void run () { + Log.info("Clearing narya temp cache '" + _tmpdir + "'."); + FileUtil.recursiveDelete(_tmpdir); + } + }); + } + + /** Strips the .jar off of jar file paths. */ + protected static String stripSuffix (String path) + { + if (path.endsWith(".jar")) { + return path.substring(0, path.length()-4); + } else { + // we have to change the path somehow + return path + "-cache"; + } + } + + /** The file from which we construct our jar file. */ + protected File _source; + + /** The last modified time of our source jar file. */ + protected long _sourceLastMod = -1; + + /** A file whose timestamp indicates whether or not our existing jar + * file has been unpacked. */ + protected File _unpacked; + + /** A directory into which we unpack files from our bundle. */ + protected File _cache; + + /** The jar file from which we load resources. */ + protected JarFile _jarSource; + + /** A directory in which we temporarily unpack our resource files. */ + protected static File _tmpdir; +} diff --git a/src/java/com/threerings/resource/ResourceManager.java b/src/java/com/threerings/resource/ResourceManager.java new file mode 100644 index 00000000..afa61d16 --- /dev/null +++ b/src/java/com/threerings/resource/ResourceManager.java @@ -0,0 +1,691 @@ +// +// $Id: ResourceManager.java 4000 2006-04-05 19:40:12Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.resource; + +import java.awt.EventQueue; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.StringTokenizer; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import javax.imageio.stream.FileImageInputStream; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; + +import com.samskivert.net.PathUtil; +import com.samskivert.util.ResultListener; +import com.samskivert.util.StringUtil; + +/** + * The resource manager is responsible for maintaining a repository of + * resources that are synchronized with a remote source. This is + * accomplished in the form of sets of jar files (resource bundles) that + * contain resources and that are updated from a remote resource + * repository via HTTP. These resource bundles are organized into + * resource sets. A resource set contains one or more resource bundles and + * is defined much like a classpath. + * + *

The resource manager can load resources from the default resource + * set, and can make available named resource sets to entities that wish + * to do their own resource loading. If the resource manager fails to + * locate a resource in the default resource set, it falls back to loading + * the resource via the classloader (which will search the classpath). + * + *

Applications that wish to make use of resource sets and their + * associated bundles must call {@link #initBundles} after constructing + * the resource manager, providing the path to a resource definition file + * which describes these resource sets. The definition file will be loaded + * and the resource bundles defined within will be loaded relative to the + * resource directory. The bundles will be cached in the user's home + * directory and only reloaded when the source resources have been + * updated. The resource definition file looks something like the + * following: + * + *

+ * resource.set.default = sets/misc/config.jar: \
+ *                        sets/misc/icons.jar
+ * resource.set.tiles = sets/tiles/ground.jar: \
+ *                      sets/tiles/objects.jar: \
+ *                      /global/resources/tiles/ground.jar: \
+ *                      /global/resources/tiles/objects.jar
+ * resource.set.sounds = sets/sounds/sfx.jar: \
+ *                       sets/sounds/music.jar: \
+ *                       /global/resources/sounds/sfx.jar: \
+ *                       /global/resources/sounds/music.jar
+ * 
+ * + *

All resource set definitions are prefixed with + * resource.set. and all text following that string is + * considered to be the name of the resource set. The resource set named + * default is the default resource set and is the one that is + * searched for resources is a call to {@link #getResource}. + * + *

When a resource is loaded from a resource set, the set is searched + * in the order that entries are specified in the definition. + */ +public class ResourceManager +{ + /** + * Provides facilities for notifying an observer of the resource + * unpacking process. + */ + public interface InitObserver + { + /** + * Indicates a percent completion along with an estimated time + * remaining in seconds. + */ + public void progress (int percent, long remaining); + + /** + * Indicates that there was a failure unpacking our resource + * bundles. + */ + public void initializationFailed (Exception e); + } + + /** + * An adapter that wraps an {@link InitObserver} and routes all method + * invocations to the AWT thread. + */ + public static class AWTInitObserver implements InitObserver + { + public AWTInitObserver (InitObserver obs) { + _obs = obs; + } + + public void progress (final int percent, final long remaining) { + EventQueue.invokeLater(new Runnable() { + public void run () { + _obs.progress(percent, remaining); + } + }); + } + + public void initializationFailed (final Exception e) { + EventQueue.invokeLater(new Runnable() { + public void run () { + _obs.initializationFailed(e); + } + }); + } + + protected InitObserver _obs; + } + + /** + * Constructs a resource manager which will load resources via the + * classloader, prepending resourceRoot to their path. + * + * @param resourceRoot the path to prepend to resource paths prior to + * attempting to load them via the classloader. When resources are + * bundled into the default resource bundle, they don't need this + * prefix, but if they're to be loaded from the classpath, it's likely + * that they'll live in some sort of resources directory + * to isolate them from the rest of the files in the classpath. This + * is not a platform dependent path (forward slash is always used to + * separate path elements). + */ + public ResourceManager (String resourceRoot) + { + this(resourceRoot, ResourceManager.class.getClassLoader()); + } + + /** + * Creates a resource manager with the specified class loader via + * which to load classes. See {@link #ResourceManager(String)} for + * further documentation. + */ + public ResourceManager (String resourceRoot, ClassLoader loader) + { + _rootPath = resourceRoot; + _loader = loader; + + // get our resource directory from resource_dir if possible + initResourceDir(null); + + // set up a URL handler so that things can be loaded via urls + // with the 'resource' protocol + AccessController.doPrivileged(new PrivilegedAction() { + public Object run () { + Handler.registerHandler(ResourceManager.this); + return null; + } + }); + } + + /** + * Initializes the bundle sets to be made available by this resource + * manager. Applications that wish to make use of resource bundles + * should call this method after constructing the resource manager. + * + * @param resourceDir the base directory to which the paths in the + * supplied configuration file are relative. If this is null, the + * system property resource_dir will be used, if + * available. + * @param configPath the path (relative to the resource dir) of the + * resource definition file. + * @param initObs a bundle initialization observer to notify of + * unpacking progress and success or failure, or null if + * the caller doesn't care to be informed; note that in the latter + * case, the calling thread will block until bundle unpacking is + * complete. + * + * @exception IOException thrown if we are unable to read our resource + * manager configuration. + */ + public void initBundles ( + String resourceDir, String configPath, InitObserver initObs) + throws IOException + { + // reinitialize our resource dir if it was specified + if (resourceDir != null) { + initResourceDir(resourceDir); + } + + // check to see if we're in developer mode in which case we won't + // unpack our resources + _unpack = !"true".equalsIgnoreCase( + System.getProperty("no_unpack_resources")); + + // load up our configuration + Properties config = new Properties(); + try { + config.load(new FileInputStream(new File(_rdir, configPath))); + } catch (Exception e) { + String errmsg = "Unable to load resource manager config " + + "[rdir=" + _rdir + ", cpath=" + configPath + "]"; + Log.warning(errmsg + "."); + Log.logStackTrace(e); + throw new IOException(errmsg); + } + + // resolve the configured resource sets + ArrayList dlist = new ArrayList(); + Enumeration names = config.propertyNames(); + while (names.hasMoreElements()) { + String key = (String)names.nextElement(); + if (!key.startsWith(RESOURCE_SET_PREFIX)) { + continue; + } + String setName = key.substring(RESOURCE_SET_PREFIX.length()); + resolveResourceSet(setName, config.getProperty(key), dlist); + } + + // if an observer was passed in, then we do not need to block + // the caller + final boolean[] shouldWait = new boolean[] { false }; + if (initObs == null) { + // if there's no observer, we'll need to block the caller + shouldWait[0] = true; + initObs = new InitObserver() { + public void progress (int percent, long remaining) { + if (percent >= 100) { + synchronized (this) { + // turn off shouldWait, in case we reached + // 100% progress before the calling thread even + // gets a chance to get to the blocking code, below + shouldWait[0] = false; + notify(); + } + } + } + public void initializationFailed (Exception e) { + synchronized (this) { + shouldWait[0] = false; + notify(); + } + } + }; + } + + // start a thread to unpack our bundles + Unpacker unpack = new Unpacker(dlist, initObs); + unpack.start(); + + if (shouldWait[0]) { + synchronized (initObs) { + if (shouldWait[0]) { + try { + initObs.wait(); + } catch (InterruptedException ie) { + Log.warning("Interrupted while waiting for bundles " + + "to unpack."); + } + } + } + } + } + + /** + * Given a path relative to the resource directory, the path is + * properly jimmied (assuming we always use /) and combined with the + * resource directory to yield a {@link File} object that can be used + * to access the resource. + * + * @return a file referencing the specified resource or null if the + * resource manager was never configured with a resource directory. + */ + public File getResourceFile (String path) + { + if (_rdir == null) { + return null; + } + if (!"/".equals(File.separator)) { + path = StringUtil.replace(path, "/", File.separator); + } + return new File(_rdir, path); + } + + /** + * Checks to see if the specified bundle exists, is unpacked and is + * ready to be used. + */ + public boolean checkBundle (String path) + { + File bfile = getResourceFile(path); + return (bfile == null) ? false : + new ResourceBundle(bfile, true, _unpack).isUnpacked(); + } + + /** + * Resolve the specified bundle (the bundle file must already exist in + * the appropriate place on the file system) and return it on the + * specified result listener. Note that the result listener may be + * notified before this method returns on the caller's thread if the + * bundle is already resolved, or it may be notified on a brand new + * thread if the bundle requires unpacking. + */ + public void resolveBundle (String path, final ResultListener listener) + { + File bfile = getResourceFile(path); + if (bfile == null) { + String errmsg = "ResourceManager not configured with " + + "resource directory."; + listener.requestFailed(new IOException(errmsg)); + return; + } + + final ResourceBundle bundle = new ResourceBundle(bfile, true, _unpack); + if (bundle.isUnpacked()) { + if (bundle.sourceIsReady()) { + listener.requestCompleted(bundle); + } else { + String errmsg = "Bundle initialization failed."; + listener.requestFailed(new IOException(errmsg)); + } + return; + } + + // start a thread to unpack our bundles + ArrayList list = new ArrayList(); + list.add(bundle); + Unpacker unpack = new Unpacker(list, new InitObserver() { + public void progress (int percent, long remaining) { + if (percent == 100) { + listener.requestCompleted(bundle); + } + } + public void initializationFailed (Exception e) { + listener.requestFailed(e); + } + }); + unpack.start(); + } + + /** + * Returns the class loader being used to load resources if/when there + * are no resource bundles from which to load them. + */ + public ClassLoader getClassLoader () + { + return _loader; + } + + /** + * Configures the class loader this manager should use to load + * resources if/when there are no bundles from which to load them. + */ + public void setClassLoader (ClassLoader loader) + { + _loader = loader; + } + + /** + * Fetches a resource from the local repository. + * + * @param path the path to the resource + * (ie. "config/miso.properties"). This should not begin with a slash. + * + * @exception IOException thrown if a problem occurs locating or + * reading the resource. + */ + public InputStream getResource (String path) + throws IOException + { + InputStream in = null; + + // first look for this resource in our default resource bundle + for (int i = 0; i < _default.length; i++) { + in = _default[i].getResource(path); + if (in != null) { + return in; + } + } + + // fallback next to an unpacked resource file + File file = getResourceFile(path); + if (file != null && file.exists()) { + return new FileInputStream(file); + } + + // if we still didn't find anything, try the classloader + final String rpath = PathUtil.appendPath(_rootPath, path); + in = (InputStream)AccessController.doPrivileged(new PrivilegedAction() { + public Object run () { + return _loader.getResourceAsStream(rpath); + } + }); + if (in != null) { + return in; + } + + // if we still haven't found it, we throw an exception + String errmsg = "Unable to locate resource [path=" + path + "]"; + throw new FileNotFoundException(errmsg); + } + + /** + * Fetches the specified resource as an {@link ImageInputStream} and + * one that takes advantage, if possible, of caching of unpacked + * resources on the local filesystem. + * + * @exception FileNotFoundException thrown if the resource could not + * be located in any of the bundles in the specified set, or if the + * specified set does not exist. + * @exception IOException thrown if a problem occurs locating or + * reading the resource. + */ + public ImageInputStream getImageResource (String path) + throws IOException + { + // first look for this resource in our default resource bundle + for (int i = 0; i < _default.length; i++) { + File file = _default[i].getResourceFile(path); + if (file != null) { + return new FileImageInputStream(file); + } + } + + // fallback next to an unpacked resource file + File file = getResourceFile(path); + if (file != null && file.exists()) { + return new FileImageInputStream(file); + } + + // if we still didn't find anything, try the classloader + final String rpath = PathUtil.appendPath(_rootPath, path); + InputStream in = (InputStream) + AccessController.doPrivileged(new PrivilegedAction() { + public Object run () { + return _loader.getResourceAsStream(rpath); + } + }); + if (in != null) { + return new MemoryCacheImageInputStream(new BufferedInputStream(in)); + } + + // if we still haven't found it, we throw an exception + String errmsg = "Unable to locate image resource [path=" + path + "]"; + throw new FileNotFoundException(errmsg); + } + + /** + * Returns an input stream from which the requested resource can be + * loaded. Note: this performs a linear search of all of the + * bundles in the set and returns the first resource found with the + * specified path, thus it is not extremely efficient and will behave + * unexpectedly if you use the same paths in different resource + * bundles. + * + * @exception FileNotFoundException thrown if the resource could not + * be located in any of the bundles in the specified set, or if the + * specified set does not exist. + * @exception IOException thrown if a problem occurs locating or + * reading the resource. + */ + public InputStream getResource (String rset, String path) + throws IOException + { + // grab the resource bundles in the specified resource set + ResourceBundle[] bundles = getResourceSet(rset); + if (bundles == null) { + throw new FileNotFoundException( + "Unable to locate resource [set=" + rset + + ", path=" + path + "]"); + } + + // look for the resource in any of the bundles + int size = bundles.length; + for (int ii = 0; ii < size; ii++) { + InputStream instr = bundles[ii].getResource(path); + if (instr != null) { +// Log.info("Found resource [rset=" + rset + +// ", bundle=" + bundles[ii].getSource().getPath() + +// ", path=" + path + ", in=" + instr + "]."); + return instr; + } + } + + throw new FileNotFoundException( + "Unable to locate resource [set=" + rset + ", path=" + path + "]"); + } + + /** + * Fetches the specified resource as an {@link ImageInputStream} and + * one that takes advantage, if possible, of caching of unpacked + * resources on the local filesystem. + * + * @exception FileNotFoundException thrown if the resource could not + * be located in any of the bundles in the specified set, or if the + * specified set does not exist. + * @exception IOException thrown if a problem occurs locating or + * reading the resource. + */ + public ImageInputStream getImageResource (String rset, String path) + throws IOException + { + // grab the resource bundles in the specified resource set + ResourceBundle[] bundles = getResourceSet(rset); + if (bundles == null) { + throw new FileNotFoundException( + "Unable to locate image resource [set=" + rset + + ", path=" + path + "]"); + } + + // look for the resource in any of the bundles + int size = bundles.length; + for (int ii = 0; ii < size; ii++) { + File file = bundles[ii].getResourceFile(path); + if (file != null) { +// Log.info("Found image resource [rset=" + rset + +// ", bundle=" + bundles[ii].getSource() + +// ", path=" + path + ", file=" + file + "]."); + return new FileImageInputStream(file); + } + } + + String errmsg = "Unable to locate image resource [set=" + rset + + ", path=" + path + "]"; + throw new FileNotFoundException(errmsg); + } + + /** + * Returns a reference to the resource set with the specified name, or + * null if no set exists with that name. Services that wish to load + * their own resources can allow the resource manager to load up a + * resource set for them, from which they can easily load their + * resources. + */ + public ResourceBundle[] getResourceSet (String name) + { + return (ResourceBundle[])_sets.get(name); + } + + protected void initResourceDir (String resourceDir) + { + // if none was specified, check the resource_dir system property + if (resourceDir == null) { + try { + resourceDir = System.getProperty("resource_dir"); + } catch (SecurityException se) { + } + } + + // if we found no resource directory, don't use one + if (resourceDir == null) { + return; + } + + // make sure there's a trailing slash + if (!resourceDir.endsWith(File.separator)) { + resourceDir += File.separator; + } + _rdir = new File(resourceDir); + } + + /** + * Loads up a resource set based on the supplied definition + * information. + */ + protected void resolveResourceSet ( + String setName, String definition, List dlist) + { + StringTokenizer tok = new StringTokenizer(definition, ":"); + ArrayList set = new ArrayList(); + + while (tok.hasMoreTokens()) { + String path = tok.nextToken().trim(); + ResourceBundle bundle = + new ResourceBundle(getResourceFile(path), true, _unpack); + set.add(bundle); + if (bundle.isUnpacked() && bundle.sourceIsReady()) { + continue; + } + dlist.add(bundle); + } + + // convert our array list into an array and stick it in the table + ResourceBundle[] setvec = new ResourceBundle[set.size()]; + set.toArray(setvec); + _sets.put(setName, setvec); + + // if this is our default resource bundle, keep a reference to it + if (DEFAULT_RESOURCE_SET.equals(setName)) { + _default = setvec; + } + } + + /** Used to unpack bundles on a separate thread. */ + protected static class Unpacker extends Thread + { + public Unpacker (List bundles, InitObserver obs) + { + _bundles = bundles; + _obs = obs; + } + + public void run () + { + try { + int count = 0; + for (Iterator iter = _bundles.iterator(); iter.hasNext(); ) { + ResourceBundle bundle = (ResourceBundle)iter.next(); + if (!bundle.sourceIsReady()) { + Log.warning("Bundle failed to initialize " + + bundle + "."); + } + if (_obs != null) { + int pct = count*100/_bundles.size(); + if (pct < 100) { + _obs.progress(pct, 1); + } + } + count++; + } + if (_obs != null) { + _obs.progress(100, 0); + } + + } catch (Exception e) { + if (_obs != null) { + _obs.initializationFailed(e); + } + } + } + + protected List _bundles; + protected InitObserver _obs; + } + + /** The classloader we use for classpath-based resource loading. */ + protected ClassLoader _loader; + + /** The directory that contains our resource bundles. */ + protected File _rdir; + + /** The prefix we prepend to resource paths before attempting to load + * them from the classpath. */ + protected String _rootPath; + + /** Whether or not to unpack our resource bundles. */ + protected boolean _unpack; + + /** Our default resource set. */ + protected ResourceBundle[] _default = new ResourceBundle[0]; + + /** A table of our resource sets. */ + protected HashMap _sets = new HashMap(); + + /** The prefix of configuration entries that describe a resource + * set. */ + protected static final String RESOURCE_SET_PREFIX = "resource.set."; + + /** The name of the default resource set. */ + protected static final String DEFAULT_RESOURCE_SET = "default"; +} diff --git a/src/java/com/threerings/util/BrowserUtil.java b/src/java/com/threerings/util/BrowserUtil.java new file mode 100644 index 00000000..315cc51c --- /dev/null +++ b/src/java/com/threerings/util/BrowserUtil.java @@ -0,0 +1,141 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import java.net.URL; + +import com.samskivert.util.ResultListener; +import com.samskivert.util.RunAnywhere; +import com.samskivert.util.StringUtil; + +/** + * Encapsulates a bunch of hackery needed to invoke an external web browser + * from within a Java application. + */ +public class BrowserUtil +{ + /** + * Opens the user's web browser with the specified URL. + * + * @param url the URL to display in an external browser. + * @param listener a listener to be notified if we failed to launch the + * browser. Note: it will not be notified of success. + */ + public static void browseURL (URL url, ResultListener listener) + { + browseURL(url, listener, "firefox"); + } + + /** + * Opens the user's web browser with the specified URL. + * + * @param url the URL to display in an external browser. + * @param listener a listener to be notified if we failed to launch the + * browser. Note: it will not be notified of success. + * @param genagent the path to the browser to execute on non-Windows, + * non-MacOS. + */ + public static void browseURL ( + URL url, ResultListener listener, String genagent) + { + String[] cmd; + if (RunAnywhere.isWindows()) { + // TODO: test this on Vinders 98 +// cmd = new String[] { "rundll32", "url.dll,FileProtocolHandler", +// url.toString() }; + String osName = System.getProperty("os.name"); + if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) { + cmd = new String[] { "command.com", "/c", "start", + "\"" + url.toString() + "\"" }; + } else { + cmd = new String[] { "cmd.exe", "/c", "start", "\"\"", + "\"" + url.toString() + "\"" }; + } + + } else if (RunAnywhere.isMacOS()) { + cmd = new String[] { "open", url.toString() }; + + } else { // Linux, Solaris, etc + cmd = new String[] { genagent, url.toString() }; + } + + Log.info("Browsing URL [cmd=" + StringUtil.join(cmd, " ") + "]."); + try { + Process process = Runtime.getRuntime().exec(cmd); + BrowserTracker tracker = new BrowserTracker(process, url, listener); + tracker.start(); + } catch (Exception e) { + Log.warning("Failed to launch browser [url=" + url + + ", error=" + e + "]."); + listener.requestFailed(e); + } + } + + protected static class BrowserTracker extends Thread + { + public BrowserTracker (Process process, URL url, ResultListener rl) { + super("BrowserLaunchWaiter"); + setDaemon(true); + _process = process; + _url = url; + _listener = rl; + } + + public void run () { + try { + _process.waitFor(); + int rv = _process.exitValue(); + if (rv == 0) { + return; + } + + String errmsg = "Launched browser failed [rv=" + rv + "]."; + Log.warning(errmsg); + if (!RunAnywhere.isWindows()) { + _listener.requestFailed(new Exception(errmsg)); + return; + } + + // if we're on windows, make a last ditch effort + String[] cmd = new String[] { + "C:\\Program Files\\Internet Explorer\\" + + "IEXPLORE.EXE", "\"" + _url.toString() + "\""}; + Process process = Runtime.getRuntime().exec(cmd); + process.waitFor(); + rv = process.exitValue(); + if (rv != 0) { + errmsg = "Failed to launch iexplore.exe [rv=" + rv + "]."; + Log.warning(errmsg); + _listener.requestFailed(new Exception(errmsg)); + } + + } catch (Exception e) { + Log.logStackTrace(e); + _listener.requestFailed(e); + } + } + + protected Process _process; + protected URL _url; + protected ResultListener _listener; + }; +} diff --git a/src/java/com/threerings/util/DirectionCodes.java b/src/java/com/threerings/util/DirectionCodes.java new file mode 100644 index 00000000..3021e214 --- /dev/null +++ b/src/java/com/threerings/util/DirectionCodes.java @@ -0,0 +1,104 @@ +// +// $Id: DirectionCodes.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +/** + * A single, top-level location for the definition of compass direction + * constants, which are used by a variety of Narya services. + */ +public interface DirectionCodes +{ + /** A direction code indicating no direction. */ + public static final int NONE = -1; + + /** A direction code indicating moving left. */ + public static final int LEFT = 0; + + /** A direction code indicating moving right. */ + public static final int RIGHT = 1; + + /** A direction code indicating a counter-clockwise rotation. */ + public static final int CCW = 0; + + /** A direction code indicating a clockwise rotation. */ + public static final int CW = 1; + + /** A direction code indicating horizontal movement. */ + public static final int HORIZONTAL = 0; + + /** A direction code indicating vertical movement. */ + public static final int VERTICAL = 1; + + /** A direction code indicating southwest. */ + public static final int SOUTHWEST = 0; + + /** A direction code indicating west. */ + public static final int WEST = 1; + + /** A direction code indicating northwest. */ + public static final int NORTHWEST = 2; + + /** A direction code indicating north. */ + public static final int NORTH = 3; + + /** A direction code indicating northeast. */ + public static final int NORTHEAST = 4; + + /** A direction code indicating east. */ + public static final int EAST = 5; + + /** A direction code indicating southeast. */ + public static final int SOUTHEAST = 6; + + /** A direction code indicating south. */ + public static final int SOUTH = 7; + + /** The number of basic compass directions. */ + public static final int DIRECTION_COUNT = 8; + + /** A direction code indicating west by southwest. */ + public static final int WESTSOUTHWEST = 8; + + /** A direction code indicating west by northwest. */ + public static final int WESTNORTHWEST = 9; + + /** A direction code indicating north by northwest. */ + public static final int NORTHNORTHWEST = 10; + + /** A direction code indicating north by northeast. */ + public static final int NORTHNORTHEAST = 11; + + /** A direction code indicating east by northeast. */ + public static final int EASTNORTHEAST = 12; + + /** A direction code indicating east by southeast. */ + public static final int EASTSOUTHEAST = 13; + + /** A direction code indicating south by southeast. */ + public static final int SOUTHSOUTHEAST = 14; + + /** A direction code indicating south by southwest. */ + public static final int SOUTHSOUTHWEST = 15; + + /** The number of fine compass directions. */ + public static final int FINE_DIRECTION_COUNT = 16; +} diff --git a/src/java/com/threerings/util/DirectionUtil.java b/src/java/com/threerings/util/DirectionUtil.java new file mode 100644 index 00000000..ca83df8a --- /dev/null +++ b/src/java/com/threerings/util/DirectionUtil.java @@ -0,0 +1,338 @@ +// +// $Id: DirectionUtil.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import java.awt.Point; + +import com.samskivert.util.IntListUtil; + +/** + * Direction related utility functions. + */ +public class DirectionUtil implements DirectionCodes +{ + /** + * Returns an array of names corresponding to each direction constant. + */ + public static String[] getDirectionNames () + { + return DIR_STRINGS; + } + + /** + * Returns a string representation of the supplied direction code. + */ + public static String toString (int direction) + { + return ((direction >= 0) && (direction < FINE_DIRECTION_COUNT)) ? + DIR_STRINGS[direction] : "INVALID"; + } + + /** + * Returns an abbreviated string representation of the supplied + * direction code. + */ + public static String toShortString (int direction) + { + return ((direction >= 0) && (direction < FINE_DIRECTION_COUNT)) ? + SHORT_DIR_STRINGS[direction] : "?"; + } + + /** + * Returns the direction code that corresponds to the supplied string + * or {@link #NONE} if the string does not correspond to a known + * direction code. + */ + public static int fromString (String dirstr) + { + for (int ii = 0; ii < FINE_DIRECTION_COUNT; ii++) { + if (DIR_STRINGS[ii].equals(dirstr)) { + return ii; + } + } + return NONE; + } + + /** + * Returns the direction code that corresponds to the supplied short + * string or {@link #NONE} if the string does not correspond to a + * known direction code. + */ + public static int fromShortString (String dirstr) + { + for (int ii = 0; ii < FINE_DIRECTION_COUNT; ii++) { + if (SHORT_DIR_STRINGS[ii].equals(dirstr)) { + return ii; + } + } + return NONE; + } + + /** + * Returns a string representation of an array of direction codes. The + * directions are represented by the abbreviated names. + */ + public static String toString (int[] directions) + { + StringBuilder buf = new StringBuilder("{"); + for (int i = 0; i < directions.length; i++) { + if (i > 0) { + buf.append(", "); + } + buf.append(toShortString(directions[i])); + } + return buf.append("}").toString(); + } + + /** + * Rotates the requested fine direction constant clockwise by + * the requested number of ticks. + */ + public static int rotateCW (int direction, int ticks) + { + for (int ii = 0; ii < ticks; ii++) { + direction = FINE_CW_ROTATE[direction]; + } + return direction; + } + + /** + * Rotates the requested fine direction constant + * counter-clockwise by the requested number of ticks. + */ + public static int rotateCCW (int direction, int ticks) + { + for (int ii = 0; ii < ticks; ii++) { + direction = FINE_CCW_ROTATE[direction]; + } + return direction; + } + + /** + * Returns the opposite of the specified direction. + */ + public static int getOpposite (int direction) + { + return rotateCW(direction, FINE_CW_ROTATE.length/2); + } + + /** + * Get the direction closest to the specified direction, out of + * the directions in the possible list (preferring a clockwise match). + */ + public static int getClosest (int direction, int[] possible) + { + return getClosest(direction, possible, true); + } + + /** + * Get the direction closest to the specified direction, out of + * the directions in the possible list. + * + * @param preferCW whether to prefer a clockwise match or a + * counter-clockwise match. + */ + public static int getClosest (int direction, int[] possible, + boolean preferCW) + { + // rotate a tick at a time, looking for matches + int first = direction; + int second = direction; + for (int ii = 0; ii <= FINE_DIRECTION_COUNT / 2; ii++) { + if (IntListUtil.contains(possible, first)) { + return first; + } + + if (ii != 0 && IntListUtil.contains(possible, second)) { + return second; + } + + first = preferCW ? rotateCW(first, 1) : rotateCCW(first, 1); + second = preferCW ? rotateCCW(second, 1) : rotateCW(second, 1); + } + + return NONE; + } + + /** + * Returns which of the eight compass directions that point + * b lies in from point a as one of the + * {@link DirectionCodes} direction constants. Note: that the + * coordinates supplied are assumed to be logical (screen) rather than + * cartesian coordinates and NORTH is considered to point + * toward the top of the screen. + */ + public static int getDirection (Point a, Point b) + { + return getDirection(a.getX(), a.getY(), b.getX(), b.getY()); + } + + /** + * Returns which of the eight compass directions that point + * b lies in from point a as one of the + * {@link DirectionCodes} direction constants. Note: that the + * coordinates supplied are assumed to be logical (screen) rather than + * cartesian coordinates and NORTH is considered to point + * toward the top of the screen. + */ + public static int getDirection (int ax, int ay, int bx, int by) + { + return getDirection(Math.atan2(by-ay, bx-ax)); + } + + /** + * Returns which of the eight compass directions that point + * b lies in from point a as one of the + * {@link DirectionCodes} direction constants. Note: that the + * coordinates supplied are assumed to be logical (screen) rather than + * cartesian coordinates and NORTH is considered to point + * toward the top of the screen. + */ + public static int getDirection (double ax, double ay, double bx, double by) + { + return getDirection(Math.atan2(by-ay, bx-ax)); + } + + /** + * Returns which of the eight compass directions is associated with + * the specified angle theta. Note: that the angle supplied + * is assumed to increase clockwise around the origin (which screen + * angles do) rather than counter-clockwise around the origin (which + * cartesian angles do) and NORTH is considered to point + * toward the top of the screen. + */ + public static int getDirection (double theta) + { + theta = ((theta + Math.PI) * 4) / Math.PI; + return (int)(Math.round(theta) + WEST) % 8; + } + + /** + * Returns which of the sixteen compass directions that point + * b lies in from point a as one of the + * {@link DirectionCodes} direction constants. Note: that the + * coordinates supplied are assumed to be logical (screen) rather than + * cartesian coordinates and NORTH is considered to point + * toward the top of the screen. + */ + public static int getFineDirection (Point a, Point b) + { + return getFineDirection(a.x, a.y, b.x, b.y); + } + + /** + * Returns which of the sixteen compass directions that point + * b lies in from point a as one of the + * {@link DirectionCodes} direction constants. Note: that the + * coordinates supplied are assumed to be logical (screen) rather than + * cartesian coordinates and NORTH is considered to point + * toward the top of the screen. + */ + public static int getFineDirection (int ax, int ay, int bx, int by) + { + return getFineDirection(Math.atan2(by-ay, bx-ax)); + } + + /** + * Returns which of the sixteen compass directions is associated with + * the specified angle theta. Note: that the angle supplied + * is assumed to increase clockwise around the origin (which screen + * angles do) rather than counter-clockwise around the origin (which + * cartesian angles do) and NORTH is considered to point + * toward the top of the screen. + */ + public static int getFineDirection (double theta) + { + theta = ((theta + Math.PI) * 8) / Math.PI; + return ANGLE_MAP[(int)Math.round(theta) % FINE_DIRECTION_COUNT]; + } + + /** + * Move the specified point in the specified screen direction, + * adjusting by the specified adjustments. Fine directions are + * not supported. + */ + public static void moveDirection (Point p, int direction, int dx, int dy) + { + if (direction >= DIRECTION_COUNT) { + throw new IllegalArgumentException( + "Fine coordinates not supported."); + } + + switch (direction) { + case NORTH: case NORTHWEST: case NORTHEAST: p.y -= dy; + } + switch (direction) { + case SOUTH: case SOUTHWEST: case SOUTHEAST: p.y += dy; + } + switch (direction) { + case WEST: case SOUTHWEST: case NORTHWEST: p.x -= dx; + } + switch (direction) { + case EAST: case SOUTHEAST: case NORTHEAST: p.x += dx; + } + } + + /** Direction constant string names. */ + protected static final String[] DIR_STRINGS = { + "SOUTHWEST", "WEST", "NORTHWEST", "NORTH", + "NORTHEAST", "EAST", "SOUTHEAST", "SOUTH", + "WESTSOUTHWEST", "WESTNORTHWEST", "NORTHNORTHWEST", "NORTHNORTHEAST", + "EASTNORTHEAST", "EASTSOUTHEAST", "SOUTHSOUTHEAST", "SOUTHSOUTHWEST", + }; + + /** Abbreviated direction constant string names. */ + protected static final String[] SHORT_DIR_STRINGS = { + "SW", "W", "NW", "N", "NE", "E", "SE", "S", + "WSW", "WNW", "NNW", "NNE", "ENE", "ESE", "SSE", "SSW", + }; + + /** Used to rotate a fine compass direction clockwise. */ + protected static final int[] FINE_CW_ROTATE = { + /* SW -> */ WESTSOUTHWEST, /* W -> */ WESTNORTHWEST, + /* NW -> */ NORTHNORTHWEST, /* N -> */ NORTHNORTHEAST, + /* NE -> */ EASTNORTHEAST, /* E -> */ EASTSOUTHEAST, + /* SE -> */ SOUTHSOUTHEAST, /* S -> */ SOUTHSOUTHWEST, + /* WSW -> */ WEST, /* WNW -> */ NORTHWEST, + /* NNW -> */ NORTH, /* NNE -> */ NORTHEAST, + /* ENE -> */ EAST, /* ESE -> */ SOUTHEAST, + /* SSE -> */ SOUTH, /* SSW -> */ SOUTHWEST + }; + + /** Used to rotate a fine compass direction counter-clockwise. */ + protected static final int[] FINE_CCW_ROTATE = { + /* SW -> */ SOUTHSOUTHWEST, /* W -> */ WESTSOUTHWEST, + /* NW -> */ WESTNORTHWEST, /* N -> */ NORTHNORTHWEST, + /* NE -> */ NORTHNORTHEAST, /* E -> */ EASTNORTHEAST, + /* SE -> */ EASTSOUTHEAST, /* S -> */ SOUTHSOUTHEAST, + /* WSW -> */ SOUTHWEST, /* WNW -> */ WEST, + /* NNW -> */ NORTHWEST, /* NNE -> */ NORTH, + /* ENE -> */ NORTHEAST, /* ESE -> */ EAST, + /* SSE -> */ SOUTHEAST, /* SSW -> */ SOUTH + }; + + /** Used to map an angle to a fine compass direction. */ + protected static final int[] ANGLE_MAP = { + WEST, WESTNORTHWEST, NORTHWEST, NORTHNORTHWEST, NORTH, NORTHNORTHEAST, + NORTHEAST, EASTNORTHEAST, EAST, EASTSOUTHEAST, SOUTHEAST, + SOUTHSOUTHEAST, SOUTH, SOUTHSOUTHWEST, SOUTHWEST, WESTSOUTHWEST }; +} diff --git a/src/java/com/threerings/util/IdleTracker.java b/src/java/com/threerings/util/IdleTracker.java new file mode 100644 index 00000000..2d30c724 --- /dev/null +++ b/src/java/com/threerings/util/IdleTracker.java @@ -0,0 +1,177 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import java.awt.AWTEvent; +import java.awt.Toolkit; +import java.awt.event.AWTEventListener; + +import com.samskivert.util.Interval; +import com.samskivert.util.RunQueue; + +/** + * Used to track user idleness in an AWT application. + */ +public abstract class IdleTracker +{ + /** + * Creates an idle tracker that will report idleness (via {@link + * #idledOut}) after toIdleTime milliseconds have elapsed. + * After an additional toAbandonTime milliseconds have + * elapsed, we will report that the user has {@link #abandonedShip}. + */ + public IdleTracker (long toIdleTime, long toAbandonTime) + { + _toIdleTime = toIdleTime; + _toAbandonTime = toAbandonTime; + + // initialize our last event time + _lastEvent = getTimeStamp(); + } + + public void start (KeyboardManager keymgr, RunQueue rqueue) + { + // we want to observe all mouse and keyboard events + long eventMask = + AWTEvent.MOUSE_EVENT_MASK | + AWTEvent.MOUSE_MOTION_EVENT_MASK | + AWTEvent.MOUSE_WHEEL_EVENT_MASK | + AWTEvent.KEY_EVENT_MASK; + + // add the global event listener + Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() { + public void eventDispatched (AWTEvent event) { + handleUserActivity(); + } + }, eventMask); + + // and tie into the keyboard manager if one is provided + if (keymgr != null) { + keymgr.registerKeyObserver(new KeyboardManager.KeyObserver() { + public void handleKeyEvent ( + int id, int keyCode, long timestamp) { + handleUserActivity(); + } + }); + } + + // register an interval to periodically check our last activity time + new Interval(rqueue) { + public void expired () { + checkIdle(); + } + }.schedule(_toIdleTime/3, true); + } + + /** + * Called when the client has been idle for {@link #_toIdleTime} + * milliseconds. + */ + protected abstract void idledOut (); + + /** + * Called when the client becomes non-idle after we have previously + * reported their idleness. + */ + protected abstract void idledIn (); + + /** + * Called when the client has been idle for {@link #_toIdleTime} plus + * {@link #_toAbandonTime} milliseconds. + */ + protected abstract void abandonedShip (); + + /** + * This should return a timestamp. We would use {@link + * System#currentTimeMillis} except that on Windows that sometimes does + * strange things like leap forward in time causing immediate idleness. + */ + protected abstract long getTimeStamp (); + + /** + * Called with any keyboard or mouse events performed on the frame so + * as to note user activity as it pertains to tracking the client idle + * state. + */ + protected void handleUserActivity () + { + // note the time of the last user action + _lastEvent = getTimeStamp(); + + // idle-in if appropriate + if (_state != ACTIVE) { + _state = ACTIVE; + idledIn(); + } + } + + /** + * Checks the last user event time and posts a command to idle them + * out if they've been inactive for too long, or log them out if + * they've been idle for too long. + */ + protected void checkIdle () + { + long now = getTimeStamp(); + + switch (_state) { + case ACTIVE: + // check whether they've idled out + if (now >= (_lastEvent + _toIdleTime)) { + Log.info("User idle for " + (now-_lastEvent) + "ms."); + _state = IDLE; + idledOut(); + } + break; + + case IDLE: + // check whether they've been idle for too long + if (now >= (_lastEvent + _toIdleTime + _toAbandonTime)) { + Log.info("User idle for " + (now-_lastEvent) + "ms. " + + "Abandoning ship."); + _state = ABANDONED; + abandonedShip(); + } + break; + } + } + +// /** The user's current state. */ +// protected static enum State { ACTIVE, IDLE, ABANDONED }; + + /** The duration after which we declare the user to be idle. */ + protected long _toIdleTime; + + /** The duration after which we declare the user to have abandoned ship. */ + protected long _toAbandonTime; + + /** The time of the last mouse or keyboard event; used to track + * whether the user is idle. */ + protected long _lastEvent; + + /** Whether the user is currently active, idle or abandoned. */ + protected int _state = ACTIVE; + + protected static final int ACTIVE = 0; + protected static final int IDLE = 1; + protected static final int ABANDONED = 2; +} diff --git a/src/java/com/threerings/util/KeyDispatcher.java b/src/java/com/threerings/util/KeyDispatcher.java new file mode 100644 index 00000000..1300261a --- /dev/null +++ b/src/java/com/threerings/util/KeyDispatcher.java @@ -0,0 +1,275 @@ +// +// $Id: KeyDispatcher.java 4181 2006-06-07 21:54:12Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import java.awt.Component; +import java.awt.Window; +import java.awt.KeyEventDispatcher; +import java.awt.KeyboardFocusManager; + +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowFocusListener; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; + +import javax.swing.JRootPane; +import javax.swing.JTable; +import javax.swing.event.AncestorEvent; +import javax.swing.event.AncestorListener; +import javax.swing.text.JTextComponent; + +import com.samskivert.util.HashIntMap; + +/** + * Handles dispatching special global key pressed and released events to + * those that care to monitor and process such things. + * + *

A {@link JTextComponent} may registered as a "chat grabber" via + * {@link #pushChatGrabber} so as to capture typed chat characters + * regardless of which component currently has the focus. + * + *

Components may also register as global {@link KeyListener}s via + * {@link #addGlobalKeyListener} to always be notified of key press and + * release events for all keys. + */ +public class KeyDispatcher + implements KeyEventDispatcher, AncestorListener, WindowFocusListener +{ + /** + * Constructs a key dispatcher. + */ + public KeyDispatcher (Window window) + { + // save things off + _window = window; + + // listen to window events on our main window so that we can release + // keys when the mouse leaves the window + _window.addWindowFocusListener(this); + + // monitor key events from the central dispatch mechanism + KeyboardFocusManager.getCurrentKeyboardFocusManager(). + addKeyEventDispatcher(this); + } + + /** + * Shuts down the key dispatcher. + */ + public void shutdown () + { + // cease monitoring key events + KeyboardFocusManager.getCurrentKeyboardFocusManager(). + removeKeyEventDispatcher(this); + + // cease observing our window + _window.removeWindowFocusListener(this); + } + + /** + * Makes the specified component the new grabber of key typed events + * that look like they're chat-related per {@link #isChatCharacter}. + */ + public void pushChatGrabber (JTextComponent comp) + { + // note this component as the new chat grabber + _curChatGrabber = comp; + + // add the component to the list of grabbers + _chatGrabbers.addLast(comp); + comp.addAncestorListener(this); + + // and request to give the new component the focus since that's a + // sensible thing to do as it's aiming to nab all key events + // henceforth + comp.requestFocusInWindow(); + } + + /** + * Removes the specified component from the list of chat grabbers so + * that it will no longer be notified of chat-like key events. + */ + public void removeChatGrabber (JTextComponent comp) + { + // remove the component from the list of grabbers + comp.removeAncestorListener(this); + _chatGrabbers.remove(comp); + + // update the current chat grabbing component + _curChatGrabber = _chatGrabbers.isEmpty() ? null : + _chatGrabbers.getLast(); + } + + /** + * Adds the key listener to receive all key events at all times. + */ + public void addGlobalKeyListener (KeyListener listener) + { + _listeners.add(listener); + } + + /** + * Removes the specified global key listener. + */ + public void removeGlobalKeyListener (KeyListener listener) + { + _listeners.remove(listener); + } + + // documentation inherited from interface KeyEventDispatcher + public boolean dispatchKeyEvent (KeyEvent e) + { + int lsize = _listeners.size(); + + switch (e.getID()) { + case KeyEvent.KEY_TYPED: + // dispatch to all the global listeners + for (int ii = 0; ii < lsize; ii++) { + _listeners.get(ii).keyTyped(e); + } + + // see if a chat grabber needs to grab it + if (_curChatGrabber != null) { + Component target = e.getComponent(); + // if the key was typed on a non-text component or one + // that wasn't editable... + if (isChatCharacter(e.getKeyChar()) && + !isTypableTarget(target)) { + // focus our grabby component, and redirect this + // key event there + _curChatGrabber.requestFocusInWindow(); + KeyboardFocusManager.getCurrentKeyboardFocusManager(). + redispatchEvent(_curChatGrabber, e); + return true; + } + } + break; + + case KeyEvent.KEY_PRESSED: + if (lsize > 0) { + for (int ii = 0; ii < lsize; ii++) { + _listeners.get(ii).keyPressed(e); + } + // remember the key event.. + _downKeys.put(e.getKeyCode(), e); + } + break; + + case KeyEvent.KEY_RELEASED: + if (lsize > 0) { + for (int ii = 0; ii < lsize; ii++) { + ((KeyListener) _listeners.get(ii)).keyReleased(e); + } + // forget the key event + _downKeys.remove(e.getKeyCode()); + } + break; + } + + return false; + } + + /** + * Returns true if the specified target component supports being typed + * into, and thus we shouldn't steal focus away from it if the user + * starts typing. + */ + protected boolean isTypableTarget (Component target) + { + return target.isShowing() && + (((target instanceof JTextComponent) && + ((JTextComponent) target).isEditable()) || + (target instanceof JTable) || + (target instanceof JRootPane)); + } + + /** + * Returns whether the specified character is a chat character. + */ + protected boolean isChatCharacter (char c) + { + return (Character.isLetterOrDigit(c) || ('/' == c)); + } + + // documentation inherited from interface WindowFocusListener + public void windowGainedFocus (WindowEvent e) + { + // nothing + } + + // documentation inherited from interface WindowFocusListener + public void windowLostFocus (WindowEvent e) + { + // un-press any keys that were left down + if (!_downKeys.isEmpty()) { + long now = System.currentTimeMillis(); + for (KeyEvent down : _downKeys.values()) { + KeyEvent up = new KeyEvent( + down.getComponent(), KeyEvent.KEY_RELEASED, now, + down.getModifiers(), down.getKeyCode(), down.getKeyChar(), + down.getKeyLocation()); + for (int ii = 0, nn = _listeners.size(); ii < nn; ii++) { + _listeners.get(ii).keyReleased(up); + } + } + _downKeys.clear(); + } + } + + // documentation inherited from interface AncestorListener + public void ancestorAdded (AncestorEvent ae) + { + // nothing + } + + // documentation inherited from interface AncestorListener + public void ancestorMoved (AncestorEvent ae) + { + // nothing + } + + // documentation inherited from interface AncestorListener + public void ancestorRemoved (AncestorEvent ae) + { + removeChatGrabber((JTextComponent) ae.getComponent()); + } + + /** The main window for which we're observing key events. */ + protected Window _window; + + /** The current most-recently pushed component that wants to grab + * alphanumeric key presses. */ + protected JTextComponent _curChatGrabber; + + /** The stack of grabbers. */ + protected LinkedList _chatGrabbers = + new LinkedList(); + + /** Global key listeners. */ + protected ArrayList _listeners = new ArrayList(); + + /** Keys that are currently held down. */ + protected HashIntMap _downKeys = new HashIntMap(); +} diff --git a/src/java/com/threerings/util/KeyTranslator.java b/src/java/com/threerings/util/KeyTranslator.java new file mode 100644 index 00000000..208905aa --- /dev/null +++ b/src/java/com/threerings/util/KeyTranslator.java @@ -0,0 +1,79 @@ +// +// $Id: KeyTranslator.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import java.util.Iterator; + +/** + * The key translator interface provides a means whereby the keyboard + * manager can map a key code to the logical {@link + * com.samskivert.swing.Controller} action command that it represents. + */ +public interface KeyTranslator +{ + /** + * Returns whether there is an action command for the key + * corresponding to the given keycode. The translator may have an + * action command for either a key press or a key release of the key, + * or both. + */ + public boolean hasCommand (int keyCode); + + /** + * Returns the action command string associated with a key press of + * the key corresponding to the given key code, or null + * if there is no associated command. + */ + public String getPressCommand (int keyCode); + + /** + * Returns the action command string associated with a key release of + * the key corresponding to the given key code, or null + * if there is no associated command. + */ + public String getReleaseCommand (int keyCode); + + /** + * Returns the number of times each second that key presses are to be + * automatically repeated while the key is held down, or + * 0 to disable auto-repeat for the key. + */ + public int getRepeatRate (int keyCode); + + /** + * Returns the delay in milliseconds before generating auto-repeated + * key press events for the specified key. + */ + public long getRepeatDelay (int keyCode); + + /** + * Returns an iterator that iterates over the available press + * commands. + */ + public Iterator enumeratePressCommands (); + + /** + * Returns an iterator that iterates over the available release + * commands. + */ + public Iterator enumerateReleaseCommands (); +} diff --git a/src/java/com/threerings/util/KeyTranslatorImpl.java b/src/java/com/threerings/util/KeyTranslatorImpl.java new file mode 100644 index 00000000..47a5de24 --- /dev/null +++ b/src/java/com/threerings/util/KeyTranslatorImpl.java @@ -0,0 +1,190 @@ +// +// $Id: KeyTranslatorImpl.java 3272 2004-12-13 07:52:09Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import java.util.ArrayList; +import java.util.Iterator; + +import com.samskivert.util.HashIntMap; + +/** + * A basic implementation of the {@link KeyTranslator} interface that + * provides facilities for mapping key codes to action command strings for + * use by the {@link KeyboardManager}. + */ +public class KeyTranslatorImpl implements KeyTranslator +{ + /** + * Adds a mapping from a key press to an action command string that + * will auto-repeat at a default repeat rate. + */ + public void addPressCommand (int keyCode, String command) + { + addPressCommand(keyCode, command, DEFAULT_REPEAT_RATE); + } + + /** + * Adds a mapping from a key press to an action command string that + * will auto-repeat at the specified repeat rate. Overwrites any + * existing mapping and repeat rate that may have already been + * registered. + * + * @param rate the number of times each second that the key press + * should be repeated while the key is down, or 0 to + * disable auto-repeat for the key. + */ + public void addPressCommand (int keyCode, String command, int rate) + { + addPressCommand(keyCode, command, rate, DEFAULT_REPEAT_DELAY); + } + + /** + * Adds a mapping from a key press to an action command string that + * will auto-repeat at the specified repeat rate after the specified + * auto-repeat delay has expired. Overwrites any existing mapping for + * the specified key code that may have already been registered. + * + * @param rate the number of times each second that the key press + * should be repeated while the key is down; passing 0 + * will result in no repeating. + * @param repeatDelay the delay in milliseconds before auto-repeating + * key press events will be generated for the key. + */ + public void addPressCommand ( + int keyCode, String command, int rate, long repeatDelay) + { + KeyRecord krec = getKeyRecord(keyCode); + krec.pressCommand = command; + krec.repeatRate = rate; + krec.repeatDelay = repeatDelay; + } + + /** + * Adds a mapping from a key release to an action command string. + * Overwrites any existing mapping that may already have been + * registered. + */ + public void addReleaseCommand (int keyCode, String command) + { + KeyRecord krec = getKeyRecord(keyCode); + krec.releaseCommand = command; + } + + /** + * Returns the key record for the specified key, creating it and + * inserting it in the key table if necessary. + */ + protected KeyRecord getKeyRecord (int keyCode) + { + KeyRecord krec = (KeyRecord)_keys.get(keyCode); + if (krec == null) { + krec = new KeyRecord(); + _keys.put(keyCode, krec); + } + return krec; + } + + // documentation inherited + public boolean hasCommand (int keyCode) + { + return (_keys.get(keyCode) != null); + } + + // documentation inherited + public String getPressCommand (int keyCode) + { + KeyRecord krec = (KeyRecord)_keys.get(keyCode); + return (krec == null) ? null : krec.pressCommand; + } + + // documentation inherited + public String getReleaseCommand (int keyCode) + { + KeyRecord krec = (KeyRecord)_keys.get(keyCode); + return (krec == null) ? null : krec.releaseCommand; + } + + // documentation inherited + public int getRepeatRate (int keyCode) + { + KeyRecord krec = (KeyRecord)_keys.get(keyCode); + return (krec == null) ? DEFAULT_REPEAT_RATE : krec.repeatRate; + } + + // documentation inherited + public long getRepeatDelay (int keyCode) + { + KeyRecord krec = (KeyRecord)_keys.get(keyCode); + return (krec == null) ? DEFAULT_REPEAT_DELAY : krec.repeatDelay; + } + + // documentation inherited + public Iterator enumeratePressCommands () + { + ArrayList commands = new ArrayList(); + Iterator iter = _keys.values().iterator(); + while (iter.hasNext()) { + KeyRecord krec = (KeyRecord)iter.next(); + commands.add(krec.pressCommand); + } + return commands.iterator(); + } + + // documentation inherited + public Iterator enumerateReleaseCommands () + { + ArrayList commands = new ArrayList(); + Iterator iter = _keys.values().iterator(); + while (iter.hasNext()) { + KeyRecord krec = (KeyRecord)iter.next(); + commands.add(krec.releaseCommand); + } + return commands.iterator(); + } + + protected static class KeyRecord + { + /** The command to be posted when the key is pressed. */ + public String pressCommand; + + /** The command to be posted when the key is released. */ + public String releaseCommand; + + /** The rate in presses per second at which the key is to be + * auto-repeated. */ + public int repeatRate; + + /** The delay in milliseconds that must expire with the key still + * pressed before auto-repeated key presses will begin. */ + public long repeatDelay; + } + + /** The keys for which commands are registered. */ + protected HashIntMap _keys = new HashIntMap(); + + /** The default key press repeat rate. */ + protected static final int DEFAULT_REPEAT_RATE = 5; + + /** The default delay in milliseconds before auto-repeated key presses + * will begin. */ + protected static final long DEFAULT_REPEAT_DELAY = 500L; +} diff --git a/src/java/com/threerings/util/KeyboardManager.java b/src/java/com/threerings/util/KeyboardManager.java new file mode 100644 index 00000000..feb4995a --- /dev/null +++ b/src/java/com/threerings/util/KeyboardManager.java @@ -0,0 +1,720 @@ +// +// $Id: KeyboardManager.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import java.awt.KeyEventDispatcher; +import java.awt.KeyboardFocusManager; +import java.awt.Window; + +import java.awt.event.KeyEvent; +import java.awt.event.WindowEvent; +import java.awt.event.WindowFocusListener; + +import java.util.Iterator; + +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import javax.swing.event.AncestorEvent; +import javax.swing.event.AncestorListener; + +import com.samskivert.swing.Controller; +import com.samskivert.util.HashIntMap; +import com.samskivert.util.Interval; +import com.samskivert.util.ObserverList; +import com.samskivert.util.RunAnywhere; + +import com.threerings.util.keybd.Keyboard; + +/** + * The keyboard manager observes keyboard actions on a particular + * component and posts commands associated with the key presses to the + * {@link Controller} hierarchy. It allows specifying the key repeat + * rate, and will begin repeating a key immediately after it is held down + * rather than depending on the system-specific key repeat delay/rate. + */ +public class KeyboardManager + implements KeyEventDispatcher, AncestorListener, WindowFocusListener +{ + /** + * An interface to be implemented by those that care to be notified + * whenever an event (either a key press or a key release) occurs for + * any key while the keyboard manager is active. We use this custom + * interface rather than the more standard {@link + * java.awt.event.KeyListener} interface so that we needn't create key + * pressed and released event objects each time a (potentially + * artificially-generated) event occurs. + */ + public interface KeyObserver + { + /** + * Called whenever a key event occurs for a particular key. + */ + public void handleKeyEvent (int id, int keyCode, long timestamp); + } + + /** + * Constructs a keyboard manager that is initially disabled. The + * keyboard manager should not be enabled until it has been supplied + * with a target component and translator via {@link #setTarget}. + */ + public KeyboardManager () + { + // capture low-level keyboard events via the keyboard focus manager + KeyboardFocusManager.getCurrentKeyboardFocusManager(). + addKeyEventDispatcher(this); + } + + /** + * Resets the keyboard manager, clearing any target and key translator + * in use and disabling the keyboard manager if it is currently + * active. + */ + public void reset () + { + setEnabled(false); + _target = null; + _xlate = null; + _focus = false; + } + + /** + * Initializes the keyboard manager with the supplied target component + * and key translator and disables the keyboard manager if it is + * currently active. + * + * @param target the component whose keyboard events are to be observed. + * @param xlate the key translator used to map keyboard events to + * controller action commands. + */ + public void setTarget (JComponent target, KeyTranslator xlate) + { + setEnabled(false); + + // save off references + _target = target; + _xlate = xlate; + } + + /** + * Registers a key observer that will be notified of all key events + * while the keyboard manager is active. + */ + public void registerKeyObserver (KeyObserver obs) + { + _observers.add(obs); + } + + /** + * Removes the supplied key observer from the list of observers to be + * notified of all key events while the keyboard manager is active. + */ + public void removeKeyObserver (KeyObserver obs) + { + _observers.remove(obs); + } + + /** + * Sets whether the keyboard manager processes keyboard input. + */ + public void setEnabled (boolean enabled) + { + // report incorrect usage + if (enabled && _target == null) { + Log.warning("Attempt to enable uninitialized keyboard manager!"); + Thread.dumpStack(); + return; + } + + // ignore NOOPs + if (enabled == _enabled) { + return; + } + + if (!enabled) { + if (Keyboard.isAvailable()) { + // restore the original key auto-repeat settings + Keyboard.setKeyRepeat(_nativeRepeat); + } + + // clear out all of our key states + releaseAllKeys(); + _keys.clear(); + + // cease listening to all of our business + if (_window != null) { + _window.removeWindowFocusListener(this); + _window = null; + } + _target.removeAncestorListener(this); + + // note that we no longer have the focus + _focus = false; + + } else { + // listen to ancestor events so that we can cease our business + // if we lose the focus + _target.addAncestorListener(this); + + // if we're already showing, listen to window focus events, + // else we have to wait until the target is added since it + // doesn't currently have a window + if (_target.isShowing() && _window == null) { + _window = SwingUtilities.getWindowAncestor(_target); + if (_window != null) { + _window.addWindowFocusListener(this); + } + } + + // assume the keyboard focus since we were just enabled + _focus = true; + + if (Keyboard.isAvailable()) { + // note whether key auto-repeating was enabled + _nativeRepeat = Keyboard.isKeyRepeatEnabled(); + + // disable native key auto-repeating so that we can + // definitively ascertain key pressed/released events + Keyboard.setKeyRepeat(false); + } + } + + // save off our new enabled state + _enabled = enabled; + } + + /** + * Sets the expected delay in milliseconds between each key + * press/release event the keyboard manager should expect to receive + * while a key is repeating. + */ + public void setRepeatDelay (long delay) + { + _repeatDelay = delay; + } + + /** + * Releases all keys and ceases any hot repeating action that may be + * going on. + */ + public void releaseAllKeys () + { + long now = System.currentTimeMillis(); + Iterator iter = _keys.elements(); + while (iter.hasNext()) { + ((KeyInfo)iter.next()).release(now); + } + } + + /** + * Called when the keyboard manager gains focus and should begin + * handling keys again if it was previously enabled. + */ + protected void gainedFocus () + { + if (Keyboard.isAvailable()) { + // disable key auto-repeating + Keyboard.setKeyRepeat(false); + } + + // note that we've regained the focus + _focus = true; + } + + /** + * Called when the keyboard manager loses focus and should cease + * handling keys. + */ + protected void lostFocus () + { + if (Keyboard.isAvailable()) { + // restore key auto-repeating + Keyboard.setKeyRepeat(_nativeRepeat); + } + + // clear out all of our keyboard state + releaseAllKeys(); + // note that we no longer have the focus + _focus = false; + } + + // documentation inherited from interface KeyEventDispatcher + public boolean dispatchKeyEvent (KeyEvent e) + { + // bail if we're not enabled, we haven't the focus, or we're not + // showing on-screen + if (!_enabled || !_focus || !_target.isShowing()) { +// Log.info("dispatchKeyEvent [enabled=" + _enabled + +// ", focus=" + _focus + +// ", showing=" + ((_target == null) ? "N/A" : +// "" + _target.isShowing()) + "]."); + return false; + } + + // handle key press and release events + switch (e.getID()) { + case KeyEvent.KEY_PRESSED: + return keyPressed(e); + + case KeyEvent.KEY_RELEASED: + return keyReleased(e); + + default: + return false; + } + } + + /** + * Called when Swing notifies us that a key has been pressed while the + * keyboard manager is active. + * + * @return true to swallow the key event + */ + protected boolean keyPressed (KeyEvent e) + { + logKey("keyPressed", e); + + // get the action command associated with this key + int keyCode = e.getKeyCode(); + boolean hasCommand = _xlate.hasCommand(keyCode); + if (hasCommand) { + // get the info object for this key, creating one if necessary + KeyInfo info = (KeyInfo)_keys.get(keyCode); + if (info == null) { + info = new KeyInfo(keyCode); + _keys.put(keyCode, info); + } + + // remember the last time this key was pressed + info.setPressTime(RunAnywhere.getWhen(e)); + } + + // notify any key observers of the key press + notifyObservers(KeyEvent.KEY_PRESSED, e.getKeyCode(), + RunAnywhere.getWhen(e)); + + return hasCommand; + } + + /** + * Called when Swing notifies us that a key has been released while + * the keyboard manager is active. + * + * @return true to swallow the key event + */ + protected boolean keyReleased (KeyEvent e) + { + logKey("keyReleased", e); + + // get the info object for this key + KeyInfo info = (KeyInfo)_keys.get(e.getKeyCode()); + if (info != null) { + // remember the last time we received a key release + info.setReleaseTime(RunAnywhere.getWhen(e)); + } + + // notify any key observers of the key release + notifyObservers(KeyEvent.KEY_RELEASED, e.getKeyCode(), + RunAnywhere.getWhen(e)); + + return (info != null); + } + + /** + * Notifies all registered key observers of the supplied key event. + * This method provides a thread-safe manner in which to notify the + * observers, which is necessary since the {@link KeyInfo} objects do + * various antics from the interval manager thread whilst we may do + * other notification from the AWT thread when normal key events are + * handled. + */ + protected synchronized void notifyObservers ( + int id, int keyCode, long timestamp) + { + _keyOp.init(id, keyCode, timestamp); + _observers.apply(_keyOp); + } + + /** + * Logs the given message and key. + */ + protected void logKey (String msg, KeyEvent e) + { + if (DEBUG_EVENTS) { + int keyCode = e.getKeyCode(); + Log.info(msg + " [key=" + KeyEvent.getKeyText(keyCode) + "]."); + } + } + + // documentation inherited from interface AncestorListener + public void ancestorAdded (AncestorEvent e) + { + gainedFocus(); + + if (_window == null) { + _window = SwingUtilities.getWindowAncestor(_target); + _window.addWindowFocusListener(this); + } + } + + // documentation inherited from interface AncestorListener + public void ancestorMoved (AncestorEvent e) + { + // nothing for now + } + + // documentation inherited from interface AncestorListener + public void ancestorRemoved (AncestorEvent e) + { + lostFocus(); + + if (_window != null) { + _window.removeWindowFocusListener(this); + _window = null; + } + } + + // documentation inherited from interface WindowFocusListener + public void windowGainedFocus (WindowEvent e) + { + gainedFocus(); + } + + // documentation inherited from interface WindowFocusListener + public void windowLostFocus (WindowEvent e) + { + lostFocus(); + } + + protected class KeyInfo extends Interval + { + /** + * Constructs a key info object for the given key code. + */ + public KeyInfo (int keyCode) + { + _keyCode = keyCode; + _keyText = KeyEvent.getKeyText(_keyCode); + _pressCommand = _xlate.getPressCommand(_keyCode); + _releaseCommand = _xlate.getReleaseCommand(_keyCode); + int rate = _xlate.getRepeatRate(_keyCode); + _pressDelay = (rate == 0) ? 0 : (1000L / rate); + _repeatDelay = _xlate.getRepeatDelay(_keyCode); + } + + /** + * Sets the last time the key was pressed. + */ + public synchronized void setPressTime (long time) + { + if (_lastPress == 0 && _pressCommand != null) { + // post the initial key press command + postPress(time); + } + + if (!_scheduled && _pressDelay > 0) { + // register an interval to post the key press command + // until the key is decidedly released + if (_repeatDelay > 0) { + schedule(_repeatDelay, _pressDelay); + + } else { + schedule(_pressDelay, true); + } + _scheduled = true; + + if (DEBUG_EVENTS) { + Log.info("Pressing key [key=" + _keyText + "]."); + } + } + + _lastPress = time; + _lastRelease = time; + } + + /** + * Sets the last time the key was released. + */ + public synchronized void setReleaseTime (long time) + { + release(time); + _lastRelease = time; + + // handle key release events received so quickly after the key + // press event that the press/release times are exactly equal + // and, in intervalExpired(), we would therefore be unable to + // distinguish between the key being initially pressed and the + // actual true key release that's taken place. + + // the only case I can think of that might result in this + // happening is if the event manager class queues up a key + // press and release event succession while other code is + // executing, and when it comes time for it to dispatch the + // events in its queue it manages to dispatch both of them to + // us really-lickety-split. one would still think at least a + // few milliseconds should pass between the press and release, + // but in any case, we arguably ought to be watching for and + // handling this case for posterity even though it would seem + // unlikely or impossible, and so, now we do, which is a good + // thing since it appears this does in fact happen, and not so + // infrequently. + if (_lastPress == _lastRelease) { + if (DEBUG_EVENTS) { + Log.warning("Insta-releasing key due to equal key " + + "press/release times [key=" + _keyText + "]."); + } + release(time); + } + } + + /** + * Releases the key if pressed and cancels any active key repeat + * interval. + */ + public synchronized void release (long timestamp) + { + // bail if we're not currently pressed + if (_lastPress == 0) { + return; + } + + if (DEBUG_EVENTS) { + Log.info("Releasing key [key=" + _keyText + "]."); + } + + // remove the repeat interval + if (_scheduled) { + cancel(); + _scheduled = false; + } + + if (_releaseCommand != null) { + // post the key release command + postRelease(timestamp); + } + + // clear out the last press and release timestamps + _lastPress = _lastRelease = 0; + } + + // documentation inherited + public synchronized void expired () + { + long now = System.currentTimeMillis(); + long deltaPress = now - _lastPress; + long deltaRelease = now - _lastRelease; + + if (KeyboardManager.DEBUG_INTERVAL) { + Log.info("Interval [key=" + _keyText + + ", deltaPress=" + deltaPress + + ", deltaRelease=" + deltaRelease + "]."); + } + + // handle a normal interval where we either (a) create a + // sub-interval if we can't yet determine definitively + // whether the key is still down, (b) cease repeating if + // we're certain the key is now up, or (c) repeat the key + // command if we're certain the key is still down + if (_lastRelease != _lastPress) { +// if (deltaRelease < _repeatDelay) { +// // register a one-shot sub-interval to +// // definitively check whether the key was released +// long delay = _repeatDelay - deltaRelease; +// _siid = IntervalManager.register( +// this, delay, Long.valueOf(_lastPress), false); +// if (KeyboardManager.DEBUG_INTERVAL) { +// Log.info("Registered sub-interval " + +// "[id=" + _siid + "]."); +// } + +// } else { + // we know the key was released, so cease repeating + release(now); +// } + + } else if (_lastPress != 0 && _pressCommand != null) { + // post the key press command again + postPress(now); + } + } + +/* + * Old stuff- sub interval stuff was commented out prior to my + * reworking of Interval, I'll be damned if I'm going to convert this + * code that wasn't even being used. + } else if (id == _siid) { + // handle the sub-interval that checks whether the key has + // really been released since the normal interval expired + // at an inopportune time for a definitive check + + // clear out the non-recurring sub-interval identifier + _siid = -1; + + // make sure the key hasn't been pressed again since the + // sub-interval was registered + if (_lastPress != ((Long)arg).longValue()) { + if (KeyboardManager.DEBUG_INTERVAL) { + Log.warning("Key pressed since sub-interval was " + + "registered, aborting release check " + + "[key=" + _keyText + "]."); + } + return; + } + + // provide the last word on whether the key was released + if ((_lastRelease != _lastPress) && + deltaRelease >= _repeatDelay) { + release(now); + + } else if (_pressCommand != null) { + // post the key command again + postPress(now); + } + } + } + **/ + + /** + * Posts the press command for this key and notifies all key + * observers of the key press. + */ + protected void postPress (long timestamp) + { + notifyObservers(KeyEvent.KEY_PRESSED, _keyCode, timestamp); + Controller.postAction(_target, _pressCommand); + } + + /** + * Posts the release command for this key and notifies all key + * observers of the key release. + */ + protected void postRelease (long timestamp) + { + notifyObservers(KeyEvent.KEY_RELEASED, _keyCode, timestamp); + Controller.postAction(_target, _releaseCommand); + } + + /** Returns a string representation of the key info object. */ + public String toString () + { + return "[key=" + _keyText + "]"; + } + + /** True if we are a scheduled interval. */ + protected boolean _scheduled = false; + + /** The last time a key released event was received for this key. */ + protected long _lastRelease; + + /** The last time a key pressed event was received for this key. */ + protected long _lastPress; + + /** The press action command associated with this key. */ + protected String _pressCommand; + + /** The release action command associated with this key. */ + protected String _releaseCommand; + + /** A text representation of this key. */ + protected String _keyText; + + /** The key code associated with this key info object. */ + protected int _keyCode; + + /** The milliseconds to sleep between sending repeat key commands. */ + protected long _pressDelay; + + /** The delay in milliseconds before auto-repeating the key press. */ + protected long _repeatDelay; + } + + /** An observer operation to notify observers of a key event. */ + protected static class KeyObserverOp implements ObserverList.ObserverOp + { + /** Initialized the operation with its parameters. */ + public void init (int id, int keyCode, long timestamp) + { + _id = id; + _keyCode = keyCode; + _timestamp = timestamp; + } + + // documentation inherited from interface ObserverList.ObserverOp + public boolean apply (Object observer) + { + ((KeyObserver)observer).handleKeyEvent(_id, _keyCode, _timestamp); + return true; + } + + /** The key event id. */ + protected int _id; + + /** The key code. */ + protected int _keyCode; + + /** The key event timestamp. */ + protected long _timestamp; + } + + /** Whether to output debugging info for individual key events. */ + protected static final boolean DEBUG_EVENTS = false; + + /** Whether to output debugging info for interval callbacks. */ + protected static final boolean DEBUG_INTERVAL = false; + + /** The default repeat delay. */ + protected static final long DEFAULT_REPEAT_DELAY = 50L; + + /** The expected approximate milliseconds between each key + * release/press event while the key is being auto-repeated. */ + protected long _repeatDelay = DEFAULT_REPEAT_DELAY; + + /** A hashtable mapping key codes to {@link KeyInfo} objects. */ + protected HashIntMap _keys = new HashIntMap(); + + /** Whether the keyboard manager currently has the keyboard focus. */ + protected boolean _focus; + + /** Whether the keyboard manager is accepting keyboard input. */ + protected boolean _enabled; + + /** The window containing our target component whose focus events we + * care to observe, or null if we're not observing a window. */ + protected Window _window; + + /** The component that receives keyboard events and that we associate + * with posted controller commands. */ + protected JComponent _target; + + /** The translator that maps keyboard events to controller commands. */ + protected KeyTranslator _xlate; + + /** The list of key observers. */ + protected ObserverList _observers = + new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY); + + /** The operation used to notify observers of actual key events. */ + protected KeyObserverOp _keyOp = new KeyObserverOp(); + + /** Whether native key auto-repeating was enabled when the keyboard + * manager was last enabled. */ + protected boolean _nativeRepeat; +} diff --git a/src/java/com/threerings/util/Log.java b/src/java/com/threerings/util/Log.java new file mode 100644 index 00000000..9a376628 --- /dev/null +++ b/src/java/com/threerings/util/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +/** + * A placeholder class that contains a reference to the log object used by + * the media services package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("util"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/util/keybd/Keyboard.java b/src/java/com/threerings/util/keybd/Keyboard.java new file mode 100644 index 00000000..509d140a --- /dev/null +++ b/src/java/com/threerings/util/keybd/Keyboard.java @@ -0,0 +1,83 @@ +// +// $Id: Keyboard.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util.keybd; + +import com.threerings.util.Log; + +/** + * Provides access to the native operating system's auto-repeat keyboard + * settings. + */ +public class Keyboard +{ + /** + * Sets whether key auto-repeating is enabled. + */ + public static native void setKeyRepeat (boolean enabled); + + /** + * Returns whether key auto-repeating is enabled. + */ + public static native boolean isKeyRepeatEnabled (); + + /** + * Tests keyboard functionality. + */ + public static void main (String[] args) + { + boolean enabled = (args.length > 0 && args[0].equals("on")); + Keyboard.setKeyRepeat(enabled); + } + + /** + * Returns whether the native keyboard interface is available. + */ + public static boolean isAvailable () + { + return _haveLib; + } + + /** + * Initializes the library and returns true if it successfully did so. + */ + protected static native boolean init (); + + /** Whether the keyboard native library was successfully loaded. */ + protected static boolean _haveLib; + + static { + try { + System.loadLibrary("keybd"); + _haveLib = init(); + if (_haveLib) { + Log.info("Loaded native keyboard library."); + } else { + Log.info("Native keyboard library initialization failed."); + } + + } catch (UnsatisfiedLinkError e) { + Log.warning("Failed to load native keyboard library " + + "[e=" + e + "]."); + _haveLib = false; + } + } +} diff --git a/src/java/com/threerings/util/keybd/Linux/Makefile b/src/java/com/threerings/util/keybd/Linux/Makefile new file mode 100644 index 00000000..e71ee11b --- /dev/null +++ b/src/java/com/threerings/util/keybd/Linux/Makefile @@ -0,0 +1,44 @@ +# +# $Id: Makefile 3330 2005-02-03 01:24:57Z mdb $ + +# +# Executable definitions + +CC=gcc +RM=rm +CP=cp +MKDIR=mkdir + +# +# Directory definitions + +ROOT=../../../../../../.. +LIBRARIES_PATH=-L/usr/X11R6/lib +INSTALL_PATH=${ROOT}/dist/lib/i686-Linux + +# +# Parameter and file definitions + +INCLUDES=-I.. -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux +LIBRARIES=-lX11 +TARGET=libkeybd.so + +# +# Target definitions + +all: ${TARGET} + +install: ${TARGET} + @${MKDIR} -p ${INSTALL_PATH} + cp ${TARGET} ${INSTALL_PATH} + +${TARGET}: com_threerings_util_keybd_Keyboard.c + @echo Compiling Keyboard.c + @${CC} ${INCLUDES} -c com_threerings_util_keybd_Keyboard.c \ + -o com_threerings_util_keybd_Keyboard.o + @echo Creating libkeybd.so + @${CC} -o ${TARGET} com_threerings_util_keybd_Keyboard.o \ + ${LIBRARIES} ${LIBRARIES_PATH} -shared + +clean: + -${RM} -f *.o ${TARGET} diff --git a/src/java/com/threerings/util/keybd/Linux/com_threerings_util_keybd_Keyboard.c b/src/java/com/threerings/util/keybd/Linux/com_threerings_util_keybd_Keyboard.c new file mode 100644 index 00000000..1def0e6f --- /dev/null +++ b/src/java/com/threerings/util/keybd/Linux/com_threerings_util_keybd_Keyboard.c @@ -0,0 +1,87 @@ +/* + * $Id: com_threerings_util_keybd_Keyboard.c 3330 2005-02-03 01:24:57Z mdb $ + */ + +#include +#include +#include +#include "com_threerings_util_keybd_Keyboard.h" + +/* defines */ +#define MESSAGE_LENGTH (256) + +/* prototype definitions */ +Display* getXDisplay (JNIEnv* env); + +JNIEXPORT jboolean JNICALL +Java_com_threerings_util_keybd_Keyboard_init ( + JNIEnv* env, jclass class, jboolean enabled) +{ + Display* display = getXDisplay(env); + if (display == NULL) { + /* If we are unable to open a display, we can't function. */ + return JNI_FALSE; + } else { + XCloseDisplay(display); + return JNI_TRUE; + } +} + +JNIEXPORT void JNICALL +Java_com_threerings_util_keybd_Keyboard_setKeyRepeat ( + JNIEnv* env, jclass class, jboolean enabled) +{ + Display* display = getXDisplay(env); + if (display == NULL) { + return; + } + + /* set the desired key auto-repeat state. */ + if (enabled) { + XAutoRepeatOn(display); + } else { + XAutoRepeatOff(display); + } + + /* close the display to save our changes */ + XCloseDisplay(display); +} + +JNIEXPORT jboolean JNICALL +Java_com_threerings_util_keybd_Keyboard_isKeyRepeatEnabled ( + JNIEnv* env, jclass class) +{ + XKeyboardState values; + Display* display = getXDisplay(env); + if (display == NULL) { + /* for now, assume auto-repeat is enabled */ + return JNI_TRUE; + } + + /* get the current keyboard control information */ + XGetKeyboardControl(display, &values); + + /* close the display */ + XCloseDisplay(display); + + return (values.global_auto_repeat) ? JNI_TRUE : JNI_FALSE; +} + +/* + * Returns a pointer to the X display, or null if an error occurred. + */ +Display* +getXDisplay (JNIEnv* env) +{ + char* disp = NULL; + Display* dpy = XOpenDisplay(disp); + if (dpy == NULL) { + char message[MESSAGE_LENGTH]; + snprintf(message, MESSAGE_LENGTH, + "Unable to open display [display=%s].\n", XDisplayName(disp)); + return NULL; + } + + /* printf("Opened display [disp=%s].\n", XDisplayName(disp)); */ + return dpy; +} diff --git a/src/java/com/threerings/util/keybd/Linux/libkeybd.so b/src/java/com/threerings/util/keybd/Linux/libkeybd.so new file mode 100755 index 00000000..e43ea176 Binary files /dev/null and b/src/java/com/threerings/util/keybd/Linux/libkeybd.so differ diff --git a/src/java/com/threerings/util/keybd/Mac OS X/Makefile b/src/java/com/threerings/util/keybd/Mac OS X/Makefile new file mode 100644 index 00000000..0fe6ecb0 --- /dev/null +++ b/src/java/com/threerings/util/keybd/Mac OS X/Makefile @@ -0,0 +1,7 @@ +# +# $Id: Makefile 3331 2005-02-03 01:25:21Z mdb $ +# +# Placeholder Makefile + +install: + @echo Nothing to see here. Move it along. diff --git a/src/java/com/threerings/util/keybd/Windows/.cvsignore b/src/java/com/threerings/util/keybd/Windows/.cvsignore new file mode 100644 index 00000000..b71fffb4 --- /dev/null +++ b/src/java/com/threerings/util/keybd/Windows/.cvsignore @@ -0,0 +1 @@ +keybd.dll diff --git a/src/java/com/threerings/util/keybd/Windows/Makefile b/src/java/com/threerings/util/keybd/Windows/Makefile new file mode 100644 index 00000000..7e2237de --- /dev/null +++ b/src/java/com/threerings/util/keybd/Windows/Makefile @@ -0,0 +1,27 @@ +# +# $Id: Makefile 3330 2005-02-03 01:24:57Z mdb $ + +CROSSTOOLS_PATH=/usr/local/cross-tools +MINGW_PATH=${CROSSTOOLS_PATH}/i386-mingw32msvc +WIN32API_PATH=${CROSSTOOLS_PATH}/w32api + +CC=${MINGW_PATH}/bin/gcc +RM=rm + +INCLUDES=-I.. -I/usr/local/jdk1.4/include -I/usr/local/jdk1.4/include/linux \ + -I${CROSSTOOLS_PATH}/include -I${WIN32API_PATH}/include +LIBRARIES_PATH=-L${CROSSTOOLS_PATH}/lib -L${WIN32API_PATH}/lib +LIBRARIES= +TARGET=keybd.dll + +all: ${TARGET} + +${TARGET}: com_threerings_util_keybd_Keyboard.c + ${CC} ${INCLUDES} -c com_threerings_util_keybd_Keyboard.c \ + -o com_threerings_util_keybd_Keyboard.o + ${CC} -o ${TARGET} com_threerings_util_keybd_Keyboard.o \ + ${LIBRARIES} ${LIBRARIES_PATH} -shared + +clean: + -${RM} *.o + -${RM} ${TARGET} diff --git a/src/java/com/threerings/util/keybd/Windows/com_threerings_util_keybd_Keyboard.c b/src/java/com/threerings/util/keybd/Windows/com_threerings_util_keybd_Keyboard.c new file mode 100644 index 00000000..7e7d1edb --- /dev/null +++ b/src/java/com/threerings/util/keybd/Windows/com_threerings_util_keybd_Keyboard.c @@ -0,0 +1,104 @@ +/* + * $Id: com_threerings_util_keybd_Keyboard.c 3330 2005-02-03 01:24:57Z mdb $ + */ + +#include +#include +#include +#include "com_threerings_util_keybd_Keyboard.h" + +/* prototype definitions */ +LRESULT CALLBACK KeyboardProc (int nCode, WPARAM wParam, LPARAM lParam); +LRESULT CALLBACK LowLevelKeyboardProc (int nCode, WPARAM wParam, LPARAM lParam); + +/* static global variables */ +static HHOOK gKeyHook; +static HINSTANCE gHInst; + +BOOL WINAPI +DllMain (HANDLE hinstDLL, DWORD dwReason, LPVOID lpvReserved) +{ + /* save off our instance handle */ + fprintf(stderr, "In DllMain.\n"); + gHInst = (HINSTANCE)hinstDLL; +} + +JNIEXPORT void JNICALL +Java_com_threerings_util_keybd_Keyboard_setKeyRepeat ( + JNIEnv* env, jclass class, jboolean enabled) +{ + if (enabled) { + if (gKeyHook != NULL) { + fprintf(stderr, "Removing windows keyboard hook.\n"); + /* remove the keyboard hook */ + UnhookWindowsHookEx(gKeyHook); + gKeyHook = NULL; + } + + } else { + /* install the hook with which we usurp all keyboard events */ +/* gKeyHook = SetWindowsHookEx( */ +/* WH_KEYBOARD_LL, LowLevelKeyboardProc, hinstExe, 0); */ + + fprintf(stderr, "Setting windows keyboard hook.\n"); + gKeyHook = SetWindowsHookEx( + WH_KEYBOARD, KeyboardProc, gHInst, 0); + } +} + +JNIEXPORT jboolean JNICALL +Java_com_threerings_util_keybd_Keyboard_isKeyRepeatEnabled ( + JNIEnv* env, jclass class) +{ + /* since windows has no global key repeat enable/disable facility, we + * simply always return true here so that we'll be sure to "re-enable" + * key repeat, which will result in our properly removing the keyboard + * hook with which we trap key events. */ + fprintf(stderr, "isKeyRepeatEnabled stderr.\n"); + printf("isKeyRepeatEnabled stdout.\n"); + return JNI_TRUE; +} + +/* + * The keyboard event hook that eats all repeated keystrokes. + */ +LRESULT CALLBACK +KeyboardProc (int nCode, WPARAM wParam, LPARAM lParam) +{ + int repeatCount = (lParam & KF_REPEAT); + fprintf(stderr, "Key down [key=%d, repeatCount=%d].\n", + wParam, repeatCount); + return (repeatCount > 1) ? 1 : CallNextHookEx(NULL, nCode, wParam, lParam); +} + +#if 0 + +/* + * The low-level keyboard event hook that eats all keystrokes. + */ +LRESULT CALLBACK +LowLevelKeyboardProc (int nCode, WPARAM wParam, LPARAM lParam) +{ + BOOL fEatKeystroke = FALSE; + + if (nCode == HC_ACTION) { + switch (wParam) { + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + case WM_KEYUP: + case WM_SYSKEYUP: + PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT) lParam; + fEatKeystroke = + ((p->vkCode == VK_TAB) && ((p->flags & LLKHF_ALTDOWN) != 0)) || + ((p->vkCode == VK_ESCAPE) && + ((p->flags & LLKHF_ALTDOWN) != 0)) || + ((p->vkCode == VK_ESCAPE) && ((GetKeyState(VK_CONTROL) & + 0x8000) != 0)); + break; + } + } + + return (fEatKeystroke) ? 1 : CallNextHookEx(NULL, nCode, wParam, lParam); +} + +#endif diff --git a/src/java/com/threerings/util/keybd/com_threerings_util_keybd_Keyboard.h b/src/java/com/threerings/util/keybd/com_threerings_util_keybd_Keyboard.h new file mode 100644 index 00000000..04f524d0 --- /dev/null +++ b/src/java/com/threerings/util/keybd/com_threerings_util_keybd_Keyboard.h @@ -0,0 +1,29 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_threerings_util_keybd_Keyboard */ + +#ifndef _Included_com_threerings_util_keybd_Keyboard +#define _Included_com_threerings_util_keybd_Keyboard +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_threerings_util_keybd_Keyboard + * Method: setKeyRepeat + * Signature: (Z)V + */ +JNIEXPORT void JNICALL Java_com_threerings_util_keybd_Keyboard_setKeyRepeat + (JNIEnv *, jclass, jboolean); + +/* + * Class: com_threerings_util_keybd_Keyboard + * Method: isKeyRepeatEnabled + * Signature: ()Z + */ +JNIEXPORT jboolean JNICALL Java_com_threerings_util_keybd_Keyboard_isKeyRepeatEnabled + (JNIEnv *, jclass); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/java/com/threerings/util/unsafe/FreeBSD/Makefile b/src/java/com/threerings/util/unsafe/FreeBSD/Makefile new file mode 100644 index 00000000..39a78063 --- /dev/null +++ b/src/java/com/threerings/util/unsafe/FreeBSD/Makefile @@ -0,0 +1,45 @@ +# +# $Id: Makefile 3332 2005-02-03 01:30:33Z mdb $ + +# +# Executable definitions + +CC=gcc +RM=rm +CP=cp +MKDIR=mkdir + +# +# Directory definitions + +ROOT=../../../../../../.. +LIBRARIES_PATH= +OSINCDIR!=uname -s | tr 'A-Z' 'a-z' +INSTALL_PATH=${ROOT}/dist/lib/i386-FreeBSD + +# +# Parameter and file definitions + +INCLUDES=-I.. -I${JAVA_HOME}/include -I${JAVA_HOME}/include/${OSINCDIR} +LIBRARIES= +TARGET=libunsafe.so + +# +# Target definitions + +all: ${TARGET} + +install: ${TARGET} + @${MKDIR} -p ${INSTALL_PATH} + cp ${TARGET} ${INSTALL_PATH} + +${TARGET}: com_threerings_util_unsafe_Unsafe.c + @echo "Compiling Unsafe.c" + @${CC} ${INCLUDES} -c com_threerings_util_unsafe_Unsafe.c \ + -o com_threerings_util_unsafe_Unsafe.o + @echo "Creating libunsafe.so" + @${CC} -o ${TARGET} com_threerings_util_unsafe_Unsafe.o \ + ${LIBRARIES} ${LIBRARIES_PATH} -shared + +clean: + -${RM} -f *.o ${TARGET} diff --git a/src/java/com/threerings/util/unsafe/FreeBSD/com_threerings_util_unsafe_Unsafe.c b/src/java/com/threerings/util/unsafe/FreeBSD/com_threerings_util_unsafe_Unsafe.c new file mode 100644 index 00000000..04ac0526 --- /dev/null +++ b/src/java/com/threerings/util/unsafe/FreeBSD/com_threerings_util_unsafe_Unsafe.c @@ -0,0 +1,97 @@ +/* + * $Id: com_threerings_util_unsafe_Unsafe.c 3653 2005-07-21 19:10:08Z mdb $ + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "com_threerings_util_unsafe_Unsafe.h" + +/* global jvmpi interface pointer */ +static JVMPI_Interface* jvmpi; + +/** A sleep method that uses select(). This seems to have about 10ms + * granularity where nanosleep() has about 20ms. Sigh. */ +static int select_sleep (int millisecs) +{ + fd_set dummy; + struct timeval toWait; + FD_ZERO(&dummy); + toWait.tv_sec = millisecs / 1000; + toWait.tv_usec = (millisecs % 1000) * 1000; + return select(0, &dummy, NULL, NULL, &toWait); +} + +JNIEXPORT void JNICALL +Java_com_threerings_util_unsafe_Unsafe_enableGC (JNIEnv* env, jclass clazz) +{ + jvmpi->EnableGC(); +} + +JNIEXPORT void JNICALL +Java_com_threerings_util_unsafe_Unsafe_disableGC (JNIEnv* env, jclass clazz) +{ + jvmpi->DisableGC(); +} + +JNIEXPORT void JNICALL +Java_com_threerings_util_unsafe_Unsafe_nativeSleep ( + JNIEnv* env, jclass clazz, jint millis) +{ +/* struct timespec tmspec; */ +/* tmspec.tv_sec = millis/1000; */ +/* tmspec.tv_nsec = (millis%1000)*1000000; */ +/* if (nanosleep(&tmspec, NULL) < 0) { */ +/* fprintf(stderr, "nanosleep() failed: %s\n", strerror(errno)); */ +/* } */ + if (select_sleep(millis) < 0) { + fprintf(stderr, "select_sleep() failed: %s\n", strerror(errno)); + } +} + +JNIEXPORT jboolean JNICALL +Java_com_threerings_util_unsafe_Unsafe_nativeSetuid ( + JNIEnv* env, jclass clazz, jint uid) +{ + if (setuid((uid_t)uid) != 0) { + fprintf(stderr, "setuid(%d) failed: %s\n", uid, strerror(errno)); + return JNI_FALSE; + } + return JNI_TRUE; +} + +JNIEXPORT jboolean JNICALL +Java_com_threerings_util_unsafe_Unsafe_nativeSetgid ( + JNIEnv* env, jclass clazz, jint gid) +{ + if (setgid((gid_t)gid) != 0) { + fprintf(stderr, "setgid(%d) failed: %s\n", gid, strerror(errno)); + return JNI_FALSE; + } + return JNI_TRUE; +} + +JNIEXPORT jboolean JNICALL +Java_com_threerings_util_unsafe_Unsafe_init (JNIEnv* env, jclass clazz) +{ + JavaVM* jvm; + + if ((*env)->GetJavaVM(env, &jvm) > 0) { + fprintf(stderr, "Failed to get JavaVM from env.\n"); + return JNI_FALSE; + } + + /* get jvmpi interface pointer */ + if (((*jvm)->GetEnv(jvm, (void**)&jvmpi, JVMPI_VERSION_1)) < 0) { + fprintf(stderr, "Failed to get JVMPI from JavaVM.\n"); + return JNI_FALSE; + } + + return JNI_TRUE; +} diff --git a/src/java/com/threerings/util/unsafe/FreeBSD/libunsafe.so b/src/java/com/threerings/util/unsafe/FreeBSD/libunsafe.so new file mode 100755 index 00000000..2a6a9e53 Binary files /dev/null and b/src/java/com/threerings/util/unsafe/FreeBSD/libunsafe.so differ diff --git a/src/java/com/threerings/util/unsafe/Linux/Makefile b/src/java/com/threerings/util/unsafe/Linux/Makefile new file mode 100644 index 00000000..c692c906 --- /dev/null +++ b/src/java/com/threerings/util/unsafe/Linux/Makefile @@ -0,0 +1,44 @@ +# +# $Id: Makefile 3331 2005-02-03 01:25:21Z mdb $ + +# +# Executable definitions + +CC=gcc +RM=rm +CP=cp +MKDIR=mkdir + +# +# Directory definitions + +ROOT=../../../../../../.. +LIBRARIES_PATH=-L/usr/X11R6/lib +INSTALL_PATH=${ROOT}/dist/lib/i686-Linux + +# +# Parameter and file definitions + +INCLUDES=-I.. -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux +LIBRARIES=-lX11 +TARGET=libunsafe.so + +# +# Target definitions + +all: ${TARGET} + +install: ${TARGET} + @${MKDIR} -p ${INSTALL_PATH} + cp ${TARGET} ${INSTALL_PATH} + +${TARGET}: com_threerings_util_unsafe_Unsafe.c + @echo Compiling Unsafe.c + @${CC} ${INCLUDES} -c com_threerings_util_unsafe_Unsafe.c \ + -o com_threerings_util_unsafe_Unsafe.o + @echo Creating ${TARGET} + @${CC} -o ${TARGET} com_threerings_util_unsafe_Unsafe.o \ + ${LIBRARIES} ${LIBRARIES_PATH} -shared + +clean: + -${RM} -f *.o ${TARGET} diff --git a/src/java/com/threerings/util/unsafe/Linux/com_threerings_util_unsafe_Unsafe.c b/src/java/com/threerings/util/unsafe/Linux/com_threerings_util_unsafe_Unsafe.c new file mode 100644 index 00000000..04ac0526 --- /dev/null +++ b/src/java/com/threerings/util/unsafe/Linux/com_threerings_util_unsafe_Unsafe.c @@ -0,0 +1,97 @@ +/* + * $Id: com_threerings_util_unsafe_Unsafe.c 3653 2005-07-21 19:10:08Z mdb $ + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "com_threerings_util_unsafe_Unsafe.h" + +/* global jvmpi interface pointer */ +static JVMPI_Interface* jvmpi; + +/** A sleep method that uses select(). This seems to have about 10ms + * granularity where nanosleep() has about 20ms. Sigh. */ +static int select_sleep (int millisecs) +{ + fd_set dummy; + struct timeval toWait; + FD_ZERO(&dummy); + toWait.tv_sec = millisecs / 1000; + toWait.tv_usec = (millisecs % 1000) * 1000; + return select(0, &dummy, NULL, NULL, &toWait); +} + +JNIEXPORT void JNICALL +Java_com_threerings_util_unsafe_Unsafe_enableGC (JNIEnv* env, jclass clazz) +{ + jvmpi->EnableGC(); +} + +JNIEXPORT void JNICALL +Java_com_threerings_util_unsafe_Unsafe_disableGC (JNIEnv* env, jclass clazz) +{ + jvmpi->DisableGC(); +} + +JNIEXPORT void JNICALL +Java_com_threerings_util_unsafe_Unsafe_nativeSleep ( + JNIEnv* env, jclass clazz, jint millis) +{ +/* struct timespec tmspec; */ +/* tmspec.tv_sec = millis/1000; */ +/* tmspec.tv_nsec = (millis%1000)*1000000; */ +/* if (nanosleep(&tmspec, NULL) < 0) { */ +/* fprintf(stderr, "nanosleep() failed: %s\n", strerror(errno)); */ +/* } */ + if (select_sleep(millis) < 0) { + fprintf(stderr, "select_sleep() failed: %s\n", strerror(errno)); + } +} + +JNIEXPORT jboolean JNICALL +Java_com_threerings_util_unsafe_Unsafe_nativeSetuid ( + JNIEnv* env, jclass clazz, jint uid) +{ + if (setuid((uid_t)uid) != 0) { + fprintf(stderr, "setuid(%d) failed: %s\n", uid, strerror(errno)); + return JNI_FALSE; + } + return JNI_TRUE; +} + +JNIEXPORT jboolean JNICALL +Java_com_threerings_util_unsafe_Unsafe_nativeSetgid ( + JNIEnv* env, jclass clazz, jint gid) +{ + if (setgid((gid_t)gid) != 0) { + fprintf(stderr, "setgid(%d) failed: %s\n", gid, strerror(errno)); + return JNI_FALSE; + } + return JNI_TRUE; +} + +JNIEXPORT jboolean JNICALL +Java_com_threerings_util_unsafe_Unsafe_init (JNIEnv* env, jclass clazz) +{ + JavaVM* jvm; + + if ((*env)->GetJavaVM(env, &jvm) > 0) { + fprintf(stderr, "Failed to get JavaVM from env.\n"); + return JNI_FALSE; + } + + /* get jvmpi interface pointer */ + if (((*jvm)->GetEnv(jvm, (void**)&jvmpi, JVMPI_VERSION_1)) < 0) { + fprintf(stderr, "Failed to get JVMPI from JavaVM.\n"); + return JNI_FALSE; + } + + return JNI_TRUE; +} diff --git a/src/java/com/threerings/util/unsafe/Linux/libunsafe.so b/src/java/com/threerings/util/unsafe/Linux/libunsafe.so new file mode 100755 index 00000000..59305842 Binary files /dev/null and b/src/java/com/threerings/util/unsafe/Linux/libunsafe.so differ diff --git a/src/java/com/threerings/util/unsafe/Mac OS X/Makefile b/src/java/com/threerings/util/unsafe/Mac OS X/Makefile new file mode 100644 index 00000000..0fe6ecb0 --- /dev/null +++ b/src/java/com/threerings/util/unsafe/Mac OS X/Makefile @@ -0,0 +1,7 @@ +# +# $Id: Makefile 3331 2005-02-03 01:25:21Z mdb $ +# +# Placeholder Makefile + +install: + @echo Nothing to see here. Move it along. diff --git a/src/java/com/threerings/util/unsafe/Unsafe.java b/src/java/com/threerings/util/unsafe/Unsafe.java new file mode 100644 index 00000000..7514362d --- /dev/null +++ b/src/java/com/threerings/util/unsafe/Unsafe.java @@ -0,0 +1,144 @@ +// +// $Id: Unsafe.java 3653 2005-07-21 19:10:08Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util.unsafe; + +import com.threerings.util.Log; +import com.samskivert.util.RunAnywhere; + +/** + * A native library for doing unsafe things. Don't use this library. If + * you must ignore that warning, then be sure you use it sparingly and + * only in very well considered cases. + */ +public class Unsafe +{ + /** + * Enables or disables garbage collection. Warning: you will + * be fucked if you leave it disabled for too long. Do not do this + * unless you are dang sure about what you're doing and are prepared + * to test your code on every platform this side of Nantucket. + * + *

Calls to this method do not nest. Regardless of how many times + * you disable GC, only one call is required to reenable it. + */ + public static void setGCEnabled (boolean enabled) + { + // we don't support nesting, NOOP if the state doesn't change + if (_loaded && enabled != _gcEnabled) { + if (_gcEnabled = enabled) { + enableGC(); + } else { + disableGC(); + } + } + } + + /** + * Causes the current thread to block for the specified number of + * milliseconds. This exists primarily to work around the fact that on + * Linux, {@link Thread#sleep} is only accurate to around 12ms which + * is wholly unacceptable. + */ + public static void sleep (int millis) + { + if (_loaded && RunAnywhere.isLinux()) { + nativeSleep(millis); + } else { + try { + Thread.sleep(millis); + } catch (InterruptedException ie) { + Log.info("Thread.sleep(" + millis + ") interrupted."); + } + } + } + + /** + * Sets the processes uid to the specified value. + * + * @return true if the uid was changed, false if we were unable to do so. + */ + public static boolean setuid (int uid) + { + if (_loaded && !RunAnywhere.isWindows()) { + return nativeSetuid(uid); + } + return false; + } + + /** + * Sets the processes uid to the specified value. + * + * @return true if the uid was changed, false if we were unable to do so. + */ + public static boolean setgid (int gid) + { + if (_loaded && !RunAnywhere.isWindows()) { + return nativeSetgid(gid); + } + return false; + } + + /** + * Reenable garbage collection after a call to {@link #disableGC}. + */ + protected static native void enableGC (); + + /** + * Disables garbage collection. + */ + protected static native void disableGC (); + + /** + * Sleeps the current thread for the specified number of milliseconds. + */ + protected static native void nativeSleep (int millis); + + /** + * Calls through to the native OS system call to change our uid. + */ + protected static native boolean nativeSetuid (int uid); + + /** + * Calls through to the native OS system call to change our gid. + */ + protected static native boolean nativeSetgid (int gid); + + /** + * Called to initialize our library. + */ + protected static native boolean init (); + + /** The current state of GC enablement. */ + protected static boolean _gcEnabled = true; + + /** Whether or not we were able to load and initialize our library. */ + protected static boolean _loaded; + + static { + try { + System.loadLibrary("unsafe"); + _loaded = init(); + } catch (UnsatisfiedLinkError e) { + Log.warning("Failed to load 'unsafe' library: " + e + "."); + } + } +} diff --git a/src/java/com/threerings/util/unsafe/Windows/.cvsignore b/src/java/com/threerings/util/unsafe/Windows/.cvsignore new file mode 100644 index 00000000..c253038a --- /dev/null +++ b/src/java/com/threerings/util/unsafe/Windows/.cvsignore @@ -0,0 +1 @@ +*.def diff --git a/src/java/com/threerings/util/unsafe/Windows/Makefile b/src/java/com/threerings/util/unsafe/Windows/Makefile new file mode 100644 index 00000000..92c204ee --- /dev/null +++ b/src/java/com/threerings/util/unsafe/Windows/Makefile @@ -0,0 +1,49 @@ +# +# $Id: Makefile 3331 2005-02-03 01:25:21Z mdb $ + +NAME=unsafe + +# +# Executable definitions + +CC=i586-mingw32msvc-gcc +RM=rm +CP=cp +MKDIR=mkdir +DLLWRAP=i586-mingw32msvc-dllwrap +# +# Directory definitions + +ROOT=../../../../../../.. +INSTALL_PATH=${ROOT}/dist/lib/i686-Windows + +# +# Source files + +SRCS = com_threerings_util_unsafe_Unsafe.c +OBJS = ${SRCS:.c=.o} + +# +# Parameter and file definitions + +INCLUDES= +CFLAGS=-I.. -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux + +LDFLAGS=-L/usr/X11R6/lib +LIBS= +TARGET=${NAME}.dll +INSTALL_TARGET=${INSTALL_PATH}/${TARGET} + +# +# Target definitions + +all: ${INSTALL_TARGET} + +${INSTALL_TARGET}: ${OBJS} + @${MKDIR} -p ${INSTALL_PATH} + ${DLLWRAP} --output-def ${NAME}.def --add-stdcall-alias \ + -o ${INSTALL_TARGET} -s ${OBJS} + +clean: + -${RM} ${OBJS} + -${RM} ${INSTALL_TARGET} diff --git a/src/java/com/threerings/util/unsafe/Windows/com_threerings_util_unsafe_Unsafe.c b/src/java/com/threerings/util/unsafe/Windows/com_threerings_util_unsafe_Unsafe.c new file mode 100644 index 00000000..dcb2e950 --- /dev/null +++ b/src/java/com/threerings/util/unsafe/Windows/com_threerings_util_unsafe_Unsafe.c @@ -0,0 +1,52 @@ +/* + * $Id: com_threerings_util_unsafe_Unsafe.c 3331 2005-02-03 01:25:21Z mdb $ + */ + +#include +#include +#include + +#include "com_threerings_util_unsafe_Unsafe.h" + +/* global jvmpi interface pointer */ +static JVMPI_Interface* jvmpi; + +JNIEXPORT void JNICALL +Java_com_threerings_util_unsafe_Unsafe_enableGC (JNIEnv* env, jclass clazz) +{ + fprintf(stderr, "Reenabling GC.\n"); + jvmpi->EnableGC(); +} + +JNIEXPORT void JNICALL +Java_com_threerings_util_unsafe_Unsafe_disableGC (JNIEnv* env, jclass clazz) +{ + fprintf(stderr, "Disabling GC.\n"); + jvmpi->DisableGC(); +} + +JNIEXPORT void JNICALL +Java_com_threerings_util_unsafe_Unsafe_nativeSleep ( + JNIEnv* env, jclass clazz, jint millis) +{ + /* not supported */ +} + +JNIEXPORT jboolean JNICALL +Java_com_threerings_util_unsafe_Unsafe_init (JNIEnv* env, jclass clazz) +{ + JavaVM* jvm; + + if ((*env)->GetJavaVM(env, &jvm) > 0) { + fprintf(stderr, "Failed to get JavaVM from env.\n"); + return JNI_FALSE; + } + + /* get jvmpi interface pointer */ + if (((*jvm)->GetEnv(jvm, (void**)&jvmpi, JVMPI_VERSION_1)) < 0) { + fprintf(stderr, "Failed to get JVMPI from JavaVM.\n"); + return JNI_FALSE; + } + + return JNI_TRUE; +} diff --git a/src/java/com/threerings/util/unsafe/com_threerings_util_unsafe_Unsafe.h b/src/java/com/threerings/util/unsafe/com_threerings_util_unsafe_Unsafe.h new file mode 100644 index 00000000..f439fed8 --- /dev/null +++ b/src/java/com/threerings/util/unsafe/com_threerings_util_unsafe_Unsafe.h @@ -0,0 +1,47 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_threerings_util_unsafe_Unsafe */ + +#ifndef _Included_com_threerings_util_unsafe_Unsafe +#define _Included_com_threerings_util_unsafe_Unsafe +#ifdef __cplusplus +extern "C" { +#endif +/* Inaccessible static: _gcEnabled */ +/* Inaccessible static: _loaded */ +/* + * Class: com_threerings_util_unsafe_Unsafe + * Method: enableGC + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_com_threerings_util_unsafe_Unsafe_enableGC + (JNIEnv *, jclass); + +/* + * Class: com_threerings_util_unsafe_Unsafe + * Method: disableGC + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_com_threerings_util_unsafe_Unsafe_disableGC + (JNIEnv *, jclass); + +/* + * Class: com_threerings_util_unsafe_Unsafe + * Method: nativeSleep + * Signature: (I)V + */ +JNIEXPORT void JNICALL Java_com_threerings_util_unsafe_Unsafe_nativeSleep + (JNIEnv *, jclass, jint); + +/* + * Class: com_threerings_util_unsafe_Unsafe + * Method: init + * Signature: ()Z + */ +JNIEXPORT jboolean JNICALL Java_com_threerings_util_unsafe_Unsafe_init + (JNIEnv *, jclass); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/tests/bin/miso-viewer b/tests/bin/miso-viewer new file mode 100755 index 00000000..1ec88363 --- /dev/null +++ b/tests/bin/miso-viewer @@ -0,0 +1,3 @@ +#!/bin/sh + +`dirname $0`/runjava com.threerings.miso.viewer.ViewerApp $* diff --git a/tests/bin/runjava b/tests/bin/runjava new file mode 100755 index 00000000..356074ae --- /dev/null +++ b/tests/bin/runjava @@ -0,0 +1,108 @@ +#!/usr/bin/perl -w + +use Getopt::Std; + +my $usage = "Usage: $0 [-p pid_file] args\n"; + +# locations of stuff +chomp($location = `dirname $0`); + +# get the server root by popping the /bin off of our parent directory +@parts = split(/\//, $location); +pop(@parts); +my $root = join("/", @parts); + +# but we're in the test directory, so we want up one +$realroot = $root; +$root = "$root/.."; + +# make sure JAVA_HOME is set +my $jhome = $ENV{"JAVA_HOME"}; +if (!defined $jhome) { + warn "$0: Error: No JAVA_HOME specified!\n"; + warn "\n"; + warn "You must set your JAVA_HOME environment variable to the\n"; + warn "the absolute path of your JDK installation. For example:\n"; + warn "\n"; + warn " % JAVA_HOME=/usr/local/jdk1.2\n"; + warn " % export JAVA_HOME\n"; + die "\n"; +} + +# make sure it's set to a directory +if (! -d $jhome) { + die "$0: Can't find a java interpreter in '$jhome'.\n"; +} + +my $java = "$jhome/bin/java"; +my $jlib = "$jhome/lib/classes.zip"; + +# determine our machine architecture +my $ostype = `uname -s`; +my $machtype = `uname -m`; +chomp($ostype); +chomp($machtype); +my $arch = "$machtype-$ostype"; + +# add our native libraries to the runtime library path +my $libs = "$root/lib/$arch"; +my $libpath = $ENV{"LD_LIBRARY_PATH"}; + +if (defined $libpath) { + $ENV{"LD_LIBRARY_PATH"} = "$libs:$libpath"; +} else { + $ENV{"LD_LIBRARY_PATH"} = $libs; +} + +# put everything in our class path +my $classpath = "-classpath $root/tests/dist/classes:$root/dist/classes"; + +# add zip and jar files from our lib/ directory and the global Java +# libraries directory +my @dirs = ( "$root/lib" ); +foreach $dir (@dirs) { + next unless (defined $dir); + if (opendir(DIR, $dir)) { + foreach $lib (grep { /.(zip|jar)/ && -f "$dir/$_" } readdir(DIR)) { + # skip nenya-*.jar because we have the nenya build directory + # in our classpath + $classpath .= ":$dir/$lib" unless $lib =~ /nenya/; + } + closedir DIR; + } +} + +# finally add the standard classes +$classpath = "$classpath:$jlib"; + +# specify our server root (this is for server code) +my $rootarg = "-Dresource_dir=$realroot/rsrc -Dtest_dir=$realroot"; + +my $pid_file = undef; +my $i = 0; + +# strip out the -p args (we'd use getopt() but the damned thing provides +# no way of escaping arguments so that we can pass args to runjava that +# get passed down to the JVM) +for ($i = 0; $i < @ARGV; $i++) { + my $arg = $ARGV[$i]; + + # stop when we see -- (and strip it out because Java don't dig --) + if ($arg eq "--") { + splice(@ARGV, $i, 1); + last; + } + + if ($arg eq "-p") { + $pid_file = $ARGV[$i+1]; + splice(@ARGV, $i, 2); + $i -= 1; # decrement i so that things stay in sync + } +} + +# log the pid file if requested to do so +print `echo $$ > $pid_file` if (defined $pid_file); + +my $cmd = "$java -mx256M $classpath $rootarg " . join(" ", @ARGV); +# print "$cmd\n"; +exec($cmd); diff --git a/tests/build.xml b/tests/build.xml new file mode 100644 index 00000000..f346119f --- /dev/null +++ b/tests/build.xml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/rsrc/bundles/components/.cvsignore b/tests/rsrc/bundles/components/.cvsignore new file mode 100644 index 00000000..1a4f5c01 --- /dev/null +++ b/tests/rsrc/bundles/components/.cvsignore @@ -0,0 +1,2 @@ +*.jar +*.map diff --git a/tests/rsrc/bundles/components/actions.xml b/tests/rsrc/bundles/components/actions.xml new file mode 100644 index 00000000..4ee164b4 --- /dev/null +++ b/tests/rsrc/bundles/components/actions.xml @@ -0,0 +1,76 @@ + + + + + + + + 5 + 50,112 + SW, W, NW, N, NE, E, SE, S + + 160, 160, 160, 160, 160, 160, 160, 160 + 100, 100, 100, 100, 100, 100, 100, 100 + 1, 1, 1, 1, 1, 1, 1, 1 + 0, 0 + 0, 0 + + + + + 7.3 + 50,112 + SW, W, NW, N, NE, E, SE, S + + 160, 160, 160, 160, 160, 160, 160, 160 + 100, 100, 100, 100, 100, 100, 100, 100 + 6, 6, 6, 6, 6, 6, 6, 6 + 0, 0 + 0, 0 + + + + + 5 + 50,112 + SW, W, NW, N, NE, E, SE, S + + 160, 160, 160, 160, 160, 160, 160, 160 + 100, 100, 100, 100, 100, 100, 100, 100 + 1, 1, 1, 1, 1, 1, 1, 1 + 0, 0 + 0, 0 + + + + + + 8 + 87,80 + + S, SSW, SW, WSW, W, WNW, NW, NNW, N, NNE, NE, ENE, E, ESE, SE, SSE + + + 168, 168, 168, 168, 168, 168, 168, 168 + 128, 128, 128, 128, 128, 128, 128, 128 + 12, 12, 12, 12, 12, 12, 12, 12 + 0, 0 + 0, 0 + + + + + 8 + 42,34 + + S, SSW, SW, WSW, W, WNW, NW, NNW, N, NNE, NE, ENE, E, ESE, SE, SSE + + + 84, 84, 84, 84, 84, 84, 84, 84 + 64, 64, 64, 64, 64, 64, 64, 64 + 2, 2, 2, 2, 2, 2, 2, 2 + 0, 0 + 0, 0 + + + diff --git a/tests/rsrc/bundles/components/classes.xml b/tests/rsrc/bundles/components/classes.xml new file mode 100644 index 00000000..4ccce56b --- /dev/null +++ b/tests/rsrc/bundles/components/classes.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/rsrc/bundles/components/pirate/.cvsignore b/tests/rsrc/bundles/components/pirate/.cvsignore new file mode 100644 index 00000000..d392f0e8 --- /dev/null +++ b/tests/rsrc/bundles/components/pirate/.cvsignore @@ -0,0 +1 @@ +*.jar diff --git a/tests/rsrc/bundles/components/pirate/female/feet/barefoot/behind_back.png b/tests/rsrc/bundles/components/pirate/female/feet/barefoot/behind_back.png new file mode 100644 index 00000000..fa249dec Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/barefoot/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/barefoot/standing.png b/tests/rsrc/bundles/components/pirate/female/feet/barefoot/standing.png new file mode 100644 index 00000000..04a2b2c7 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/barefoot/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/barefoot/walking.png b/tests/rsrc/bundles/components/pirate/female/feet/barefoot/walking.png new file mode 100644 index 00000000..529c9289 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/barefoot/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/boots/behind_back.png b/tests/rsrc/bundles/components/pirate/female/feet/boots/behind_back.png new file mode 100644 index 00000000..36b8742b Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/boots/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/boots/standing.png b/tests/rsrc/bundles/components/pirate/female/feet/boots/standing.png new file mode 100644 index 00000000..796d9ad3 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/boots/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/boots/walking.png b/tests/rsrc/bundles/components/pirate/female/feet/boots/walking.png new file mode 100644 index 00000000..69b109ac Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/boots/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/buckleshoes/behind_back.png b/tests/rsrc/bundles/components/pirate/female/feet/buckleshoes/behind_back.png new file mode 100644 index 00000000..311ddc59 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/buckleshoes/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/buckleshoes/standing.png b/tests/rsrc/bundles/components/pirate/female/feet/buckleshoes/standing.png new file mode 100644 index 00000000..3e58c5ea Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/buckleshoes/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/buckleshoes/walking.png b/tests/rsrc/bundles/components/pirate/female/feet/buckleshoes/walking.png new file mode 100644 index 00000000..b9be4047 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/buckleshoes/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/pegleg_barefoot/behind_back.png b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_barefoot/behind_back.png new file mode 100644 index 00000000..e97c5124 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_barefoot/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/pegleg_barefoot/standing.png b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_barefoot/standing.png new file mode 100644 index 00000000..d05f58e8 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_barefoot/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/pegleg_barefoot/walking.png b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_barefoot/walking.png new file mode 100644 index 00000000..8d06f3e5 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_barefoot/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/pegleg_boots/behind_back.png b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_boots/behind_back.png new file mode 100644 index 00000000..f47e5fc7 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_boots/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/pegleg_boots/standing.png b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_boots/standing.png new file mode 100644 index 00000000..087f8d08 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_boots/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/pegleg_boots/walking.png b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_boots/walking.png new file mode 100644 index 00000000..fd6086ab Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_boots/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/pegleg_buckleshoes/behind_back.png b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_buckleshoes/behind_back.png new file mode 100644 index 00000000..7944f1ae Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_buckleshoes/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/pegleg_buckleshoes/standing.png b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_buckleshoes/standing.png new file mode 100644 index 00000000..2240c2ea Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_buckleshoes/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/feet/pegleg_buckleshoes/walking.png b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_buckleshoes/walking.png new file mode 100644 index 00000000..334c2b12 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/feet/pegleg_buckleshoes/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hair/long_straight/behind_back.png b/tests/rsrc/bundles/components/pirate/female/hair/long_straight/behind_back.png new file mode 100644 index 00000000..a525f1ac Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hair/long_straight/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hair/long_straight/standing.png b/tests/rsrc/bundles/components/pirate/female/hair/long_straight/standing.png new file mode 100644 index 00000000..6e3c4a90 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hair/long_straight/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hair/long_straight/walking.png b/tests/rsrc/bundles/components/pirate/female/hair/long_straight/walking.png new file mode 100644 index 00000000..2538c069 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hair/long_straight/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hair/messy_short/behind_back.png b/tests/rsrc/bundles/components/pirate/female/hair/messy_short/behind_back.png new file mode 100644 index 00000000..0ee738bc Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hair/messy_short/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hair/messy_short/standing.png b/tests/rsrc/bundles/components/pirate/female/hair/messy_short/standing.png new file mode 100644 index 00000000..c3e63139 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hair/messy_short/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hair/messy_short/walking.png b/tests/rsrc/bundles/components/pirate/female/hair/messy_short/walking.png new file mode 100644 index 00000000..ded1602e Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hair/messy_short/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hair/ponytail/behind_back.png b/tests/rsrc/bundles/components/pirate/female/hair/ponytail/behind_back.png new file mode 100644 index 00000000..6fd55964 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hair/ponytail/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hair/ponytail/standing.png b/tests/rsrc/bundles/components/pirate/female/hair/ponytail/standing.png new file mode 100644 index 00000000..86bd3c0b Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hair/ponytail/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hair/ponytail/walking.png b/tests/rsrc/bundles/components/pirate/female/hair/ponytail/walking.png new file mode 100644 index 00000000..33a7c454 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hair/ponytail/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hand_left/barehand/behind_back.png b/tests/rsrc/bundles/components/pirate/female/hand_left/barehand/behind_back.png new file mode 100644 index 00000000..0c631d03 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hand_left/barehand/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hand_left/barehand/standing.png b/tests/rsrc/bundles/components/pirate/female/hand_left/barehand/standing.png new file mode 100644 index 00000000..e50ac6d6 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hand_left/barehand/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hand_left/barehand/walking.png b/tests/rsrc/bundles/components/pirate/female/hand_left/barehand/walking.png new file mode 100644 index 00000000..e55edf3b Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hand_left/barehand/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hand_left/hook/behind_back.png b/tests/rsrc/bundles/components/pirate/female/hand_left/hook/behind_back.png new file mode 100644 index 00000000..4b02ee9e Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hand_left/hook/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hand_left/hook/standing.png b/tests/rsrc/bundles/components/pirate/female/hand_left/hook/standing.png new file mode 100644 index 00000000..87a7e36d Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hand_left/hook/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hand_left/hook/walking.png b/tests/rsrc/bundles/components/pirate/female/hand_left/hook/walking.png new file mode 100644 index 00000000..abfa1930 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hand_left/hook/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hand_right/barehand/behind_back.png b/tests/rsrc/bundles/components/pirate/female/hand_right/barehand/behind_back.png new file mode 100644 index 00000000..88ace639 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hand_right/barehand/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hand_right/barehand/standing.png b/tests/rsrc/bundles/components/pirate/female/hand_right/barehand/standing.png new file mode 100644 index 00000000..db839959 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hand_right/barehand/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hand_right/barehand/walking.png b/tests/rsrc/bundles/components/pirate/female/hand_right/barehand/walking.png new file mode 100644 index 00000000..49d613ac Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hand_right/barehand/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/captains/behind_back.png b/tests/rsrc/bundles/components/pirate/female/hat/captains/behind_back.png new file mode 100644 index 00000000..c8c43a8a Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/captains/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/captains/standing.png b/tests/rsrc/bundles/components/pirate/female/hat/captains/standing.png new file mode 100644 index 00000000..8f91ec1b Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/captains/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/captains/walking.png b/tests/rsrc/bundles/components/pirate/female/hat/captains/walking.png new file mode 100644 index 00000000..87690fdc Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/captains/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/small_feathered/behind_back.png b/tests/rsrc/bundles/components/pirate/female/hat/small_feathered/behind_back.png new file mode 100644 index 00000000..5329f83c Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/small_feathered/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/small_feathered/standing.png b/tests/rsrc/bundles/components/pirate/female/hat/small_feathered/standing.png new file mode 100644 index 00000000..5cf0f142 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/small_feathered/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/small_feathered/walking.png b/tests/rsrc/bundles/components/pirate/female/hat/small_feathered/walking.png new file mode 100644 index 00000000..77a67592 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/small_feathered/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/tricorne/behind_back.png b/tests/rsrc/bundles/components/pirate/female/hat/tricorne/behind_back.png new file mode 100644 index 00000000..0197fc3d Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/tricorne/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/tricorne/standing.png b/tests/rsrc/bundles/components/pirate/female/hat/tricorne/standing.png new file mode 100644 index 00000000..404d7c1a Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/tricorne/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/tricorne/walking.png b/tests/rsrc/bundles/components/pirate/female/hat/tricorne/walking.png new file mode 100644 index 00000000..1bb3e2b8 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/tricorne/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/wide_brimmed/behind_back.png b/tests/rsrc/bundles/components/pirate/female/hat/wide_brimmed/behind_back.png new file mode 100644 index 00000000..b040e819 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/wide_brimmed/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/wide_brimmed/standing.png b/tests/rsrc/bundles/components/pirate/female/hat/wide_brimmed/standing.png new file mode 100644 index 00000000..83029db1 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/wide_brimmed/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/hat/wide_brimmed/walking.png b/tests/rsrc/bundles/components/pirate/female/hat/wide_brimmed/walking.png new file mode 100644 index 00000000..9a4469f9 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/hat/wide_brimmed/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/head/eyepatch/behind_back.png b/tests/rsrc/bundles/components/pirate/female/head/eyepatch/behind_back.png new file mode 100644 index 00000000..8f56599c Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/head/eyepatch/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/head/eyepatch/standing.png b/tests/rsrc/bundles/components/pirate/female/head/eyepatch/standing.png new file mode 100644 index 00000000..edfc0859 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/head/eyepatch/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/head/eyepatch/walking.png b/tests/rsrc/bundles/components/pirate/female/head/eyepatch/walking.png new file mode 100644 index 00000000..cd55ca68 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/head/eyepatch/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/head/normal/behind_back.png b/tests/rsrc/bundles/components/pirate/female/head/normal/behind_back.png new file mode 100644 index 00000000..377c5076 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/head/normal/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/head/normal/standing.png b/tests/rsrc/bundles/components/pirate/female/head/normal/standing.png new file mode 100644 index 00000000..9a38d412 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/head/normal/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/head/normal/walking.png b/tests/rsrc/bundles/components/pirate/female/head/normal/walking.png new file mode 100644 index 00000000..34963e8f Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/head/normal/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/legs/dressbottom_long/behind_back.png b/tests/rsrc/bundles/components/pirate/female/legs/dressbottom_long/behind_back.png new file mode 100644 index 00000000..d082a11c Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/legs/dressbottom_long/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/legs/dressbottom_long/standing.png b/tests/rsrc/bundles/components/pirate/female/legs/dressbottom_long/standing.png new file mode 100644 index 00000000..c291a6d0 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/legs/dressbottom_long/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/legs/dressbottom_long/walking.png b/tests/rsrc/bundles/components/pirate/female/legs/dressbottom_long/walking.png new file mode 100644 index 00000000..75789a7a Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/legs/dressbottom_long/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/legs/stripey_knickers/behind_back.png b/tests/rsrc/bundles/components/pirate/female/legs/stripey_knickers/behind_back.png new file mode 100644 index 00000000..27f03b3c Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/legs/stripey_knickers/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/legs/stripey_knickers/standing.png b/tests/rsrc/bundles/components/pirate/female/legs/stripey_knickers/standing.png new file mode 100644 index 00000000..c5d572fe Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/legs/stripey_knickers/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/legs/stripey_knickers/walking.png b/tests/rsrc/bundles/components/pirate/female/legs/stripey_knickers/walking.png new file mode 100644 index 00000000..07bb2d2a Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/legs/stripey_knickers/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/torso/bodice/behind_back.png b/tests/rsrc/bundles/components/pirate/female/torso/bodice/behind_back.png new file mode 100644 index 00000000..b0706e7b Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/torso/bodice/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/torso/bodice/standing.png b/tests/rsrc/bundles/components/pirate/female/torso/bodice/standing.png new file mode 100644 index 00000000..eae1c1a9 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/torso/bodice/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/torso/bodice/walking.png b/tests/rsrc/bundles/components/pirate/female/torso/bodice/walking.png new file mode 100644 index 00000000..880d4485 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/torso/bodice/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/torso/dresstop/behind_back.png b/tests/rsrc/bundles/components/pirate/female/torso/dresstop/behind_back.png new file mode 100644 index 00000000..71f771a1 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/torso/dresstop/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/torso/dresstop/standing.png b/tests/rsrc/bundles/components/pirate/female/torso/dresstop/standing.png new file mode 100644 index 00000000..814ff9ee Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/torso/dresstop/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/female/torso/dresstop/walking.png b/tests/rsrc/bundles/components/pirate/female/torso/dresstop/walking.png new file mode 100644 index 00000000..7cdda709 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/female/torso/dresstop/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/eyepatch/blackpatch/behind_back.png b/tests/rsrc/bundles/components/pirate/male/eyepatch/blackpatch/behind_back.png new file mode 100644 index 00000000..b3b1ba3a Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/eyepatch/blackpatch/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/eyepatch/blackpatch/standing.png b/tests/rsrc/bundles/components/pirate/male/eyepatch/blackpatch/standing.png new file mode 100644 index 00000000..a5716811 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/eyepatch/blackpatch/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/eyepatch/blackpatch/walking.png b/tests/rsrc/bundles/components/pirate/male/eyepatch/blackpatch/walking.png new file mode 100644 index 00000000..afc22015 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/eyepatch/blackpatch/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/barefoot/behind_back.png b/tests/rsrc/bundles/components/pirate/male/feet/barefoot/behind_back.png new file mode 100644 index 00000000..274bcc49 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/barefoot/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/barefoot/standing.png b/tests/rsrc/bundles/components/pirate/male/feet/barefoot/standing.png new file mode 100644 index 00000000..9ed34de9 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/barefoot/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/barefoot/walking.png b/tests/rsrc/bundles/components/pirate/male/feet/barefoot/walking.png new file mode 100644 index 00000000..01bc8ef3 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/barefoot/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/boots/behind_back.png b/tests/rsrc/bundles/components/pirate/male/feet/boots/behind_back.png new file mode 100644 index 00000000..ebb6c2fa Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/boots/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/boots/standing.png b/tests/rsrc/bundles/components/pirate/male/feet/boots/standing.png new file mode 100644 index 00000000..9736c9cf Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/boots/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/boots/walking.png b/tests/rsrc/bundles/components/pirate/male/feet/boots/walking.png new file mode 100644 index 00000000..884426b2 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/boots/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/pegleg_barefoot/behind_back.png b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_barefoot/behind_back.png new file mode 100644 index 00000000..64bc01c3 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_barefoot/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/pegleg_barefoot/standing.png b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_barefoot/standing.png new file mode 100644 index 00000000..0e9b3410 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_barefoot/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/pegleg_barefoot/walking.png b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_barefoot/walking.png new file mode 100644 index 00000000..c1acb564 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_barefoot/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/pegleg_boots/behind_back.png b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_boots/behind_back.png new file mode 100644 index 00000000..370378f3 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_boots/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/pegleg_boots/standing.png b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_boots/standing.png new file mode 100644 index 00000000..db3278a2 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_boots/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/feet/pegleg_boots/walking.png b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_boots/walking.png new file mode 100644 index 00000000..1d2251c4 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/feet/pegleg_boots/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hair/ponytail/behind_back.png b/tests/rsrc/bundles/components/pirate/male/hair/ponytail/behind_back.png new file mode 100644 index 00000000..18f7959f Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hair/ponytail/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hair/ponytail/standing.png b/tests/rsrc/bundles/components/pirate/male/hair/ponytail/standing.png new file mode 100644 index 00000000..bab363ae Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hair/ponytail/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hair/ponytail/walking.png b/tests/rsrc/bundles/components/pirate/male/hair/ponytail/walking.png new file mode 100644 index 00000000..7481d333 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hair/ponytail/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hair/princevaliant/behind_back.png b/tests/rsrc/bundles/components/pirate/male/hair/princevaliant/behind_back.png new file mode 100644 index 00000000..7a128f69 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hair/princevaliant/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hair/princevaliant/standing.png b/tests/rsrc/bundles/components/pirate/male/hair/princevaliant/standing.png new file mode 100644 index 00000000..377fe5f7 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hair/princevaliant/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hair/princevaliant/walking.png b/tests/rsrc/bundles/components/pirate/male/hair/princevaliant/walking.png new file mode 100644 index 00000000..7f927230 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hair/princevaliant/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hand_left/barehand/behind_back.png b/tests/rsrc/bundles/components/pirate/male/hand_left/barehand/behind_back.png new file mode 100644 index 00000000..aee02e40 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hand_left/barehand/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hand_left/barehand/standing.png b/tests/rsrc/bundles/components/pirate/male/hand_left/barehand/standing.png new file mode 100644 index 00000000..854a4102 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hand_left/barehand/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hand_left/barehand/walking.png b/tests/rsrc/bundles/components/pirate/male/hand_left/barehand/walking.png new file mode 100644 index 00000000..72113e81 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hand_left/barehand/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hand_left/hook/behind_back.png b/tests/rsrc/bundles/components/pirate/male/hand_left/hook/behind_back.png new file mode 100644 index 00000000..1ad1e64e Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hand_left/hook/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hand_left/hook/standing.png b/tests/rsrc/bundles/components/pirate/male/hand_left/hook/standing.png new file mode 100644 index 00000000..82d87293 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hand_left/hook/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hand_left/hook/walking.png b/tests/rsrc/bundles/components/pirate/male/hand_left/hook/walking.png new file mode 100644 index 00000000..7ebffa05 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hand_left/hook/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hand_right/barehand/behind_back.png b/tests/rsrc/bundles/components/pirate/male/hand_right/barehand/behind_back.png new file mode 100644 index 00000000..8885be32 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hand_right/barehand/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hand_right/barehand/standing.png b/tests/rsrc/bundles/components/pirate/male/hand_right/barehand/standing.png new file mode 100644 index 00000000..3fbf6547 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hand_right/barehand/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hand_right/barehand/walking.png b/tests/rsrc/bundles/components/pirate/male/hand_right/barehand/walking.png new file mode 100644 index 00000000..0ecbe245 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hand_right/barehand/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hat/bandana/behind_back.png b/tests/rsrc/bundles/components/pirate/male/hat/bandana/behind_back.png new file mode 100644 index 00000000..8366a569 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hat/bandana/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hat/bandana/standing.png b/tests/rsrc/bundles/components/pirate/male/hat/bandana/standing.png new file mode 100644 index 00000000..f5df63c9 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hat/bandana/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hat/bandana/walking.png b/tests/rsrc/bundles/components/pirate/male/hat/bandana/walking.png new file mode 100644 index 00000000..574e966f Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hat/bandana/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hat/captains/behind_back.png b/tests/rsrc/bundles/components/pirate/male/hat/captains/behind_back.png new file mode 100644 index 00000000..9a77ae34 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hat/captains/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hat/captains/standing.png b/tests/rsrc/bundles/components/pirate/male/hat/captains/standing.png new file mode 100644 index 00000000..e930b059 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hat/captains/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hat/captains/walking.png b/tests/rsrc/bundles/components/pirate/male/hat/captains/walking.png new file mode 100644 index 00000000..6544eede Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hat/captains/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hat/sleepinghat/behind_back.png b/tests/rsrc/bundles/components/pirate/male/hat/sleepinghat/behind_back.png new file mode 100644 index 00000000..58024fdc Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hat/sleepinghat/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hat/sleepinghat/standing.png b/tests/rsrc/bundles/components/pirate/male/hat/sleepinghat/standing.png new file mode 100644 index 00000000..2240d51e Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hat/sleepinghat/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/hat/sleepinghat/walking.png b/tests/rsrc/bundles/components/pirate/male/hat/sleepinghat/walking.png new file mode 100644 index 00000000..0ba521a2 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/hat/sleepinghat/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/head/bald/behind_back.png b/tests/rsrc/bundles/components/pirate/male/head/bald/behind_back.png new file mode 100644 index 00000000..f253f5a3 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/head/bald/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/head/bald/standing.png b/tests/rsrc/bundles/components/pirate/male/head/bald/standing.png new file mode 100644 index 00000000..22b57541 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/head/bald/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/head/bald/walking.png b/tests/rsrc/bundles/components/pirate/male/head/bald/walking.png new file mode 100644 index 00000000..af521e20 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/head/bald/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/head/goatee/behind_back.png b/tests/rsrc/bundles/components/pirate/male/head/goatee/behind_back.png new file mode 100644 index 00000000..d6188631 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/head/goatee/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/head/goatee/standing.png b/tests/rsrc/bundles/components/pirate/male/head/goatee/standing.png new file mode 100644 index 00000000..fe2a83e8 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/head/goatee/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/head/goatee/walking.png b/tests/rsrc/bundles/components/pirate/male/head/goatee/walking.png new file mode 100644 index 00000000..d6f8c761 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/head/goatee/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/head/shortbeard/behind_back.png b/tests/rsrc/bundles/components/pirate/male/head/shortbeard/behind_back.png new file mode 100644 index 00000000..a9107a7e Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/head/shortbeard/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/head/shortbeard/standing.png b/tests/rsrc/bundles/components/pirate/male/head/shortbeard/standing.png new file mode 100644 index 00000000..8df2e693 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/head/shortbeard/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/head/shortbeard/walking.png b/tests/rsrc/bundles/components/pirate/male/head/shortbeard/walking.png new file mode 100644 index 00000000..95adbbb8 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/head/shortbeard/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/legs/pants/behind_back.png b/tests/rsrc/bundles/components/pirate/male/legs/pants/behind_back.png new file mode 100644 index 00000000..d5f4d7b3 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/legs/pants/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/legs/pants/standing.png b/tests/rsrc/bundles/components/pirate/male/legs/pants/standing.png new file mode 100644 index 00000000..bd44895f Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/legs/pants/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/legs/pants/walking.png b/tests/rsrc/bundles/components/pirate/male/legs/pants/walking.png new file mode 100644 index 00000000..7ffca8f2 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/legs/pants/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/legs/stripeyknickers/behind_back.png b/tests/rsrc/bundles/components/pirate/male/legs/stripeyknickers/behind_back.png new file mode 100644 index 00000000..cfea577a Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/legs/stripeyknickers/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/legs/stripeyknickers/standing.png b/tests/rsrc/bundles/components/pirate/male/legs/stripeyknickers/standing.png new file mode 100644 index 00000000..29c2b1c8 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/legs/stripeyknickers/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/legs/stripeyknickers/walking.png b/tests/rsrc/bundles/components/pirate/male/legs/stripeyknickers/walking.png new file mode 100644 index 00000000..65a7f87e Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/legs/stripeyknickers/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/torso/shirt_sash/behind_back.png b/tests/rsrc/bundles/components/pirate/male/torso/shirt_sash/behind_back.png new file mode 100644 index 00000000..7e66a6a8 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/torso/shirt_sash/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/torso/shirt_sash/standing.png b/tests/rsrc/bundles/components/pirate/male/torso/shirt_sash/standing.png new file mode 100644 index 00000000..e832b040 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/torso/shirt_sash/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/torso/shirt_sash/walking.png b/tests/rsrc/bundles/components/pirate/male/torso/shirt_sash/walking.png new file mode 100644 index 00000000..2606fe00 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/torso/shirt_sash/walking.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/torso/vest_stripedshirt_belt/behind_back.png b/tests/rsrc/bundles/components/pirate/male/torso/vest_stripedshirt_belt/behind_back.png new file mode 100644 index 00000000..4e57a6cb Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/torso/vest_stripedshirt_belt/behind_back.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/torso/vest_stripedshirt_belt/standing.png b/tests/rsrc/bundles/components/pirate/male/torso/vest_stripedshirt_belt/standing.png new file mode 100644 index 00000000..202b318c Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/torso/vest_stripedshirt_belt/standing.png differ diff --git a/tests/rsrc/bundles/components/pirate/male/torso/vest_stripedshirt_belt/walking.png b/tests/rsrc/bundles/components/pirate/male/torso/vest_stripedshirt_belt/walking.png new file mode 100644 index 00000000..74e82679 Binary files /dev/null and b/tests/rsrc/bundles/components/pirate/male/torso/vest_stripedshirt_belt/walking.png differ diff --git a/tests/rsrc/bundles/components/vessel/.cvsignore b/tests/rsrc/bundles/components/vessel/.cvsignore new file mode 100644 index 00000000..d392f0e8 --- /dev/null +++ b/tests/rsrc/bundles/components/vessel/.cvsignore @@ -0,0 +1 @@ +*.jar diff --git a/tests/rsrc/bundles/components/vessel/navsail/merchgal/sailing.png b/tests/rsrc/bundles/components/vessel/navsail/merchgal/sailing.png new file mode 100644 index 00000000..007f7ca6 Binary files /dev/null and b/tests/rsrc/bundles/components/vessel/navsail/merchgal/sailing.png differ diff --git a/tests/rsrc/bundles/components/vessel/navsail/merchgal/sailing_small.png b/tests/rsrc/bundles/components/vessel/navsail/merchgal/sailing_small.png new file mode 100644 index 00000000..96f728e7 Binary files /dev/null and b/tests/rsrc/bundles/components/vessel/navsail/merchgal/sailing_small.png differ diff --git a/tests/rsrc/bundles/components/vessel/navsail/smsloop/sailing.png b/tests/rsrc/bundles/components/vessel/navsail/smsloop/sailing.png new file mode 100644 index 00000000..c09b89ea Binary files /dev/null and b/tests/rsrc/bundles/components/vessel/navsail/smsloop/sailing.png differ diff --git a/tests/rsrc/bundles/components/vessel/navsail/smsloop/sailing_small.png b/tests/rsrc/bundles/components/vessel/navsail/smsloop/sailing_small.png new file mode 100644 index 00000000..5e6c5b6d Binary files /dev/null and b/tests/rsrc/bundles/components/vessel/navsail/smsloop/sailing_small.png differ diff --git a/tests/rsrc/bundles/tiles/.cvsignore b/tests/rsrc/bundles/tiles/.cvsignore new file mode 100644 index 00000000..2eac42e1 --- /dev/null +++ b/tests/rsrc/bundles/tiles/.cvsignore @@ -0,0 +1 @@ +tilesets.map diff --git a/tests/rsrc/bundles/tiles/bundler-config.xml b/tests/rsrc/bundles/tiles/bundler-config.xml new file mode 100644 index 00000000..19103b66 --- /dev/null +++ b/tests/rsrc/bundles/tiles/bundler-config.xml @@ -0,0 +1,28 @@ + + + + + + + bundle/base + + com.threerings.miso.tile.tools.xml.BaseTileSetRuleSet + + + + + + bundle/fringe + + com.threerings.media.tile.tools.xml.SwissArmyTileSetRuleSet + + + + + + bundle/object + + com.threerings.media.tile.tools.xml.ObjectTileSetRuleSet + + + diff --git a/tests/rsrc/bundles/tiles/ground/.cvsignore b/tests/rsrc/bundles/tiles/ground/.cvsignore new file mode 100644 index 00000000..edfd8732 --- /dev/null +++ b/tests/rsrc/bundles/tiles/ground/.cvsignore @@ -0,0 +1 @@ +bundle.jar diff --git a/tests/rsrc/bundles/tiles/ground/bundle.xml b/tests/rsrc/bundles/tiles/ground/bundle.xml new file mode 100644 index 00000000..6f2c55cd --- /dev/null +++ b/tests/rsrc/bundles/tiles/ground/bundle.xml @@ -0,0 +1,70 @@ + + + + + + + + cobble.png + 48 + 64 + 3 + 1, 1, 1 + + + + dirt.png + 48 + 64 + 3 + 1, 1, 1 + + + + sand.png + 48 + 64 + 3 + 1, 1, 1 + + + + woodplanks.png + 48 + 64 + 3 + 1, 1, 1 + + + + seawater.png + 48 + 64 + 5 + 0, 0, 0, 0, 0 + + + + + + fringe.png + 48, 48, 48, 48, 48 + 64, 64, 64, 64, 64 + 8, 8, 8, 8, 8 + + + + seafringe.png + 48 + 64 + 17 + + + + fringemask.png + 48 + 64 + 17 + + + diff --git a/tests/rsrc/bundles/tiles/ground/cobble.png b/tests/rsrc/bundles/tiles/ground/cobble.png new file mode 100644 index 00000000..e3a1c82c Binary files /dev/null and b/tests/rsrc/bundles/tiles/ground/cobble.png differ diff --git a/tests/rsrc/bundles/tiles/ground/dirt.png b/tests/rsrc/bundles/tiles/ground/dirt.png new file mode 100644 index 00000000..12286a6a Binary files /dev/null and b/tests/rsrc/bundles/tiles/ground/dirt.png differ diff --git a/tests/rsrc/bundles/tiles/ground/fringe.png b/tests/rsrc/bundles/tiles/ground/fringe.png new file mode 100644 index 00000000..390f4561 Binary files /dev/null and b/tests/rsrc/bundles/tiles/ground/fringe.png differ diff --git a/tests/rsrc/bundles/tiles/ground/fringemask.png b/tests/rsrc/bundles/tiles/ground/fringemask.png new file mode 100644 index 00000000..fb539fc1 Binary files /dev/null and b/tests/rsrc/bundles/tiles/ground/fringemask.png differ diff --git a/tests/rsrc/bundles/tiles/ground/sand.png b/tests/rsrc/bundles/tiles/ground/sand.png new file mode 100644 index 00000000..c586d4cc Binary files /dev/null and b/tests/rsrc/bundles/tiles/ground/sand.png differ diff --git a/tests/rsrc/bundles/tiles/ground/seafringe.png b/tests/rsrc/bundles/tiles/ground/seafringe.png new file mode 100644 index 00000000..17d86326 Binary files /dev/null and b/tests/rsrc/bundles/tiles/ground/seafringe.png differ diff --git a/tests/rsrc/bundles/tiles/ground/seawater.png b/tests/rsrc/bundles/tiles/ground/seawater.png new file mode 100644 index 00000000..0de609ff Binary files /dev/null and b/tests/rsrc/bundles/tiles/ground/seawater.png differ diff --git a/tests/rsrc/bundles/tiles/ground/woodplanks.png b/tests/rsrc/bundles/tiles/ground/woodplanks.png new file mode 100644 index 00000000..b89b6afe Binary files /dev/null and b/tests/rsrc/bundles/tiles/ground/woodplanks.png differ diff --git a/tests/rsrc/bundles/tiles/objects/.cvsignore b/tests/rsrc/bundles/tiles/objects/.cvsignore new file mode 100644 index 00000000..edfd8732 --- /dev/null +++ b/tests/rsrc/bundles/tiles/objects/.cvsignore @@ -0,0 +1 @@ +bundle.jar diff --git a/tests/rsrc/bundles/tiles/objects/bag_cloth.png b/tests/rsrc/bundles/tiles/objects/bag_cloth.png new file mode 100644 index 00000000..09eebd9a Binary files /dev/null and b/tests/rsrc/bundles/tiles/objects/bag_cloth.png differ diff --git a/tests/rsrc/bundles/tiles/objects/barrel_oak.png b/tests/rsrc/bundles/tiles/objects/barrel_oak.png new file mode 100644 index 00000000..e89c18af Binary files /dev/null and b/tests/rsrc/bundles/tiles/objects/barrel_oak.png differ diff --git a/tests/rsrc/bundles/tiles/objects/bundle.xml b/tests/rsrc/bundles/tiles/objects/bundle.xml new file mode 100644 index 00000000..6d90278c --- /dev/null +++ b/tests/rsrc/bundles/tiles/objects/bundle.xml @@ -0,0 +1,137 @@ + + + + + + + + house.png + 380 + 480 + 4 + 0, 0 + 0, 0 + 4, 5, 4, 5 + 5, 4, 5, 4 + 173, 205, 173, 205 + 466, 466, 466, 466 + + + + tree_palm.png + 300 + 480 + 4 + 0, 0 + 0, 0 + 1, 1, 1, 1 + 1, 1, 1, 1 + 149, 149, 149, 149 + 456, 456, 456, 456 + + + + dockedge.png + 100 + 200 + 4 + 0, 0 + 0, 0 + 1, 1, 1, 1 + 1, 1, 1, 1 + 49, 49, 49, 49 + 176, 176, 176, 176 + + + + bag_cloth.png + 100 + 200 + 1 + 0, 0 + 0, 0 + 1 + 1 + 49 + 176 + + + + barrel_oak.png + 100 + 200 + 1 + 0, 0 + 0, 0 + 1 + 1 + 49 + 176 + + + + crate.png + 100 + 200 + 2 + 0, 0 + 0, 0 + 1, 1 + 1, 1 + 49, 49 + 176, 176 + + + + mooring.png + 100 + 200 + 4 + 0, 0 + 0, 0 + 1, 1, 1, 1 + 1, 1, 1, 1 + 49, 49, 49, 49 + 176, 176, 176, 176 + + + + tree_oak.png + 300 + 480 + 2 + 0, 0 + 0, 0 + 1, 1 + 1, 1 + 149, 149 + 456, 456 + + + + chest_treasure.png + 200 + 280 + 2 + 0, 0 + 0, 0 + 2, 2 + 2, 2 + 99, 99 + 245, 245 + + + + well.png + 200 + 280 + 2 + 0, 0 + 0, 0 + 2, 2 + 2, 2 + 99, 99 + 245, 245 + + + diff --git a/tests/rsrc/bundles/tiles/objects/chest_purse.png b/tests/rsrc/bundles/tiles/objects/chest_purse.png new file mode 100644 index 00000000..212ee172 Binary files /dev/null and b/tests/rsrc/bundles/tiles/objects/chest_purse.png differ diff --git a/tests/rsrc/bundles/tiles/objects/chest_treasure.png b/tests/rsrc/bundles/tiles/objects/chest_treasure.png new file mode 100644 index 00000000..9bab9fb6 Binary files /dev/null and b/tests/rsrc/bundles/tiles/objects/chest_treasure.png differ diff --git a/tests/rsrc/bundles/tiles/objects/crate.png b/tests/rsrc/bundles/tiles/objects/crate.png new file mode 100644 index 00000000..f5befa89 Binary files /dev/null and b/tests/rsrc/bundles/tiles/objects/crate.png differ diff --git a/tests/rsrc/bundles/tiles/objects/dockedge.png b/tests/rsrc/bundles/tiles/objects/dockedge.png new file mode 100644 index 00000000..bf127090 Binary files /dev/null and b/tests/rsrc/bundles/tiles/objects/dockedge.png differ diff --git a/tests/rsrc/bundles/tiles/objects/house.png b/tests/rsrc/bundles/tiles/objects/house.png new file mode 100644 index 00000000..6b96766c Binary files /dev/null and b/tests/rsrc/bundles/tiles/objects/house.png differ diff --git a/tests/rsrc/bundles/tiles/objects/mooring.png b/tests/rsrc/bundles/tiles/objects/mooring.png new file mode 100644 index 00000000..e11d98b9 Binary files /dev/null and b/tests/rsrc/bundles/tiles/objects/mooring.png differ diff --git a/tests/rsrc/bundles/tiles/objects/tree_oak.png b/tests/rsrc/bundles/tiles/objects/tree_oak.png new file mode 100644 index 00000000..eaf1d931 Binary files /dev/null and b/tests/rsrc/bundles/tiles/objects/tree_oak.png differ diff --git a/tests/rsrc/bundles/tiles/objects/tree_palm.png b/tests/rsrc/bundles/tiles/objects/tree_palm.png new file mode 100644 index 00000000..3c143b0c Binary files /dev/null and b/tests/rsrc/bundles/tiles/objects/tree_palm.png differ diff --git a/tests/rsrc/bundles/tiles/objects/well.png b/tests/rsrc/bundles/tiles/objects/well.png new file mode 100644 index 00000000..bb35cb9d Binary files /dev/null and b/tests/rsrc/bundles/tiles/objects/well.png differ diff --git a/tests/rsrc/config/media/iconmgr.properties b/tests/rsrc/config/media/iconmgr.properties new file mode 100644 index 00000000..e8d7b3ee --- /dev/null +++ b/tests/rsrc/config/media/iconmgr.properties @@ -0,0 +1,7 @@ +# +# $Id: iconmgr.properties 924 2002-02-03 22:43:54Z mdb $ +# +# Test properties for the icon manager + +test.path = /media/miso/tiles/node-icons.png +test.metrics = 8, 16, 16 diff --git a/tests/rsrc/config/miso/components.xml b/tests/rsrc/config/miso/components.xml new file mode 100644 index 00000000..bcf1393b --- /dev/null +++ b/tests/rsrc/config/miso/components.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/rsrc/config/miso/scrolling.properties b/tests/rsrc/config/miso/scrolling.properties new file mode 100644 index 00000000..34dddb02 --- /dev/null +++ b/tests/rsrc/config/miso/scrolling.properties @@ -0,0 +1,59 @@ +# +# $Id: scrolling.properties 1014 2002-02-17 23:48:38Z mdb $ +# +# Initial test config values for miso development. +# + +# the tile set repository class that handles tile set storage +tilesetrepo = com.threerings.miso.tile.XMLTileSetRepository + +# the scene repository class that handles scene storage +scenerepo = com.threerings.miso.scene.xml.XMLSceneRepository + +# the directory containing scene description files +sceneroot = rsrc/scenes + +# the tileset descriptions +tilesets = rsrc/config/miso/tilesets.xml + +# the component descriptions +components = rsrc/config/miso/components.xml + +# the list of building types +buildings = Inn, Hall, Bank, Agent, Palace + +# the tileset id for the building icon images +buildings_icon_tsid = 1007 + +# ---------------------------------- +# begin isometric view configuration + +# tile dimensions in pixels +tile_width = 64 +tile_height = 48 + +# fine coordinate system granularity in intra-tile count +fine_granularity = 4 + +# scene dimensions in tile counts +scene_width = 14 +scene_height = 14 + +# scene viewport dimensions in tile counts +scene_view_width = 5 +scene_view_height = 6 + +# vertical offset for scene display origin in tile count +scene_offset_y = -4 + +# whether to show tile coordinates +show_coords = false + +# whether to show locations +show_locs = false + +# whether to show sprite paths +show_paths = false + +# end isometric view configuration +# ---------------------------------- diff --git a/tests/rsrc/config/miso/tile/.cvsignore b/tests/rsrc/config/miso/tile/.cvsignore new file mode 100644 index 00000000..aa4f5775 --- /dev/null +++ b/tests/rsrc/config/miso/tile/.cvsignore @@ -0,0 +1 @@ +fringeconf.dat diff --git a/tests/rsrc/config/miso/tile/fringeconf.xml b/tests/rsrc/config/miso/tile/fringeconf.xml new file mode 100644 index 00000000..46ae23a1 --- /dev/null +++ b/tests/rsrc/config/miso/tile/fringeconf.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/rsrc/config/miso/tilesets-components.xml b/tests/rsrc/config/miso/tilesets-components.xml new file mode 100644 index 00000000..41b11b39 --- /dev/null +++ b/tests/rsrc/config/miso/tilesets-components.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + 94, 94, 94, 94, 94, 94, 94, 94 + 94, 94, 94, 94, 94, 94, 94, 94 + 1, 1, 1, 1, 1, 1, 1, 1 + 1, 1 + 39, 18 + + + + + 94, 94, 94, 94, 94, 94, 94, 94 + 94, 94, 94, 94, 94, 94, 94, 94 + 6, 6, 6, 6, 6, 6, 6, 6 + 52, 51 + 39, 18 + + + + + diff --git a/tests/rsrc/config/miso/tilesets.xml b/tests/rsrc/config/miso/tilesets.xml new file mode 100644 index 00000000..be631571 --- /dev/null +++ b/tests/rsrc/config/miso/tilesets.xml @@ -0,0 +1,78 @@ + + + + + + + + + + media/miso/tiles/sample.png + 48 + 64 + 14 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 + + + + media/miso/tiles/node-icons.png + 16 + 16 + 8 + + + + media/miso/tiles/editor-icons.png + 22 + 20 + 5 + + + + media/miso/tiles/building.png + 293 + 224 + 4 + 2, 2 + 4, 0 + + + + + + + + + + + + media/miso/tiles/landscape.png + 48 + 64 + 5 + 1, 0, 1, 1, 1 + + + + media/miso/tiles/faux-building.png + 265 + 224 + 4 + + + + + + + + + + + + media/miso/tiles/fringe.png + 48, 48, 48, 48, 48 + 64, 64, 64, 64, 64 + 8, 8, 8, 8, 8 + + + diff --git a/tests/rsrc/config/miso/viewer.properties b/tests/rsrc/config/miso/viewer.properties new file mode 100644 index 00000000..8ee7ee79 --- /dev/null +++ b/tests/rsrc/config/miso/viewer.properties @@ -0,0 +1,7 @@ +# +# $Id: viewer.properties 604 2001-11-08 02:58:24Z mdb $ +# +# Miso scene viewer application config values. +# + +default_scene = scenes/default.xml diff --git a/tests/rsrc/config/resource/editor.properties b/tests/rsrc/config/resource/editor.properties new file mode 100644 index 00000000..55eaa263 --- /dev/null +++ b/tests/rsrc/config/resource/editor.properties @@ -0,0 +1,16 @@ +# +# $Id: manager.properties 2121 2003-01-13 22:55:28Z mdb $ +# +# Test resource manager configuration + +# configure the BundledTileSetRepository +resource.set.tilesets = bundles/tiles/ground/bundle.jar:\ + bundles/tiles/objects/bundle.jar + +# configure the BundledComponentRepository +resource.set.components = bundles/components/metadata.jar:\ + bundles/components/pirate/components.jar: \ + bundles/components/vessel/components.jar + +resource.set.general = \ + bundles/components/pirate/components.jar diff --git a/tests/rsrc/config/resource/manager.properties b/tests/rsrc/config/resource/manager.properties new file mode 100644 index 00000000..55eaa263 --- /dev/null +++ b/tests/rsrc/config/resource/manager.properties @@ -0,0 +1,16 @@ +# +# $Id: manager.properties 2121 2003-01-13 22:55:28Z mdb $ +# +# Test resource manager configuration + +# configure the BundledTileSetRepository +resource.set.tilesets = bundles/tiles/ground/bundle.jar:\ + bundles/tiles/objects/bundle.jar + +# configure the BundledComponentRepository +resource.set.components = bundles/components/metadata.jar:\ + bundles/components/pirate/components.jar: \ + bundles/components/vessel/components.jar + +resource.set.general = \ + bundles/components/pirate/components.jar diff --git a/tests/rsrc/media/miso/tiles/building.png b/tests/rsrc/media/miso/tiles/building.png new file mode 100644 index 00000000..16fca481 Binary files /dev/null and b/tests/rsrc/media/miso/tiles/building.png differ diff --git a/tests/rsrc/media/miso/tiles/components/standing_hat_tri.png b/tests/rsrc/media/miso/tiles/components/standing_hat_tri.png new file mode 100644 index 00000000..5257e88a Binary files /dev/null and b/tests/rsrc/media/miso/tiles/components/standing_hat_tri.png differ diff --git a/tests/rsrc/media/miso/tiles/components/standing_torso.png b/tests/rsrc/media/miso/tiles/components/standing_torso.png new file mode 100644 index 00000000..e3d4801f Binary files /dev/null and b/tests/rsrc/media/miso/tiles/components/standing_torso.png differ diff --git a/tests/rsrc/media/miso/tiles/components/standing_torso_red.png b/tests/rsrc/media/miso/tiles/components/standing_torso_red.png new file mode 100644 index 00000000..0eff5f76 Binary files /dev/null and b/tests/rsrc/media/miso/tiles/components/standing_torso_red.png differ diff --git a/tests/rsrc/media/miso/tiles/components/walking_hat_tri.png b/tests/rsrc/media/miso/tiles/components/walking_hat_tri.png new file mode 100644 index 00000000..443cc8c1 Binary files /dev/null and b/tests/rsrc/media/miso/tiles/components/walking_hat_tri.png differ diff --git a/tests/rsrc/media/miso/tiles/components/walking_torso.png b/tests/rsrc/media/miso/tiles/components/walking_torso.png new file mode 100644 index 00000000..425124c9 Binary files /dev/null and b/tests/rsrc/media/miso/tiles/components/walking_torso.png differ diff --git a/tests/rsrc/media/miso/tiles/components/walking_torso_red.png b/tests/rsrc/media/miso/tiles/components/walking_torso_red.png new file mode 100644 index 00000000..2878ad73 Binary files /dev/null and b/tests/rsrc/media/miso/tiles/components/walking_torso_red.png differ diff --git a/tests/rsrc/media/miso/tiles/faux-building.png b/tests/rsrc/media/miso/tiles/faux-building.png new file mode 100644 index 00000000..1fd3c29b Binary files /dev/null and b/tests/rsrc/media/miso/tiles/faux-building.png differ diff --git a/tests/rsrc/media/miso/tiles/landscape.png b/tests/rsrc/media/miso/tiles/landscape.png new file mode 100644 index 00000000..30e499ae Binary files /dev/null and b/tests/rsrc/media/miso/tiles/landscape.png differ diff --git a/tests/rsrc/media/miso/tiles/node-icons.png b/tests/rsrc/media/miso/tiles/node-icons.png new file mode 100644 index 00000000..e267fd01 Binary files /dev/null and b/tests/rsrc/media/miso/tiles/node-icons.png differ diff --git a/tests/rsrc/media/miso/tiles/sample.png b/tests/rsrc/media/miso/tiles/sample.png new file mode 100644 index 00000000..d971bef0 Binary files /dev/null and b/tests/rsrc/media/miso/tiles/sample.png differ diff --git a/tests/rsrc/media/test.wav b/tests/rsrc/media/test.wav new file mode 100644 index 00000000..c33acae8 Binary files /dev/null and b/tests/rsrc/media/test.wav differ diff --git a/tests/rsrc/media/tile/bundle/tools/.cvsignore b/tests/rsrc/media/tile/bundle/tools/.cvsignore new file mode 100644 index 00000000..edfd8732 --- /dev/null +++ b/tests/rsrc/media/tile/bundle/tools/.cvsignore @@ -0,0 +1 @@ +bundle.jar diff --git a/tests/rsrc/media/tile/bundle/tools/building.png b/tests/rsrc/media/tile/bundle/tools/building.png new file mode 100644 index 00000000..16fca481 Binary files /dev/null and b/tests/rsrc/media/tile/bundle/tools/building.png differ diff --git a/tests/rsrc/media/tile/bundle/tools/bundle.xml b/tests/rsrc/media/tile/bundle/tools/bundle.xml new file mode 100644 index 00000000..f84ffdbc --- /dev/null +++ b/tests/rsrc/media/tile/bundle/tools/bundle.xml @@ -0,0 +1,35 @@ + + + + + + + node-icons.png + + 16 + 8 + + + + + + fringe.png + 48, 48, 48, 48, 48 + 64, 64, 64, 64, 64 + 8, 8, 8, 8, 8 + + + + + + building.png + 293 + 224 + 4 + 2, 2 + 4, 0 + 4, 3, 4, 3 + 3, 4, 3, 4 + + + diff --git a/tests/rsrc/media/tile/bundle/tools/bundler-config.xml b/tests/rsrc/media/tile/bundle/tools/bundler-config.xml new file mode 100644 index 00000000..3edd05d9 --- /dev/null +++ b/tests/rsrc/media/tile/bundle/tools/bundler-config.xml @@ -0,0 +1,19 @@ + + + + + + bundle/uniform + com.threerings.media.tools.tile.xml.UniformTileSetRuleSet + + + bundle/swissarmy + + com.threerings.media.tools.tile.xml.SwissArmyTileSetRuleSet + + + + bundle/object + com.threerings.media.tools.tile.xml.ObjectTileSetRuleSet + + diff --git a/tests/rsrc/media/tile/bundle/tools/fringe.png b/tests/rsrc/media/tile/bundle/tools/fringe.png new file mode 100644 index 00000000..390f4561 Binary files /dev/null and b/tests/rsrc/media/tile/bundle/tools/fringe.png differ diff --git a/tests/rsrc/media/tile/bundle/tools/node-icons.png b/tests/rsrc/media/tile/bundle/tools/node-icons.png new file mode 100644 index 00000000..e267fd01 Binary files /dev/null and b/tests/rsrc/media/tile/bundle/tools/node-icons.png differ diff --git a/tests/rsrc/media/tile/tools/xml/tilesets.xml b/tests/rsrc/media/tile/tools/xml/tilesets.xml new file mode 100644 index 00000000..f75bb667 --- /dev/null +++ b/tests/rsrc/media/tile/tools/xml/tilesets.xml @@ -0,0 +1,37 @@ + + + + + + + node-icons.png + 16 + 16 + 8 + + + + + + fringe.png + 48, 48, 48, 48, 48 + 64, 64, 64, 64, 64 + 8, 8, 8, 8, 8 + 5, 5 + 10, 10 + + + + + + building.png + 293 + 224 + 4 + 2, 2 + 4, 0 + 4, 3, 4, 3 + 3, 4, 3, 4 + + + diff --git a/tests/rsrc/scenes/idyll.xml b/tests/rsrc/scenes/idyll.xml new file mode 100644 index 00000000..2c5abae1 --- /dev/null +++ b/tests/rsrc/scenes/idyll.xml @@ -0,0 +1,19 @@ + + + + 22 + 22 + 10 + 12 + 131072, 131072, 131072, 131072, 131073, 131073, 131073, 131073, 131074, 65537, 65537, 131072, 131074, 131074, 131074, 131074, 131072, 131073, 131074, 131074, 131072, 131074, 131074, 131072, 131072, 131072, 131073, 131073, 131073, 131074, 131072, 65536, 65538, 131072, 131072, 131074, 131072, 131072, 131073, 131074, 131072, 131072, 131072, 131072, 131072, 131074, 131072, 131074, 131072, 131073, 131074, 131073, 131074, 65537, 65536, 131072, 131073, 131072, 131073, 131072, 131074, 131073, 131073, 131073, 131072, 131074, 131073, 131072, 131073, 131074, 131074, 131072, 131074, 131072, 131073, 65536, 65538, 131074, 131074, 131074, 131073, 131073, 131072, 131072, 131073, 131072, 131072, 131072, 131074, 131072, 131074, 131072, 131074, 131074, 131073, 131074, 131074, 65536, 65536, 131073, 131072, 131072, 131074, 131074, 131074, 131073, 131072, 131072, 131074, 131073, 131074, 131073, 131074, 131072, 131073, 131072, 131073, 131073, 131074, 65537, 65538, 131072, 131072, 131072, 131072, 131073, 131072, 131072, 131073, 131074, 131074, 131074, 131073, 131074, 131072, 131073, 131074, 131073, 131072, 131072, 131072, 65536, 65537, 131074, 131073, 131074, 131072, 131074, 131073, 131074, 131073, 131072, 131073, 131072, 131073, 131074, 131074, 131074, 131072, 131074, 131074, 131074, 131072, 65538, 65538, 131074, 131074, 131073, 131073, 131074, 131073, 131073, 131074, 131074, 131072, 131072, 131072, 131073, 131073, 131072, 131072, 131073, 131073, 131074, 131073, 65536, 65537, 65538, 65536, 65537, 65536, 65537, 65537, 65538, 65538, 65536, 131074, 131073, 131073, 131072, 131073, 131074, 131074, 131074, 131073, 131074, 131072, 65536, 65537, 65538, 65536, 65537, 65536, 65538, 65536, 65538, 65537, 65536, 131073, 131073, 131074, 131072, 131074, 131073, 131072, 131074, 131072, 131073, 131072, 65536, 65538, 65536, 65538, 65537, 65538, 65537, 65538, 65536, 65536, 65538, 65538, 131074, 131072, 131074, 131073, 131074, 131074, 131074, 131073, 131074, 131072, 65538, 131073, 131074, 131074, 131072, 131074, 131072, 131074, 131074, 131074, 131072, 131074, 131073, 131073, 131072, 131073, 131072, 131072, 131073, 131073, 131074, 131072, 65537, 131074, 131073, 131073, 131072, 131073, 131072, 131074, 131074, 131073, 131072, 131073, 131072, 131072, 131073, 131074, 131073, 131073, 131074, 65536, 65538, 65538, 65538, 131074, 131073, 131073, 131074, 131073, 131073, 131072, 131073, 131074, 131074, 131072, 131074, 131074, 131073, 131072, 131072, 131074, 131072, 65536, 65538, 65538, 65538, 131073, 131074, 131073, 131074, 131073, 131072, 131073, 131074, 131074, 131073, 131073, 131074, 131072, 131074, 131072, 131073, 131073, 131074, 65536, 65536, 65537, 65537, 131073, 131074, 131073, 131074, 131073, 131072, 131074, 131074, 131073, 131073, 131074, 131074, 131072, 131073, 131073, 131074, 131073, 131074, 65536, 65536, 65537, 65538, 131072, 131073, 131074, 131073, 131073, 131074, 131072, 131074, 131073, 131072, 131074, 131072, 131073, 131073, 131072, 131073, 131073, 131072, 131072, 131072, 65537, 65538, 131074, 131072, 131072, 131072, 131073, 131072, 131072, 131074, 131073, 131074, 131073, 131072, 131073, 131072, 131072, 131072, 131072, 131072, 131073, 131072, 65537, 65536, 131073, 131073, 131074, 131073, 131074, 131074, 131073, 131073, 131072, 131074, 131073, 131074, 131072, 131073, 131073, 131073, 131073, 131072, 131072, 131072, 65537, 65537, 131074, 131074, 131074, 131072, 131073, 131074, 131072, 131074, 131072, 131072, 131074, 131072, 131074, 131074, 131073, 131073, 131073, 131072, 131072, 131074, 131074, 131073, 131073, 131073, 131072, 131072, 131072, 131074, 131074, 131072, 131072, 131074, 131074, 131072, 131074, 131072, 131073, 131074, 131073, 131073, 131072, 131074, 131072, 131073, 131072, 131073, 131073, 131073, 131074, 131074, 131073, 131074, 131073, 131074, 131072, 131074 + + + + + + + + + + + diff --git a/tests/rsrc/scenes/wooded.xml b/tests/rsrc/scenes/wooded.xml new file mode 100644 index 00000000..479f33d3 --- /dev/null +++ b/tests/rsrc/scenes/wooded.xml @@ -0,0 +1,31 @@ + + + + 22 + 22 + 10 + 12 + 131072, 131073, 131073, 131073, 131074, 131072, 131072, 131072, 131073, 131074, 131074, 131072, 131074, 131074, 131072, 131072, 131073, 131073, 131072, 131073, 131073, 131072, 131074, 131072, 131074, 131074, 131072, 131073, 131074, 131072, 131074, 131074, 131074, 131072, 131074, 131074, 131072, 131074, 131072, 131072, 131073, 131074, 131074, 131072, 131074, 131072, 131072, 131072, 131072, 131074, 131073, 131073, 131074, 131073, 131074, 131073, 131074, 131073, 131074, 131074, 131072, 131072, 131073, 131074, 131074, 131073, 131073, 131073, 131073, 131073, 131072, 131073, 131072, 131072, 131072, 131072, 131072, 131074, 131074, 131073, 131074, 131072, 131074, 131074, 131073, 131074, 131073, 131073, 131072, 131073, 131073, 131072, 131074, 131073, 131073, 131074, 131072, 131072, 131073, 131072, 131072, 131073, 131072, 131072, 131074, 131072, 131072, 131074, 131073, 131072, 131072, 131072, 131074, 131074, 131073, 131073, 131073, 131073, 131074, 131073, 131074, 131072, 131074, 131073, 131073, 131072, 131074, 131073, 131072, 131074, 131073, 131072, 131074, 131074, 131074, 131072, 131074, 131073, 131074, 131072, 131073, 131072, 131072, 131072, 131073, 131074, 131073, 131073, 131072, 131072, 131073, 131072, 131072, 131074, 131074, 131074, 131072, 131074, 131074, 131074, 131074, 131074, 131074, 131072, 131072, 131073, 131074, 131073, 131073, 131073, 131072, 131073, 131073, 131073, 131073, 131074, 131073, 131074, 131072, 131072, 131073, 131073, 131072, 131073, 131074, 131074, 131074, 131074, 131072, 131073, 131074, 131072, 131072, 131072, 131074, 131072, 131073, 131073, 131074, 131072, 131072, 131073, 131073, 131072, 131074, 131072, 131073, 131073, 131073, 131073, 131074, 131073, 131072, 131073, 131072, 131073, 131072, 131073, 131072, 131072, 131074, 131074, 131072, 131073, 131074, 131074, 131073, 131074, 131074, 131072, 131073, 131072, 131072, 131072, 131073, 131073, 131072, 131073, 131072, 131074, 131074, 131074, 131073, 131073, 131073, 131073, 131073, 131073, 131073, 131073, 131074, 131073, 131072, 131072, 131074, 131072, 131074, 131073, 131073, 131074, 131074, 131074, 131074, 131072, 131073, 131073, 131074, 131072, 131072, 131073, 131072, 131074, 131073, 131074, 131072, 131072, 131073, 131073, 131074, 131072, 131073, 131074, 131072, 131073, 131074, 131073, 131072, 131072, 131074, 131072, 131073, 131073, 131072, 131074, 131073, 131072, 131074, 131072, 131074, 131072, 131072, 131074, 131073, 131074, 131074, 131073, 131072, 131074, 131072, 131074, 131073, 131073, 131073, 131073, 131072, 131074, 131073, 131073, 131074, 131073, 131073, 131074, 131074, 131074, 131072, 131073, 131072, 131074, 131072, 131074, 131072, 131072, 131073, 131074, 131073, 131072, 131074, 131073, 131073, 131073, 131074, 131073, 131073, 131072, 131074, 131072, 131072, 131072, 131073, 131074, 131074, 131073, 131072, 131074, 131073, 131074, 131074, 131073, 131074, 131073, 131073, 131072, 131072, 131074, 131073, 131072, 131073, 131074, 131073, 131074, 131073, 131072, 131073, 131073, 131072, 131074, 131072, 131074, 131074, 131072, 131074, 131074, 131072, 131072, 131072, 131073, 131072, 131073, 131072, 131073, 131073, 131074, 131074, 131073, 131074, 131074, 131073, 131074, 131072, 131073, 131072, 131074, 131073, 131073, 131073, 131073, 131073, 131073, 131072, 131073, 131073, 131074, 131073, 131072, 131072, 131074, 131072, 131073, 131072, 131074, 131073, 131073, 131073, 131073, 131073, 131072, 131072, 131072, 131072, 131074, 131072, 131073, 131073, 131073, 131074, 131074, 131072, 131072, 131074, 131073, 131073, 131073, 131072, 131072, 131074, 131073, 131074, 131073, 131074, 131073, 131073, 131073, 131074, 131073, 131074, 131072, 131073, 131072, 131073, 131073, 131074, 131072, 131073, 131072, 131072, 131072, 131074, 131073, 131072, 131072, 131074, 131073, 131072, 131072, 131073, 131073, 131074, 131072, 131074, 131072, 131072, 131074, 131074, 131074 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/src/java/com/threerings/cast/CharSpriteViz.java b/tests/src/java/com/threerings/cast/CharSpriteViz.java new file mode 100644 index 00000000..a560ef54 --- /dev/null +++ b/tests/src/java/com/threerings/cast/CharSpriteViz.java @@ -0,0 +1,153 @@ +// +// $Id: CharSpriteViz.java 3355 2005-02-17 01:54:54Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Graphics; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.media.image.ImageManager; +import com.threerings.resource.ResourceManager; +import com.threerings.util.DirectionCodes; +import com.threerings.util.DirectionUtil; + +import com.threerings.cast.bundle.BundledComponentRepository; + +/** + * Displays a character sprite in all of the various orientations. + */ +public class CharSpriteViz extends JPanel + implements DirectionCodes, MouseMotionListener +{ + public CharSpriteViz ( + CharacterManager charmgr, CharacterComponent ccomp, String action) + { + // get a handle on our sprite + _sprite = charmgr.getCharacter( + new CharacterDescriptor( + new int[] { ccomp.componentId }, null)); + + // put the sprite in the appropriate action mode + _sprite.setRestingAction(action); + _sprite.setFollowingPathAction(action); + _sprite.setActionSequence(action); + + addMouseMotionListener(this); + } + + public void mouseDragged (MouseEvent event) + { + } + + public void mouseMoved (MouseEvent event) + { + int orient = DirectionUtil.getFineDirection( + getWidth()/2, getHeight()/2, event.getX(), event.getY()); + if (_orient != orient) { + System.out.println("Switching to " + + DirectionUtil.toShortString(orient) + "."); + _orient = orient; + repaint(); + } + } + + public void paintComponent (Graphics g) + { + super.paintComponent(g); + + int width = getWidth(), height = getHeight(); + int cx = width/2, cy = height/2; + g.setColor(Color.darkGray); + g.drawLine(cx, cy, 0, cy); + g.drawLine(cx, cy, 0, cy/2); + g.drawLine(cx, cy, 0, 0); + g.drawLine(cx, cy, cx/2, 0); + g.drawLine(cx, cy, cx, 0); + g.drawLine(cx, cy, 3*width/4, 0); + g.drawLine(cx, cy, width, 0); + g.drawLine(cx, cy, width, cy/2); + g.drawLine(cx, cy, width, cy); + g.drawLine(cx, cy, width, 3*height/4); + g.drawLine(cx, cy, width, height); + g.drawLine(cx, cy, 3*width/4, height); + g.drawLine(cx, cy, cx, height); + g.drawLine(cx, cy, cx/2, height); + g.drawLine(cx, cy, 0, height); + g.drawLine(cx, cy, 0, 3*height/4); + + _sprite.setLocation(cx, cy); + _sprite.setOrientation(_orient); + _sprite.paint((Graphics2D)g); + } + + public static void main (String[] args) + { + if (args.length < 3) { + System.err.println("Usage: CharSpriteViz cclass cname action"); + System.err.println(" (eg. CharSpriteViz navsail smsloop sailing"); + System.exit(-1); + } + + JFrame frame = new JFrame("CharSpriteViz"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + try { + ResourceManager rmgr = new ResourceManager("rsrc"); + rmgr.initBundles( + null, "config/resource/manager.properties", null); + ImageManager imgr = new ImageManager(rmgr, frame); + ComponentRepository crepo = + new BundledComponentRepository(rmgr, imgr, "components"); + CharacterManager charmgr = new CharacterManager(imgr, crepo); + CharacterComponent ccomp = crepo.getComponent(args[0], args[1]); + + frame.getContentPane().add( + new CharSpriteViz(charmgr, ccomp, args[2]), + BorderLayout.CENTER); + frame.setSize(200, 200); + SwingUtil.centerWindow(frame); + frame.show(); + + } catch (NoSuchComponentException nsce) { + System.err.println("No component with specified class " + + "and name [cclass=" + args[0] + + ", cname=" + args[1] + "]."); + System.exit(-1); + + } catch (Exception e) { + e.printStackTrace(System.err); + System.exit(-1); + } + } + + protected CharacterSprite _sprite; + protected int _orient = NORTH; +} diff --git a/tests/src/java/com/threerings/cast/builder/TestApp.java b/tests/src/java/com/threerings/cast/builder/TestApp.java new file mode 100644 index 00000000..bfaa5c10 --- /dev/null +++ b/tests/src/java/com/threerings/cast/builder/TestApp.java @@ -0,0 +1,76 @@ +// +// $Id: TestApp.java 3355 2005-02-17 01:54:54Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.builder; + +import java.io.IOException; +import javax.swing.JFrame; + +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.media.image.ImageManager; +import com.threerings.resource.ResourceManager; + +import com.threerings.cast.CharacterManager; +import com.threerings.cast.ComponentRepository; +import com.threerings.cast.bundle.BundledComponentRepository; + +public class TestApp +{ + public TestApp (String[] args) + throws IOException + { + _frame = new TestFrame(); + _frame.setSize(800, 600); + SwingUtil.centerWindow(_frame); + + ResourceManager rmgr = new ResourceManager("rsrc"); + rmgr.initBundles( + null, "config/resource/manager.properties", null); + ImageManager imgr = new ImageManager(rmgr, _frame); + + ComponentRepository crepo = + new BundledComponentRepository(rmgr, imgr, "components"); + CharacterManager charmgr = new CharacterManager(imgr, crepo); + + // initialize the frame + ((TestFrame)_frame).init(charmgr, crepo); + } + + public void run () + { + _frame.pack(); + _frame.show(); + } + + public static void main (String[] args) + { + try { + TestApp app = new TestApp(args); + app.run(); + } catch (IOException ioe) { + System.err.println("Error initializing app."); + ioe.printStackTrace(); + } + } + + protected JFrame _frame; +} diff --git a/tests/src/java/com/threerings/cast/builder/TestFrame.java b/tests/src/java/com/threerings/cast/builder/TestFrame.java new file mode 100644 index 00000000..734eafa4 --- /dev/null +++ b/tests/src/java/com/threerings/cast/builder/TestFrame.java @@ -0,0 +1,43 @@ +// +// $Id: TestFrame.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.builder; + +import javax.swing.JFrame; + +import com.threerings.cast.CharacterManager; +import com.threerings.cast.ComponentRepository; + +public class TestFrame extends JFrame +{ + public TestFrame () + { + super("Character Builder"); + + setResizable(false); + setDefaultCloseOperation(EXIT_ON_CLOSE); + } + + public void init (CharacterManager charmgr, ComponentRepository crepo) + { + getContentPane().add(new BuilderPanel(charmgr, crepo, "male/")); + } +} diff --git a/tests/src/java/com/threerings/cast/bundle/BundledComponentRepositoryTest.java b/tests/src/java/com/threerings/cast/bundle/BundledComponentRepositoryTest.java new file mode 100644 index 00000000..599f9244 --- /dev/null +++ b/tests/src/java/com/threerings/cast/bundle/BundledComponentRepositoryTest.java @@ -0,0 +1,85 @@ +// +// $Id: BundledComponentRepositoryTest.java 3720 2005-10-05 01:39:45Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.cast.bundle; + +import java.awt.Component; +import java.util.Iterator; + +import com.threerings.cast.ComponentClass; +import com.threerings.media.image.ImageManager; +import com.threerings.resource.ResourceManager; + +import junit.framework.Test; +import junit.framework.TestCase; + +public class BundledComponentRepositoryTest extends TestCase +{ + public BundledComponentRepositoryTest () + { + super(BundledComponentRepositoryTest.class.getName()); + } + + public void runTest () + { + try { + ResourceManager rmgr = new ResourceManager("rsrc"); + rmgr.initBundles( + null, "config/resource/manager.properties", null); + ImageManager imgr = new ImageManager(rmgr, (Component)null); + BundledComponentRepository repo = + new BundledComponentRepository(rmgr, imgr, "components"); + +// System.out.println("Classes: " + StringUtil.toString( +// repo.enumerateComponentClasses())); + +// System.out.println("Actions: " + StringUtil.toString( +// repo.enumerateActionSequences())); + +// System.out.println("Action sets: " + StringUtil.toString( +// repo._actionSets.values().iterator())); + + Iterator iter = repo.enumerateComponentClasses(); + while (iter.hasNext()) { + ComponentClass cclass = (ComponentClass)iter.next(); +// System.out.println("IDs [" + cclass + "]: " + +// StringUtil.toString( +// repo.enumerateComponentIds(cclass))); + } + + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + public static void main (String[] args) + { + BundledComponentRepositoryTest test = + new BundledComponentRepositoryTest(); + test.runTest(); + } + + public static Test suite () + { + return new BundledComponentRepositoryTest(); + } +} diff --git a/tests/src/java/com/threerings/geom/NearestViz.java b/tests/src/java/com/threerings/geom/NearestViz.java new file mode 100644 index 00000000..51c58c8b --- /dev/null +++ b/tests/src/java/com/threerings/geom/NearestViz.java @@ -0,0 +1,101 @@ +// +// $Id: NearestViz.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.geom; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Point; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import com.samskivert.swing.util.SwingUtil; + +/** + * Renders the nearest point on a line just for kicks. + */ +public class NearestViz extends JPanel + implements MouseMotionListener +{ + public NearestViz () + { + addMouseMotionListener(this); + } + + public void doLayout () + { + super.doLayout(); + _center = new Point(getWidth() / 2, getHeight() / 2); + } + + public void paintComponent (Graphics g) + { + super.paintComponent(g); + + int dx = (int)Math.round(Math.cos(_theta) * 200); + int dy = (int)Math.round(Math.sin(_theta) * 200); + + g.setColor(Color.blue); + g.drawLine(_center.x, _center.y, _center.x + dx, _center.y - dy); + g.drawLine(_center.x, _center.y, _center.x - dx, _center.y + dy); + + // locate the point nearest the spot + Point p2 = new Point(_center.x + dx, _center.y - dy); + Point n = new Point(); + GeomUtil.nearestToLine(_center, p2, _spot, n); + + g.setColor(Color.red); + g.drawOval(n.x - 4, n.y - 4, 8, 8); + + // kick theta along for the next tick because it's fun + _theta += (float)(Math.PI/100); + } + + public void mouseDragged (MouseEvent event) + { + } + + public void mouseMoved (MouseEvent event) + { + _spot.x = event.getX(); + _spot.y = event.getY(); + repaint(); + } + + public static void main (String[] args) + { + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.getContentPane().add(new NearestViz(), BorderLayout.CENTER); + frame.setSize(300, 300); + SwingUtil.centerWindow(frame); + frame.show(); + } + + protected float _theta = (float)(3 * Math.PI / 5); + protected Point _center; + protected Point _spot = new Point(); +} diff --git a/tests/src/java/com/threerings/geom/WhichSideViz.java b/tests/src/java/com/threerings/geom/WhichSideViz.java new file mode 100644 index 00000000..6914b9e8 --- /dev/null +++ b/tests/src/java/com/threerings/geom/WhichSideViz.java @@ -0,0 +1,113 @@ +// +// $Id: WhichSideViz.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.geom; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Point; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import com.samskivert.swing.util.SwingUtil; + +/** + * Renders the nearest point on a line just for kicks. + */ +public class WhichSideViz extends JPanel + implements MouseMotionListener +{ + public WhichSideViz () + { + addMouseMotionListener(this); + } + + public void doLayout () + { + super.doLayout(); + _center = new Point(getWidth() / 2, getHeight() / 2); + } + + public void paintComponent (Graphics g) + { + super.paintComponent(g); + + int dx = (int)Math.round(Math.cos(_theta) * 200); + int dy = (int)Math.round(Math.sin(_theta) * 200); + + g.setColor(Color.blue); + g.drawLine(_center.x, _center.y, _center.x + dx, _center.y + dy); + g.setColor(Color.white); + g.drawLine(_center.x, _center.y, _center.x - dx, _center.y - dy); + + int nx = _center.x + (int)Math.round(1000*Math.cos(_theta + Math.PI/2)), + ny = _center.y + (int)Math.round(1000*Math.sin(_theta + Math.PI/2)); + g.setColor(Color.pink); + g.drawLine(_center.x, _center.y, nx, ny); + + // figure out which side the line is on + String str; + int side = GeomUtil.whichSide(_center, _theta, _spot); + if (side > 0) { + str = "R"; + } else if (side < 0) { + str = "L"; + } else { + str = "O"; + } + + g.setColor(Color.black); + g.drawString(str, _spot.x, _spot.y); + + // kick theta along for the next tick because it's fun + _theta += (float)(Math.PI/100); + } + + public void mouseDragged (MouseEvent event) + { + } + + public void mouseMoved (MouseEvent event) + { + _spot.x = event.getX(); + _spot.y = event.getY(); + repaint(); + } + + public static void main (String[] args) + { + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.getContentPane().add(new WhichSideViz(), BorderLayout.CENTER); + frame.setSize(300, 300); + SwingUtil.centerWindow(frame); + frame.show(); + } + + protected float _theta = (float)(3 * Math.PI / 5); + protected Point _center; + protected Point _spot = new Point(); +} diff --git a/tests/src/java/com/threerings/jme/JmeCanvasTest.java b/tests/src/java/com/threerings/jme/JmeCanvasTest.java new file mode 100644 index 00000000..089778dd --- /dev/null +++ b/tests/src/java/com/threerings/jme/JmeCanvasTest.java @@ -0,0 +1,62 @@ +// +// $Id$ + +package com.threerings.jme; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.JButton; +import javax.swing.JFrame; + +import com.jme.bounding.BoundingBox; +import com.jme.math.Vector3f; +import com.jme.scene.shape.Box; + +/** + * Tests the JME/AWT integration bits. + */ +public class JmeCanvasTest extends JmeCanvasApp +{ + public static void main (String[] args) + { + final JmeCanvasTest app = new JmeCanvasTest(); + JFrame frame = new JFrame("JmeCanvasTest"); + frame.getContentPane().add(app.getCanvas(), BorderLayout.CENTER); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setSize(800, 600); + frame.setVisible(true); + + JButton button = new JButton("Create box"); + frame.getContentPane().add(button, BorderLayout.SOUTH); + button.addActionListener(new ActionListener() { + public void actionPerformed (ActionEvent event) { + app.addBox(); + } + }); + app.run(); + } + + public void addBox () + { + Vector3f max = new Vector3f(5, 5, 5); + Vector3f min = new Vector3f(-5, -5, -5); + + Box box = new Box("Box", min, max); + box.setModelBound(new BoundingBox()); + box.updateModelBound(); + box.setLocalTranslation(new Vector3f(0, 0, -10)); + _geom.attachChild(box); + _geom.updateRenderState(); + } + + protected JmeCanvasTest () + { + super(800, 600); + } + + protected void initRoot () + { + super.initRoot(); + } +} diff --git a/tests/src/java/com/threerings/jme/client/JabberApp.java b/tests/src/java/com/threerings/jme/client/JabberApp.java new file mode 100644 index 00000000..523ea4db --- /dev/null +++ b/tests/src/java/com/threerings/jme/client/JabberApp.java @@ -0,0 +1,160 @@ +// +// $Id: JabberApp.java 3098 2004-08-27 02:12:55Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.client; + +import java.io.InputStream; +import java.io.InputStreamReader; + +import com.jme.bounding.BoundingBox; +import com.jme.math.FastMath; +import com.jme.math.Matrix3f; +import com.jme.math.Vector3f; +import com.jme.renderer.ColorRGBA; +import com.jme.scene.shape.Box; +import com.jme.util.LoggingSystem; +import com.jme.util.geom.BufferUtils; +import com.jmex.bui.BStyleSheet; + +import com.threerings.util.Name; + +import com.threerings.presents.client.Client; +import com.threerings.presents.net.UsernamePasswordCreds; + +import com.threerings.jme.Log; +import com.threerings.jme.JmeApp; + +/** + * The main point of entry for the Jabber client application. It creates + * and initializes the myriad components of the client and sets all the + * proper wheels in motion. + */ +public class JabberApp extends JmeApp +{ + /** Used to configure our user interface. */ + public static BStyleSheet stylesheet; + + // documentation inherited + public boolean init () + { + if (!super.init()) { + return false; + } + + // initialize our client instance + _client = new JabberClient(); + _client.init(this); + + // add some simple geometry for kicks + Vector3f max = new Vector3f(15, 15, 15); + Vector3f min = new Vector3f(5, 5, 5); + + Box t = new Box("Box", min, max); + t.setModelBound(new BoundingBox()); + t.updateModelBound(); + t.setLocalTranslation(new Vector3f(0, 0, -15)); + ColorRGBA[] colors = new ColorRGBA[24]; + for (int i = 0; i < 24; i++) { + colors[i] = ColorRGBA.randomColor(); + } + t.setColorBuffer(0, BufferUtils.createFloatBuffer(colors)); + _root.attachChild(t); + _root.updateRenderState(); + + // set up the camera + Vector3f loc = new Vector3f(0, -200, 200); + _camera.setLocation(loc); + Matrix3f rotm = new Matrix3f(); + rotm.fromAngleAxis(-FastMath.PI/5, _camera.getLeft()); + rotm.mult(_camera.getDirection(), _camera.getDirection()); + rotm.mult(_camera.getUp(), _camera.getUp()); + rotm.mult(_camera.getLeft(), _camera.getLeft()); + _camera.update(); + + // speed up key input + _input.setActionSpeed(100f); + + return true; + } + + public void run (String server, String username, String password) + { + Client client = _client.getContext().getClient(); + + // pass them on to the client + Log.info("Using [server=" + server + "."); + client.setServer(server, Client.DEFAULT_SERVER_PORTS); + + // configure the client with some credentials and logon + if (username != null && password != null) { + // create and set our credentials + client.setCredentials( + new UsernamePasswordCreds(new Name(username), password)); + client.logon(); + } + + // now start up the main event loop + run(); + } + + // documentation inherited + public void stop () + { + // log off before we shutdown + Client client = _client.getContext().getClient(); + if (client.isLoggedOn()) { + client.logoff(false); + } + Log.info("Stopping."); + super.stop(); + } + + public static void main (String[] args) + { + LoggingSystem.getLogger().setLevel(java.util.logging.Level.OFF); + + String server = "localhost"; + if (args.length > 0) { + server = args[0]; + } + + // load up the default BUI stylesheet + try { + InputStream stin = JabberApp.class.getClassLoader(). + getResourceAsStream("rsrc/style.bss"); + stylesheet = new BStyleSheet( + new InputStreamReader(stin), + new BStyleSheet.DefaultResourceProvider()); + } catch (Exception e) { + e.printStackTrace(System.err); + System.exit(-1); + } + + String username = (args.length > 1) ? args[1] : null; + String password = (args.length > 2) ? args[2] : null; + + JabberApp app = new JabberApp(); + app.init(); + app.run(server, username, password); + } + + protected JabberClient _client; +} diff --git a/tests/src/java/com/threerings/jme/client/JabberClient.java b/tests/src/java/com/threerings/jme/client/JabberClient.java new file mode 100644 index 00000000..2fc854f9 --- /dev/null +++ b/tests/src/java/com/threerings/jme/client/JabberClient.java @@ -0,0 +1,228 @@ +// +// $Id: JabberClient.java 3283 2004-12-22 19:23:00Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.client; + +import com.jmex.bui.BRootNode; +import com.jme.input.InputHandler; +import com.jme.renderer.Renderer; +import com.jme.scene.Node; +import com.jme.system.DisplaySystem; + +import com.samskivert.util.Config; +import com.threerings.util.MessageManager; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.SessionObserver; +import com.threerings.presents.dobj.DObjectManager; + +import com.threerings.crowd.chat.client.ChatDirector; +import com.threerings.crowd.client.LocationDirector; +import com.threerings.crowd.client.OccupantDirector; +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.jme.JmeContext; +import com.threerings.jme.camera.CameraHandler; + +/** + * The Jabber client takes care of instantiating all of the proper + * managers and loading up all of the necessary configuration and getting + * the client bootstrapped. It can be extended by games that require an + * extended context implementation. + */ +public class JabberClient + implements SessionObserver +{ + /** + * Initializes a new client and provides it with a frame in which to + * display everything. + */ + public void init (JabberApp app) + { + _app = app; + + // create our context + _ctx = createContextImpl(); + + // create the directors/managers/etc. provided by the context + createContextServices(app); + + // for test purposes, hardcode the server info + _client.setServer("localhost", Client.DEFAULT_SERVER_PORTS); + _client.addClientObserver(this); + } + + /** + * Returns a reference to the context in effect for this client. This + * reference is valid for the lifetime of the application. + */ + public CrowdContext getContext () + { + return _ctx; + } + + // documentation inherited from interface SessionObserver + public void clientDidLogon (Client client) + { + // enter the one and only chat room; giant hack warning: we know + // that the place we're headed to is ID 2 because there's only one + // place on the whole server; a normal client would either issue + // an invocation service request at this point or look at + // something in the BootstrapData to figure out what to do + _ctx.getLocationDirector().moveTo(2); + } + + // documentation inherited from interface SessionObserver + public void clientObjectDidChange (Client client) + { + // nada + } + + // documentation inherited from interface SessionObserver + public void clientDidLogoff (Client client) + { + System.exit(0); + } + + /** + * Creates the {@link JabberContext} implementation that will be + * passed around to all of the client code. Derived classes may wish + * to override this and create some extended context implementation. + */ + protected CrowdContext createContextImpl () + { + return new JabberContextImpl(); + } + + /** + * Creates and initializes the various services that are provided by + * the context. Derived classes that provide an extended context + * should override this method and create their own extended + * services. They should be sure to call + * super.createContextServices. + */ + protected void createContextServices (JabberApp app) + { + // create the handles on our various services + _client = new Client(null, app); + + // create our managers and directors + _locdir = new LocationDirector(_ctx); + _occdir = new OccupantDirector(_ctx); + _msgmgr = new MessageManager(MESSAGE_MANAGER_PREFIX); + _chatdir = new ChatDirector(_ctx, _msgmgr, null); + } + + /** + * The context implementation. This provides access to all of the + * objects and services that are needed by the operating client. + */ + protected class JabberContextImpl implements CrowdContext, JmeContext + { + /** + * Apparently the default constructor has default access, rather + * than protected access, even though this class is declared to be + * protected. Why, I don't know, but we need to be able to extend + * this class elsewhere, so we need this. + */ + protected JabberContextImpl () { + } + + public Client getClient () { + return _client; + } + + public DObjectManager getDObjectManager () { + return _client.getDObjectManager(); + } + + public Config getConfig () { + return _config; + } + + public LocationDirector getLocationDirector () { + return _locdir; + } + + public OccupantDirector getOccupantDirector () { + return _occdir; + } + + public ChatDirector getChatDirector () { + return _chatdir; + } + + public void setPlaceView (PlaceView view) { + // TBD + } + + public void clearPlaceView (PlaceView view) { + // we'll just let the next place view replace our old one + } + + public Renderer getRenderer () { + return _app.getContext().getRenderer(); + } + + public DisplaySystem getDisplay () { + return _app.getContext().getDisplay(); + } + + public CameraHandler getCameraHandler () { + return _app.getContext().getCameraHandler(); + } + + public Node getGeometry () { + return _app.getContext().getGeometry(); + } + + public Node getInterface () { + return _app.getContext().getInterface(); + } + + public InputHandler getInputHandler () { + return _app.getContext().getInputHandler(); + } + +// public InputHandler getBufferedInputHandler () { +// return _app.getContext().getBufferedInputHandler(); +// } + + public BRootNode getRootNode () { + return _app.getContext().getRootNode(); + } + } + + protected CrowdContext _ctx; + protected JabberApp _app; + protected Config _config = new Config("jabber"); + + protected Client _client; + protected LocationDirector _locdir; + protected OccupantDirector _occdir; + protected ChatDirector _chatdir; + protected MessageManager _msgmgr; + + /** The prefix prepended to localization bundle names before looking + * them up in the classpath. */ + protected static final String MESSAGE_MANAGER_PREFIX = "rsrc.i18n"; +} diff --git a/tests/src/java/com/threerings/jme/client/JabberController.java b/tests/src/java/com/threerings/jme/client/JabberController.java new file mode 100644 index 00000000..329381b6 --- /dev/null +++ b/tests/src/java/com/threerings/jme/client/JabberController.java @@ -0,0 +1,19 @@ +// +// $Id$ + +package com.threerings.jme.client; + +import com.threerings.crowd.client.PlaceController; +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.util.CrowdContext; + +/** + * Handles the basic bits for our simple chat room. + */ +public class JabberController extends PlaceController +{ + protected PlaceView createPlaceView (CrowdContext ctx) + { + return new JabberView(ctx); + } +} diff --git a/tests/src/java/com/threerings/jme/client/JabberView.java b/tests/src/java/com/threerings/jme/client/JabberView.java new file mode 100644 index 00000000..c0c810fd --- /dev/null +++ b/tests/src/java/com/threerings/jme/client/JabberView.java @@ -0,0 +1,51 @@ +// +// $Id$ + +package com.threerings.jme.client; + +import com.jmex.bui.BLabel; +import com.jmex.bui.BWindow; +import com.jmex.bui.layout.BorderLayout; + +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.jme.JmeContext; +import com.threerings.jme.chat.ChatView; + +/** + * Manages the "view" when we're in the chat room. + */ +public class JabberView extends BWindow + implements PlaceView +{ + public JabberView (CrowdContext ctx) + { + super(JabberApp.stylesheet, new BorderLayout()); + + _ctx = ctx; + _jctx = (JmeContext)ctx; + _chat = new ChatView(_jctx, _ctx.getChatDirector()); + add(_chat, BorderLayout.CENTER); + + _jctx.getRootNode().addWindow(this); + setBounds(0, 40, 640, 240); + } + + // documentation inherited from interface PlaceView + public void willEnterPlace (PlaceObject plobj) + { + _chat.willEnterPlace(plobj); + } + + // documentation inherited from interface PlaceView + public void didLeavePlace (PlaceObject plobj) + { + _chat.didLeavePlace(plobj); + } + + protected CrowdContext _ctx; + protected JmeContext _jctx; + protected ChatView _chat; +} diff --git a/tests/src/java/com/threerings/jme/data/JabberConfig.java b/tests/src/java/com/threerings/jme/data/JabberConfig.java new file mode 100644 index 00000000..4f640013 --- /dev/null +++ b/tests/src/java/com/threerings/jme/data/JabberConfig.java @@ -0,0 +1,26 @@ +// +// $Id$ + +package com.threerings.jme.data; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.jme.client.JabberController; + +/** + * Defines the necessary bits for our chat room. + */ +public class JabberConfig extends PlaceConfig +{ + // documentation inherited + public Class getControllerClass () + { + return JabberController.class; + } + + // documentation inherited + public String getManagerClassName () + { + // nothing special needed on the server side + return "com.threerings.crowd.server.PlaceManager"; + } +} diff --git a/tests/src/java/com/threerings/jme/server/JabberServer.java b/tests/src/java/com/threerings/jme/server/JabberServer.java new file mode 100644 index 00000000..8ff43870 --- /dev/null +++ b/tests/src/java/com/threerings/jme/server/JabberServer.java @@ -0,0 +1,49 @@ +// +// $Id$ + +package com.threerings.jme.server; + +import com.threerings.crowd.Log; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.server.CrowdServer; +import com.threerings.crowd.server.PlaceManager; +import com.threerings.crowd.server.PlaceRegistry; + +import com.threerings.jme.data.JabberConfig; + +/** + * A basic server that creates a single room and sticks everyone in it + * where they can chat with one another. + */ +public class JabberServer extends CrowdServer +{ + // documentation inherited + public void init () + throws Exception + { + super.init(); + + // create a single location + plreg.createPlace( + new JabberConfig(), new PlaceRegistry.CreationObserver() { + public void placeCreated (PlaceObject place, PlaceManager pmgr) { + Log.info("Created chat room " + pmgr.where() + "."); + _place = pmgr; + } + }); + } + + public static void main (String[] args) + { + JabberServer server = new JabberServer(); + try { + server.init(); + server.run(); + } catch (Exception e) { + Log.warning("Unable to initialize server."); + Log.logStackTrace(e); + } + } + + protected PlaceManager _place; +} diff --git a/tests/src/java/com/threerings/jme/sprite/PathTest.java b/tests/src/java/com/threerings/jme/sprite/PathTest.java new file mode 100644 index 00000000..ee6d0273 --- /dev/null +++ b/tests/src/java/com/threerings/jme/sprite/PathTest.java @@ -0,0 +1,150 @@ +// +// $Id$ + +package com.threerings.jme.sprite; + +import java.util.Arrays; +import java.util.logging.Level; + +import com.jme.math.FastMath; +import com.jme.math.Matrix3f; +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; +import com.jme.renderer.ColorRGBA; +import com.jme.scene.shape.Box; +import com.jme.scene.shape.Disk; +import com.jme.scene.shape.Quad; +import com.jme.scene.shape.Sphere; +import com.jme.scene.state.LightState; +import com.jme.util.LoggingSystem; + +import com.threerings.jme.JmeApp; +import com.threerings.jme.sprite.LineSegmentPath; + +/** + * Used for testing paths. + */ +public class PathTest extends JmeApp +{ + protected void initRoot () + { + super.initRoot(); + + float dist = 10; + + // set up the camera + Vector3f loc = new Vector3f(0, -dist, 10); + _camera.setLocation(loc); + Matrix3f rotm = new Matrix3f(); + rotm.fromAngleAxis(-FastMath.PI/4, _camera.getLeft()); + rotm.multLocal(_camera.getDirection()); + rotm.multLocal(_camera.getUp()); + rotm.multLocal(_camera.getLeft()); + _camera.update(); + + for (int yy = -1; yy < 1; yy++) { + for (int xx = -1; xx < 1; xx++) { + Quad quad = new Quad("ground", 10, 10); + quad.setLightCombineMode(LightState.OFF); + int index = (yy+1)*2 + (xx+1); + quad.setSolidColor(COLORS[index]); + quad.setLocalTranslation( + new Vector3f(xx * 10 + 5, yy * 10 + 5, -1)); + _geom.attachChild(quad); + } + } + + for (int yy = -1; yy <= 1; yy++) { + for (int xx = -1; xx <= 1; xx++) { + if (xx != 0 || yy != 0) { + setup(xx * dist, yy * dist); + } + } + } + } + + protected void setup (float x, float y) + { + Box target = new Box("target", new Vector3f(-.1f, -.1f, -.1f), + new Vector3f(.1f, .1f, .1f)); + target.setLocalTranslation(new Vector3f(x, y, 0)); + _geom.attachChild(target); + + Box box = new Box("box", new Vector3f(-1, -0.5f, -0.25f), + new Vector3f(1, 0.5f, 0.25f)); + Disk disk = new Disk("dot", 10, 10, 0.5f); + disk.setLightCombineMode(LightState.OFF); + disk.setLocalTranslation(new Vector3f(0.5f, 0f, 0.3f)); + Sprite shot = new Sprite(); + shot.attachChild(box); + shot.attachChild(disk); + _geom.attachChild(shot); + + testBallistic(shot, target); +// testLineSegment(shot); + } + + protected void testBallistic (Sprite shot, Box target) + { + // start with the "cannon" pointed along the x axis + Vector3f diff = target.getLocalTranslation().subtract( + shot.getLocalTranslation()); + Vector3f vel = diff.normalize(); + float range = diff.length(), angle = -FastMath.PI/4; + + Vector3f axis = UP.cross(vel); + axis.normalizeLocal(); + + // rotate it up (around the y axis) the necessary elevation + Quaternion rot = new Quaternion(); + rot.fromAngleAxis(angle, axis); + rot.multLocal(vel); + + // give the cannon its muzzle velocity + float muzvel = FastMath.sqrt(range * BallisticPath.G / + FastMath.sin(2*angle)); + vel.multLocal(muzvel); + + Vector3f start = new Vector3f(0, 0, 0); + float time = BallisticPath.computeFlightTime(range, muzvel, angle); + shot.move(new OrientingBallisticPath( + shot, new Vector3f(1, 0, 0), start, vel, GRAVITY, time)); + + System.out.println("Range: " + range); + System.out.println("Muzzle velocity: " + muzvel); + System.out.println("Rotation axis: " + axis); + System.out.println("Angle: " + angle + " (" + + (angle * 180 / FastMath.PI) + ")"); + System.out.println("Flight time: " + time); + System.out.println("Velocity: " + vel); + } + + protected void testLineSegment (Sprite shot) + { + Vector3f[] points = new Vector3f[] { + new Vector3f(0, 0, 0), new Vector3f(2, 0, 0), new Vector3f(3, 0, 0), + new Vector3f(3, 2, 0), new Vector3f(5, 2, 0), new Vector3f(5, -2, 0), + new Vector3f(-4, -2, 0) + }; + float[] durations = new float[points.length]; + Arrays.fill(durations, 1f); + Vector3f up = new Vector3f(0, 0, 1); + Vector3f orient = new Vector3f(1, 0, 0); + shot.move(new LineSegmentPath(shot, up, orient, points, durations)); + } + + public static void main (String[] args) + { + LoggingSystem.getLogger().setLevel(Level.OFF); + PathTest test = new PathTest(); + if (!test.init()) { + System.exit(-1); + } + test.run(); + } + + protected static final Vector3f GRAVITY = new Vector3f(0, 0, -9.8f); + protected static final Vector3f UP = new Vector3f(0, 0, 1); + protected static final ColorRGBA[] COLORS = { + ColorRGBA.red, ColorRGBA.green, ColorRGBA.blue, ColorRGBA.gray }; +} diff --git a/tests/src/java/com/threerings/media/ImageLoadingSpeed.java b/tests/src/java/com/threerings/media/ImageLoadingSpeed.java new file mode 100644 index 00000000..749475de --- /dev/null +++ b/tests/src/java/com/threerings/media/ImageLoadingSpeed.java @@ -0,0 +1,80 @@ +// +// $Id: ImageLoadingSpeed.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.image.BufferedImage; +import javax.imageio.ImageIO; + +import java.io.*; + +import com.threerings.media.image.FastImageIO; + +/** + * Tests our image loading speed. + */ +public class ImageLoadingSpeed +{ + public static void main (String[] args) + { + if (args.length < 1) { + System.err.println("Usage: ImageLoadingTest image"); + System.exit(-1); + } + + File file = new File(args[0]); + File ffile = new File(args[0] + FastImageIO.FILE_SUFFIX); + try { + BufferedImage image = ImageIO.read(file); + FastImageIO.write(image, new FileOutputStream(ffile)); + } catch (IOException ioe) { + ioe.printStackTrace(System.err); + System.exit(-1); + } + + long start = System.currentTimeMillis(); + int iter = 0; + while (true) { + try { + FastImageIO.read(ffile).getWidth(); + } catch (IOException ioe) { + ioe.printStackTrace(System.err); + System.exit(-1); + } + + if (++iter == 100) { + long now = System.currentTimeMillis(); + long elapsed = now - start; + System.err.println("Loaded " + args.length + + " images a total of " + iter + + " times in " + elapsed + "ms."); + System.err.println("An average of " + (elapsed/iter) + + "ms per image."); + + System.gc(); + try { Thread.sleep(1000); } catch (Throwable t) {} + + start = now; + iter = 0; + } + } + } +} diff --git a/tests/src/java/com/threerings/media/TestIconManager.java b/tests/src/java/com/threerings/media/TestIconManager.java new file mode 100644 index 00000000..3aa73bce --- /dev/null +++ b/tests/src/java/com/threerings/media/TestIconManager.java @@ -0,0 +1,65 @@ +// +// $Id: TestIconManager.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media; + +import java.awt.BorderLayout; +import javax.swing.*; + +import com.samskivert.swing.HGroupLayout; +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.media.image.ImageManager; +import com.threerings.media.tile.TileManager; +import com.threerings.resource.ResourceManager; + +/** + * Does something extraordinary. + */ +public class TestIconManager +{ + public static void main (String[] args) + { + try { + JFrame frame = new JFrame("TestIconManager"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + ResourceManager rmgr = new ResourceManager("rsrc"); + ImageManager imgr = new ImageManager(rmgr, frame); + TileManager tmgr = new TileManager(imgr); + IconManager iconmgr = new IconManager( + tmgr, "rsrc/config/media/iconmgr.properties"); + + JPanel panel = new JPanel(new HGroupLayout()); + for (int i = 0; i < 8; i++) { + panel.add(new JButton(iconmgr.getIcon("test", i))); + } + + frame.getContentPane().add(panel, BorderLayout.CENTER); + frame.pack(); + SwingUtil.centerWindow(frame); + frame.show(); + + } catch (Exception e) { + e.printStackTrace(System.err); + } + } +} diff --git a/tests/src/java/com/threerings/media/sound/SoundTestApp.java b/tests/src/java/com/threerings/media/sound/SoundTestApp.java new file mode 100644 index 00000000..e6f2ed61 --- /dev/null +++ b/tests/src/java/com/threerings/media/sound/SoundTestApp.java @@ -0,0 +1,68 @@ +// +// $Id: SoundTestApp.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.sound; + +import com.threerings.resource.ResourceManager; +import com.threerings.media.Log; + +public class SoundTestApp +{ + public SoundTestApp (String[] args) + { + if (args.length == 0) { + Log.info("Usage: runjava com.threerings.media.SoundTestApp " + + " [ ...]"); + System.exit(0); + } + + ResourceManager rmgr = new ResourceManager("rsrc"); + _soundmgr = new SoundManager(rmgr, null, null); + _keys = args; + } + + public void run () + { + for (int ii = 0; ii < _keys.length; ii++) { + System.out.println("Playing " + _keys[ii] + "."); + _soundmgr.play(SoundManager.DEFAULT, + "com/threerings/media/sound/", _keys[ii]); + } + _soundmgr.shutdown(); + + // the sound manager starts up threads that never seem to exit so + // we have to stick a fork in things after a short while + try { + Thread.sleep(5000L); + } catch (InterruptedException ie) { + } + System.exit(0); + } + + public static void main (String[] args) + { + SoundTestApp app = new SoundTestApp(args); + app.run(); + } + + protected String[] _keys; + protected SoundManager _soundmgr; +} diff --git a/tests/src/java/com/threerings/media/sound/sounds.properties b/tests/src/java/com/threerings/media/sound/sounds.properties new file mode 100644 index 00000000..e2952dc1 --- /dev/null +++ b/tests/src/java/com/threerings/media/sound/sounds.properties @@ -0,0 +1,7 @@ +# +# $Id: sounds.properties 2051 2002-12-10 21:36:53Z mdb $ +# +# Sound test app configuration file + +sound1 = media/test.wav +sound2 = foo/bar.wav diff --git a/tests/src/java/com/threerings/media/tile/bundle/BundledTileSetRepositoryTest.java b/tests/src/java/com/threerings/media/tile/bundle/BundledTileSetRepositoryTest.java new file mode 100644 index 00000000..1e73fad8 --- /dev/null +++ b/tests/src/java/com/threerings/media/tile/bundle/BundledTileSetRepositoryTest.java @@ -0,0 +1,70 @@ +// +// $Id: BundledTileSetRepositoryTest.java 3720 2005-10-05 01:39:45Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.bundle; + +import java.awt.Component; +import java.util.Iterator; + +import com.threerings.media.image.ImageManager; +import com.threerings.resource.ResourceManager; + +import junit.framework.Test; +import junit.framework.TestCase; + +public class BundledTileSetRepositoryTest extends TestCase +{ + public BundledTileSetRepositoryTest () + { + super(BundledTileSetRepositoryTest.class.getName()); + } + + public void runTest () + { + try { + ResourceManager rmgr = new ResourceManager("rsrc"); + rmgr.initBundles( + null, "config/resource/manager.properties", null); + BundledTileSetRepository repo = new BundledTileSetRepository( + rmgr, new ImageManager(rmgr, (Component)null), "tilesets"); + Iterator sets = repo.enumerateTileSets(); + while (sets.hasNext()) { + sets.next(); +// System.out.println(sets.next()); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static Test suite () + { + return new BundledTileSetRepositoryTest(); + } + + public static void main (String[] args) + { + BundledTileSetRepositoryTest test = + new BundledTileSetRepositoryTest(); + test.runTest(); + } +} diff --git a/tests/src/java/com/threerings/media/tile/bundle/tools/BuildTestTileSetBundle.java b/tests/src/java/com/threerings/media/tile/bundle/tools/BuildTestTileSetBundle.java new file mode 100644 index 00000000..da1f285a --- /dev/null +++ b/tests/src/java/com/threerings/media/tile/bundle/tools/BuildTestTileSetBundle.java @@ -0,0 +1,94 @@ +// +// $Id: BuildTestTileSetBundle.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.bundle.tools; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; + +import com.samskivert.io.PersistenceException; +import com.samskivert.test.TestUtil; + +import com.threerings.media.tile.TileSetIDBroker; + +public class BuildTestTileSetBundle +{ + public static void main (String[] args) + { + try { + TileSetIDBroker broker = new DummyTileSetIDBroker(); + + // sort out some paths + String configPath = TestUtil.getResourcePath(CONFIG_PATH); + String descPath = TestUtil.getResourcePath(BUNDLE_DESC_PATH); + String targetPath = TestUtil.getResourcePath(TARGET_PATH); + + // create our bundler and get going + TileSetBundler bundler = new TileSetBundler(configPath); + File descFile = new File(descPath); + bundler.createBundle(broker, descFile, targetPath); + + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + /** Dummy tileset id broker that makes up tileset ids (which are + * consistent in the course of execution of the application, but not + * between invocations). */ + protected static class DummyTileSetIDBroker + extends HashMap + implements TileSetIDBroker + { + public int getTileSetID (String tileSetName) + throws PersistenceException + { + Integer id = (Integer)get(tileSetName); + if (id == null) { + id = new Integer(++_nextId); + put(tileSetName, id); + } + return id.intValue(); + } + + public boolean tileSetMapped (String tileSetName) + { + return containsKey(tileSetName); + } + + public void commit () + throws PersistenceException + { + } + + protected int _nextId; + } + + protected static final String CONFIG_PATH = + "rsrc/media/tile/bundle/tools/bundler-config.xml"; + + protected static final String BUNDLE_DESC_PATH = + "rsrc/media/tile/bundle/tools/bundle.xml"; + + protected static final String TARGET_PATH = + "rsrc/media/tile/bundle/tools/bundle.jar"; +} diff --git a/tests/src/java/com/threerings/media/tile/tools/xml/XMLTileSetParserTest.java b/tests/src/java/com/threerings/media/tile/tools/xml/XMLTileSetParserTest.java new file mode 100644 index 00000000..d36bc3bf --- /dev/null +++ b/tests/src/java/com/threerings/media/tile/tools/xml/XMLTileSetParserTest.java @@ -0,0 +1,78 @@ +// +// $Id: XMLTileSetParserTest.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.tile.tools.xml; + +import java.io.IOException; +import java.util.Iterator; +import java.util.HashMap; + +import junit.framework.Test; +import junit.framework.TestCase; + +public class XMLTileSetParserTest extends TestCase +{ + public XMLTileSetParserTest () + { + super(XMLTileSetParserTest.class.getName()); + } + + public void runTest () + { + HashMap sets = new HashMap(); + + XMLTileSetParser parser = new XMLTileSetParser(); + // add some rulesets + parser.addRuleSet("tilesets/uniform", new UniformTileSetRuleSet()); + parser.addRuleSet("tilesets/swissarmy", new SwissArmyTileSetRuleSet()); + parser.addRuleSet("tilesets/object", new ObjectTileSetRuleSet()); + + // load up the tilesets + try { + parser.loadTileSets(TILESET_PATH, sets); + + // print them out + Iterator iter = sets.values().iterator(); + while (iter.hasNext()) { + iter.next(); + // System.out.println(iter.next()); + } + + } catch (IOException ioe) { + ioe.printStackTrace(); + fail("loadTileSets() failed"); + } + } + + public static Test suite () + { + return new XMLTileSetParserTest(); + } + + public static void main (String[] args) + { + XMLTileSetParserTest test = new XMLTileSetParserTest(); + test.runTest(); + } + + protected static final String TILESET_PATH = + "rsrc/media/tile/tools/xml/tilesets.xml"; +} diff --git a/tests/src/java/com/threerings/media/util/PathViz.java b/tests/src/java/com/threerings/media/util/PathViz.java new file mode 100644 index 00000000..9575c991 --- /dev/null +++ b/tests/src/java/com/threerings/media/util/PathViz.java @@ -0,0 +1,111 @@ +// +// $Id: PathViz.java 4181 2006-06-07 21:54:12Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import javax.swing.JFrame; + +import com.threerings.media.FrameManager; +import com.threerings.media.ManagedJFrame; +import com.threerings.media.MediaPanel; + +import com.threerings.media.sprite.Sprite; + +/** + * A test app that is useful for visualizing paths during their + * development. + */ +public class PathViz extends MediaPanel +{ + public PathViz (FrameManager fmgr) + { + super(fmgr); + + // create a sprite that we can send along a path + Sprite sprite = new HappySprite(); + addSprite(sprite); + + // create a path for our sprite + Point start = new Point(150, 150); + double sangle = -3*Math.PI/4; + ArcPath path = new ArcPath( + start, 64, 48, sangle, -Math.PI/2, MOVE_DURATION, ArcPath.NORMAL); +// LinePath path = new LinePath(0, 0, 300, 300, MOVE_DURATION); + sprite.move(path); + } + + // documentation inherited from interface + public void paintBetween (Graphics2D gfx, Rectangle dirtyRect) + { + super.paintBetween(gfx, dirtyRect); + gfx.setColor(Color.gray); + gfx.fill(dirtyRect); + _spritemgr.renderSpritePaths(gfx); + } + + public Dimension getPreferredSize () + { + return new Dimension(300, 300); + } + + public static void main (String[] args) + { + ManagedJFrame frame = new ManagedJFrame("Path viz"); + FrameManager fmgr = FrameManager.newInstance(frame, frame); + + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setContentPane(new PathViz(fmgr)); + + fmgr.start(); + frame.pack(); + frame.setVisible(true); + } + + protected static class HappySprite extends Sprite + { + public HappySprite () + { + _bounds.width = 32; + _bounds.height = 32; + _oxoff = 16; + _oyoff = 16; + } + + public void paint (Graphics2D gfx) + { + gfx.setColor(Color.blue); + int hx = _bounds.x + _bounds.width/2; + int hy = _bounds.y + _bounds.height/2; + gfx.drawLine(hx, _bounds.y, hx, _bounds.y + _bounds.height-1); + gfx.drawLine(_bounds.x, hy, _bounds.x+_bounds.width-1, hy); + gfx.setColor(Color.black); + gfx.drawRect(_bounds.x, _bounds.y, + _bounds.width-1, _bounds.height-1); + } + } + + protected static final long MOVE_DURATION = 10*1000L; +} diff --git a/tests/src/java/com/threerings/media/util/TraceViz.java b/tests/src/java/com/threerings/media/util/TraceViz.java new file mode 100644 index 00000000..43ac47d9 --- /dev/null +++ b/tests/src/java/com/threerings/media/util/TraceViz.java @@ -0,0 +1,115 @@ +// +// $Id: TraceViz.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.media.util; + +import java.awt.Color; +import java.awt.Image; +import java.awt.image.BufferedImage; + +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.samskivert.swing.VGroupLayout; +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.media.Log; +import com.threerings.media.image.ImageManager; +import com.threerings.media.image.ImageUtil; + +/** + * Simple application for testing image trace functionality. + */ +public class TraceViz +{ + public TraceViz (String[] args) + throws IOException + { + // create the frame and image manager + _frame = new JFrame(); + _frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + ImageManager imgr = new ImageManager(null, _frame); + + // set up the content panel + JPanel content = (JPanel)_frame.getContentPane(); + content.setBackground(Color.white); + content.setLayout(new VGroupLayout()); + + // load the image + BufferedImage image = ImageIO.read(new File(args[0])); + if (image == null) { + throw new RuntimeException("Failed to read file " + + "[file=" + args[0] + "]."); + } + +// // create a compatible image +// BufferedImage cimage = imgr.createImage( +// image.getWidth(null), image.getHeight(null), +// Transparency.TRANSLUCENT); +// Graphics g = cimage.getGraphics(); +// g.drawImage(image, 0, 0, null); +// g.dispose(); +// image = cimage; + + // create the traced image + Image timage = ImageUtil.createTracedImage( + imgr, image, Color.red, 5, 0.4f, 0.1f); + + // display the (prepared) original and traced image + content.add(new JLabel(new ImageIcon(image))); + content.add(new JLabel(new ImageIcon(timage))); + } + + public void run () + { + _frame.pack(); + SwingUtil.centerWindow(_frame); + _frame.show(); + } + + public static void main (String[] args) + { + try { + if (args.length == 0) { + System.out.println( + "Usage: java com.threerings.media.util.TraceViz " + + ""); + return; + } + + TraceViz app = new TraceViz(args); + app.run(); + + } catch (Exception e) { + Log.warning("Failed to run application: " + e); + Log.logStackTrace(e); + } + } + + protected JFrame _frame; +} diff --git a/tests/src/java/com/threerings/miso/client/ScrollingFrame.java b/tests/src/java/com/threerings/miso/client/ScrollingFrame.java new file mode 100644 index 00000000..38b6c720 --- /dev/null +++ b/tests/src/java/com/threerings/miso/client/ScrollingFrame.java @@ -0,0 +1,89 @@ +// +// $Id: ScrollingFrame.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.Color; +import java.awt.Component; +import java.awt.GraphicsConfiguration; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; + +import com.samskivert.swing.VGroupLayout; + +import com.threerings.media.SafeScrollPane; + +/** + * The main application window. + */ +public class ScrollingFrame extends JFrame +{ + /** + * Creates a frame in which the scrolling test app can operate. + */ + public ScrollingFrame (GraphicsConfiguration gc) + { + super(gc); + + // set up the frame options + setTitle("Scene scrolling test"); + // setUndecorated(true); + setIgnoreRepaint(true); + setResizable(false); + setDefaultCloseOperation(EXIT_ON_CLOSE); + + // set the frame and content panel background to black + setBackground(Color.black); + getContentPane().setBackground(Color.black); + + // create some interface elements to go with our scrolling panel + VGroupLayout vgl = new VGroupLayout(VGroupLayout.STRETCH); + vgl.setOffAxisPolicy(VGroupLayout.STRETCH); + getContentPane().setLayout(vgl); + + vgl = new VGroupLayout(VGroupLayout.NONE); + vgl.setOffAxisPolicy(VGroupLayout.STRETCH); + JPanel stuff = new JPanel(vgl); + for (int i = 0; i < 10; i++) { + stuff.add(new JButton("Button " + i)); + } + getContentPane().add(new SafeScrollPane(stuff)); + } + + /** + * Sets the panel displayed by this frame. + */ + public void setPanel (Component panel) + { + // if we had an old panel, remove it + if (_panel != null) { + getContentPane().remove(_panel); + } + + // now add the new one + _panel = panel; + getContentPane().add(_panel, 0); + } + + protected Component _panel; +} diff --git a/tests/src/java/com/threerings/miso/client/ScrollingScene.java b/tests/src/java/com/threerings/miso/client/ScrollingScene.java new file mode 100644 index 00000000..b7aa76c4 --- /dev/null +++ b/tests/src/java/com/threerings/miso/client/ScrollingScene.java @@ -0,0 +1,124 @@ +// +// $Id: ScrollingScene.java 3355 2005-02-17 01:54:54Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.Rectangle; + +import java.util.Iterator; +import java.util.Random; + +import com.samskivert.io.PersistenceException; + +import com.threerings.media.tile.NoSuchTileSetException; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileSetRepository; + +import com.threerings.miso.data.MisoSceneModel; +import com.threerings.miso.tile.BaseTile; +import com.threerings.miso.tile.BaseTileSet; +import com.threerings.miso.util.MisoContext; +import com.threerings.miso.util.ObjectSet; +import com.threerings.miso.data.ObjectInfo; + +/** + * Provides an infinite array of tiles in which to scroll. + */ +public class ScrollingScene extends MisoSceneModel +{ + public ScrollingScene (MisoContext ctx) + throws NoSuchTileSetException, PersistenceException + { + // locate the water tileset + TileSetRepository tsrepo = ctx.getTileManager().getTileSetRepository(); + Iterator iter = tsrepo.enumerateTileSets(); + String tsname = null; + while (iter.hasNext()) { + TileSet tset = (TileSet)iter.next(); + // yay for built-in regex support! + if (tset.getName().matches(".*[Ww]ater.*") && + tset instanceof BaseTileSet) { + tsname = tset.getName(); + break; + } + } + + if (tsname == null) { + throw new RuntimeException("Unable to locate water tileset."); + } + + // now we look the tileset up by name so that it is properly + // initialized and all that business + TileSet wtset = ctx.getTileManager().getTileSet(tsname); + + // grab our four repeating tiles + _tiles = new BaseTile[wtset.getTileCount()]; + for (int ii = 0; ii < wtset.getTileCount(); ii++) { + _tiles[ii] = (BaseTile)wtset.getTile(ii); + } + } + + public int getBaseTileId (int x, int y) + { + return -1; + } + + public boolean setBaseTile (int fqTileId, int x, int y) + { + return false; + } + + public boolean addObject (ObjectInfo info) + { + return true; + } + + public void getObjects (Rectangle region, ObjectSet set) + { + } + + public void updateObject (ObjectInfo info) + { + } + + public boolean removeObject (ObjectInfo info) + { + return false; + } + + // documentation inherited from interface + public BaseTile getBaseTile (int x, int y) + { + long seed = ((x^y) ^ multiplier) & mask; + long hash = (seed * multiplier + addend) & mask; + int tidx = (int)((hash >> 10) % _tiles.length); + return _tiles[tidx]; + } + + protected BaseTile[] _tiles; + protected Random _rand = new Random(); + + protected final static long multiplier = 0x5DEECE66DL; + protected final static long addend = 0xBL; + protected final static long mask = (1L << 48) - 1; + + protected static final int WATER_TILESET_ID = 8; +} diff --git a/tests/src/java/com/threerings/miso/client/ScrollingTestApp.java b/tests/src/java/com/threerings/miso/client/ScrollingTestApp.java new file mode 100644 index 00000000..091f2b12 --- /dev/null +++ b/tests/src/java/com/threerings/miso/client/ScrollingTestApp.java @@ -0,0 +1,234 @@ +// +// $Id: ScrollingTestApp.java 4181 2006-06-07 21:54:12Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.client; + +import java.awt.DisplayMode; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; + +import java.io.IOException; + +import com.samskivert.swing.util.SwingUtil; +import com.samskivert.util.Config; + +import com.threerings.resource.ResourceManager; + +import com.threerings.media.FrameManager; +import com.threerings.media.image.ImageManager; +import com.threerings.media.util.LinePath; +import com.threerings.media.util.Path; + +import com.threerings.media.sprite.PathAdapter; +import com.threerings.media.sprite.Sprite; + +import com.threerings.media.tile.bundle.BundledTileSetRepository; + +import com.threerings.cast.CharacterComponent; +import com.threerings.cast.CharacterDescriptor; +import com.threerings.cast.CharacterManager; +import com.threerings.cast.CharacterSprite; +import com.threerings.cast.NoSuchComponentException; +import com.threerings.cast.bundle.BundledComponentRepository; + +import com.threerings.miso.Log; +import com.threerings.miso.MisoConfig; +import com.threerings.miso.tile.MisoTileManager; +import com.threerings.miso.util.MisoContext; + +/** + * Tests the scrolling capabilities of the IsoSceneView. + */ +public class ScrollingTestApp +{ + /** + * Construct and initialize the scrolling test app. + */ + public ScrollingTestApp (String[] args) + throws IOException + { + // get the graphics environment + GraphicsEnvironment env = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + + // get the target graphics device + GraphicsDevice gd = env.getDefaultScreenDevice(); + Log.info("Graphics device [dev=" + gd + + ", mem=" + gd.getAvailableAcceleratedMemory() + + ", displayChange=" + gd.isDisplayChangeSupported() + + ", fullScreen=" + gd.isFullScreenSupported() + "]."); + + // get the graphics configuration and display mode information + GraphicsConfiguration gc = gd.getDefaultConfiguration(); + DisplayMode dm = gd.getDisplayMode(); + Log.info("Display mode [bits=" + dm.getBitDepth() + + ", wid=" + dm.getWidth() + ", hei=" + dm.getHeight() + + ", refresh=" + dm.getRefreshRate() + "]."); + + // create the window + _frame = new ScrollingFrame(gc); + + // set up our frame manager + _framemgr = FrameManager.newInstance(_frame, _frame); + + // we don't need to configure anything + ResourceManager rmgr = new ResourceManager("rsrc"); + rmgr.initBundles( + null, "config/resource/manager.properties", null); + ImageManager imgr = new ImageManager(rmgr, _frame); + _tilemgr = new MisoTileManager(rmgr, imgr); + _tilemgr.setTileSetRepository( + new BundledTileSetRepository(rmgr, imgr, "tilesets")); + + // hack in some different MisoProperties + MisoConfig.config = new Config("rsrc/config/miso/scrolling"); + + // create the context object + MisoContext ctx = new ContextImpl(); + + // create the various managers + BundledComponentRepository crepo = + new BundledComponentRepository(rmgr, imgr, "components"); + CharacterManager charmgr = new CharacterManager(imgr, crepo); + + // create our scene view panel + _panel = new MisoScenePanel(ctx, MisoConfig.getSceneMetrics()); + _frame.setPanel(_panel); + + // create our "ship" sprite + String scclass = "navsail", scname = "smsloop"; + try { + CharacterComponent ccomp = crepo.getComponent(scclass, scname); + CharacterDescriptor desc = new CharacterDescriptor( + new int[] { ccomp.componentId }, null); + + // now create the actual sprite and stick 'em in the scene + _ship = charmgr.getCharacter(desc); + if (_ship != null) { + _ship.setFollowingPathAction("sailing"); + _ship.setRestingAction("sailing"); + _ship.setActionSequence("sailing"); + _ship.setLocation(0, 0); + _panel.addSprite(_ship); + } + + } catch (NoSuchComponentException nsce) { + Log.warning("Can't locate ship component [class=" + scclass + + ", name=" + scname + "]."); + } + + _ship.addSpriteObserver(new PathAdapter() { + public void pathCompleted (Sprite sprite, Path path, long when) { + // keep scrolling for a spell + if (++_sidx < DX.length) { + int x = _ship.getX(), y = _ship.getY(); + LinePath lpath = new LinePath( + x, y, x + DX[_sidx], y + DY[_sidx], 30000l); + _ship.move(lpath); + } + } + protected int _sidx = -1; + protected final int[] DX = { 1620, 0, 1000, -1000, 1000, 2000 }; + protected final int[] DY = { 1400, 1000, 0, 1000, -1000, 1000 }; + }); + + // make the panel follow the ship around + _panel.setFollowsPathable(_ship, MisoScenePanel.CENTER_ON_PATHABLE); + int x = _ship.getX(), y = _ship.getY(); + _ship.move(new LinePath(x, y, x, y + 1000, 3000l)); + + // size and position the window, entering full-screen exclusive + // mode if available + if (gd.isFullScreenSupported()) { + Log.info("Entering full-screen exclusive mode."); + gd.setFullScreenWindow(_frame); + _frame.setUndecorated(true); + + } else { + Log.warning("Full-screen exclusive mode not available."); + // _frame.pack(); + _frame.setSize(200, 300); + SwingUtil.centerWindow(_frame); + } + + try { + _panel.setSceneModel(new ScrollingScene(ctx)); + } catch (Exception e) { + Log.warning("Error creating scene: " + e); + Log.logStackTrace(e); + } + } + + /** + * The implementation of the MisoContext interface that provides + * handles to the manager objects that offer commonly used services. + */ + protected class ContextImpl implements MisoContext + { + public MisoTileManager getTileManager () { + return _tilemgr; + } + + public FrameManager getFrameManager () { + return _framemgr; + } + } + + /** + * Run the application. + */ + public void run () + { + // show the window + _frame.show(); + _framemgr.start(); + } + + /** + * Instantiate the application object and start it running. + */ + public static void main (String[] args) + { + try { + ScrollingTestApp app = new ScrollingTestApp(args); + app.run(); + } catch (IOException ioe) { + System.err.println("Error initializing scrolling app."); + ioe.printStackTrace(); + } + } + + /** The tile manager object. */ + protected MisoTileManager _tilemgr; + + /** The frame manager. */ + protected FrameManager _framemgr; + + /** The main application window. */ + protected ScrollingFrame _frame; + + /** The main panel. */ + protected MisoScenePanel _panel; + + /** The ship in the center of our screen. */ + protected CharacterSprite _ship; +} diff --git a/tests/src/java/com/threerings/miso/viewer/ViewerApp.java b/tests/src/java/com/threerings/miso/viewer/ViewerApp.java new file mode 100644 index 00000000..c488fcf9 --- /dev/null +++ b/tests/src/java/com/threerings/miso/viewer/ViewerApp.java @@ -0,0 +1,191 @@ +// +// $Id: ViewerApp.java 4181 2006-06-07 21:54:12Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.viewer; + +import java.awt.DisplayMode; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.io.IOException; + +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.resource.ResourceManager; + +import com.threerings.media.FrameManager; +import com.threerings.media.image.ImageManager; +import com.threerings.media.tile.bundle.BundledTileSetRepository; + +import com.threerings.cast.CharacterManager; +import com.threerings.cast.bundle.BundledComponentRepository; + +import com.threerings.miso.Log; +import com.threerings.miso.data.SimpleMisoSceneModel; +import com.threerings.miso.tile.MisoTileManager; +import com.threerings.miso.tools.xml.SimpleMisoSceneParser; +import com.threerings.miso.util.MisoContext; + +/** + * The ViewerApp is a scene viewing application that allows for trying + * out game scenes in a pseudo-runtime environment. + */ +public class ViewerApp +{ + /** + * Construct and initialize the ViewerApp object. + */ + public ViewerApp (String[] args) + throws IOException + { + if (args.length < 1) { + System.err.println("Usage: ViewerApp scene_file.xml"); + System.exit(-1); + } + + // get the graphics environment + GraphicsEnvironment env = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + + // get the target graphics device + GraphicsDevice gd = env.getDefaultScreenDevice(); + Log.info("Graphics device [dev=" + gd + + ", mem=" + gd.getAvailableAcceleratedMemory() + + ", displayChange=" + gd.isDisplayChangeSupported() + + ", fullScreen=" + gd.isFullScreenSupported() + "]."); + + // get the graphics configuration and display mode information + GraphicsConfiguration gc = gd.getDefaultConfiguration(); + DisplayMode dm = gd.getDisplayMode(); + Log.info("Display mode [bits=" + dm.getBitDepth() + + ", wid=" + dm.getWidth() + ", hei=" + dm.getHeight() + + ", refresh=" + dm.getRefreshRate() + "]."); + + // create the window + _frame = new ViewerFrame(gc); + _framemgr = FrameManager.newInstance(_frame, _frame); + + // we don't need to configure anything + ResourceManager rmgr = new ResourceManager("rsrc"); + rmgr.initBundles( + null, "config/resource/manager.properties", null); + ImageManager imgr = new ImageManager(rmgr, _frame); + _tilemgr = new MisoTileManager(rmgr, imgr); + _tilemgr.setTileSetRepository( + new BundledTileSetRepository(rmgr, imgr, "tilesets")); + + // create the context object + MisoContext ctx = new ContextImpl(); + + // create the various managers + BundledComponentRepository crepo = + new BundledComponentRepository(rmgr, imgr, "components"); + CharacterManager charmgr = new CharacterManager(imgr, crepo); + + // create our scene view panel + _panel = new ViewerSceneViewPanel(ctx, charmgr, crepo); + _frame.setPanel(_panel); + + // load up the scene specified by the user + try { + SimpleMisoSceneParser parser = new SimpleMisoSceneParser(""); + SimpleMisoSceneModel model = parser.parseScene(args[0]); + if (model == null) { + Log.warning("No miso scene found in scene file " + + "[path=" + args[0] + "]."); + System.exit(-1); + } + _panel.setSceneModel(model); + + } catch (Exception e) { + Log.warning("Unable to parse scene [path=" + args[0] + "]."); + Log.logStackTrace(e); + System.exit(-1); + } + + // size and position the window, entering full-screen exclusive + // mode if available + if (gd.isFullScreenSupported()) { + Log.info("Entering full-screen exclusive mode."); + gd.setFullScreenWindow(_frame); + + } else { + Log.warning("Full-screen exclusive mode not available."); + // _frame.pack(); + _frame.setSize(600, 400); + SwingUtil.centerWindow(_frame); + } + } + + /** + * The implementation of the MisoContext interface that provides + * handles to the config and manager objects that offer commonly used + * services. + */ + protected class ContextImpl implements MisoContext + { + public MisoTileManager getTileManager () + { + return _tilemgr; + } + + public FrameManager getFrameManager () + { + return _framemgr; + } + } + + /** + * Run the application. + */ + public void run () + { + // show the window + _frame.show(); + _framemgr.start(); + } + + /** + * Instantiate the application object and start it running. + */ + public static void main (String[] args) + { + try { + ViewerApp app = new ViewerApp(args); + app.run(); + } catch (IOException ioe) { + System.err.println("Error initializing viewer app."); + ioe.printStackTrace(); + } + } + + /** The tile manager object. */ + protected MisoTileManager _tilemgr; + + /** The frame manager. */ + protected FrameManager _framemgr; + + /** The main application window. */ + protected ViewerFrame _frame; + + /** The main panel. */ + protected ViewerSceneViewPanel _panel; +} diff --git a/tests/src/java/com/threerings/miso/viewer/ViewerFrame.java b/tests/src/java/com/threerings/miso/viewer/ViewerFrame.java new file mode 100644 index 00000000..5728f18d --- /dev/null +++ b/tests/src/java/com/threerings/miso/viewer/ViewerFrame.java @@ -0,0 +1,94 @@ +// +// $Id: ViewerFrame.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.viewer; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.GraphicsConfiguration; +import java.awt.event.ActionEvent; + +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; + +import com.samskivert.swing.util.MenuUtil; + +/** + * The viewer frame is the main application window. + */ +public class ViewerFrame extends JFrame +{ + /** + * Creates a frame in which the viewer application can operate. + */ + public ViewerFrame (GraphicsConfiguration gc) + { + super(gc); + + // set up the frame options + setTitle("Scene Viewer"); + setResizable(false); + setDefaultCloseOperation(EXIT_ON_CLOSE); + + // set the frame and content panel background to black + setBackground(Color.black); + getContentPane().setBackground(Color.black); + + // create the "Settings" menu + JMenu menuSettings = new JMenu("Settings"); + MenuUtil.addMenuItem( + menuSettings, "Preferences", this, "handlePreferences"); + + // create the menu bar + JMenuBar bar = new JMenuBar(); + bar.add(menuSettings); + + // add the menu bar to the frame + setJMenuBar(bar); + } + + /** + * Sets the panel displayed by this frame. + */ + public void setPanel (Component panel) + { + // if we had an old panel, remove it + if (_panel != null) { + getContentPane().remove(_panel); + } + + // now add the new one + _panel = panel; + getContentPane().add(_panel, BorderLayout.CENTER); + } + + /** + * Dummy callback method. + */ + public void handlePreferences (ActionEvent event) + { + System.err.println("Nothing doing!"); + } + + protected Component _panel; +} diff --git a/tests/src/java/com/threerings/miso/viewer/ViewerSceneViewPanel.java b/tests/src/java/com/threerings/miso/viewer/ViewerSceneViewPanel.java new file mode 100644 index 00000000..e045efae --- /dev/null +++ b/tests/src/java/com/threerings/miso/viewer/ViewerSceneViewPanel.java @@ -0,0 +1,237 @@ +// +// $Id: ViewerSceneViewPanel.java 4188 2006-06-13 18:03:48Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.miso.viewer; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.event.MouseEvent; + +import com.samskivert.util.RandomUtil; + +import com.threerings.cast.CharacterDescriptor; +import com.threerings.cast.CharacterManager; +import com.threerings.cast.CharacterSprite; +import com.threerings.cast.ComponentRepository; +import com.threerings.cast.util.CastUtil; + +import com.threerings.media.sprite.PathObserver; +import com.threerings.media.sprite.Sprite; +import com.threerings.media.sprite.SpriteManager; +import com.threerings.media.util.LineSegmentPath; + +import com.threerings.media.util.Path; +import com.threerings.media.util.PerformanceMonitor; +import com.threerings.media.util.PerformanceObserver; + +import com.threerings.miso.Log; +import com.threerings.miso.MisoConfig; +import com.threerings.miso.client.MisoScenePanel; +import com.threerings.miso.data.MisoSceneModel; +import com.threerings.miso.util.MisoContext; + +public class ViewerSceneViewPanel extends MisoScenePanel + implements PerformanceObserver, PathObserver +{ + /** + * Construct the panel and initialize it with a context. + */ + public ViewerSceneViewPanel (MisoContext ctx, + CharacterManager charmgr, + ComponentRepository crepo) + { + super(ctx, MisoConfig.getSceneMetrics()); + + // create the character descriptors + _descUser = CastUtil.getRandomDescriptor("female", crepo); + _descDecoy = CastUtil.getRandomDescriptor("male", crepo); + + // create the manipulable sprite + _sprite = createSprite(_spritemgr, charmgr, _descUser); + setFollowsPathable(_sprite, CENTER_ON_PATHABLE); + + // create the decoy sprites + createDecoys(_spritemgr, charmgr); + + PerformanceMonitor.register(this, "paint", 1000); + } + + // documentation inherited + public void setSceneModel (MisoSceneModel model) + { + super.setSceneModel(model); + Log.info("Using " + model + "."); + } + + // documentation inherited + public void doLayout () + { + super.doLayout(); + + // now that we have a scene, we can create valid paths for our + // decoy sprites + createDecoyPaths(); + } + + /** + * Creates a new sprite. + */ + protected CharacterSprite createSprite ( + SpriteManager spritemgr, CharacterManager charmgr, + CharacterDescriptor desc) + { + CharacterSprite s = charmgr.getCharacter(desc); + if (s != null) { + // start 'em out standing + s.setActionSequence(CharacterSprite.STANDING); + s.setLocation(300, 300); + s.addSpriteObserver(this); + spritemgr.addSprite(s); + } + + return s; + } + + /** + * Creates the decoy sprites. + */ + protected void createDecoys ( + SpriteManager spritemgr, CharacterManager charmgr) + { + _decoys = new CharacterSprite[NUM_DECOYS]; + for (int ii = 0; ii < NUM_DECOYS; ii++) { + _decoys[ii] = createSprite(spritemgr, charmgr, _descDecoy); + } + } + + /** + * Creates paths for the decoy sprites. + */ + protected void createDecoyPaths () + { + for (int ii = 0; ii < NUM_DECOYS; ii++) { + if (_decoys[ii] != null) { + createRandomPath(_decoys[ii]); + } + } + } + + // documentation inherited + public void paint (Graphics g) + { + super.paint(g); + PerformanceMonitor.tick(this, "paint"); + } + + // documentation inherited + public void checkpoint (String name, int ticks) + { + Log.info(name + " [ticks=" + ticks + "]."); + } + + // documentation inherited + public void mousePressed (MouseEvent e) + { + super.mousePressed(e); + + int x = e.getX(), y = e.getY(); + Log.info("Mouse pressed +" + x + "+" + y); + + switch (e.getModifiers()) { + case MouseEvent.BUTTON1_MASK: + createPath(_sprite, x, y); + break; + + case MouseEvent.BUTTON2_MASK: + for (int ii = 0; ii < NUM_DECOYS; ii++) { + createPath(_decoys[ii], x, y); + } + break; + } + } + + /** + * Assigns the sprite a path leading to the given destination + * screen coordinates. Returns whether a path was successfully + * assigned. + */ + protected boolean createPath (CharacterSprite s, int x, int y) + { + // get the path from here to there + LineSegmentPath path = (LineSegmentPath)getPath(s, x, y, true); + if (path == null) { + s.cancelMove(); + return false; + } + + // start the sprite moving along the path + path.setVelocity(100f/1000f); + s.move(path); + return true; + } + + /** + * Assigns a new random path to the given sprite. + */ + protected void createRandomPath (CharacterSprite s) + { + Dimension d = _vbounds.getSize(); + if (d.width <= 0 || d.height <= 0) { + return; + } + int x, y; + do { + x = RandomUtil.getInt(d.width); + y = RandomUtil.getInt(d.height); + } while (!createPath(s, x, y)); + } + + // documentation inherited + public void pathCompleted (Sprite sprite, Path path, long when) + { + CharacterSprite s = (CharacterSprite)sprite; + if (s != _sprite) { + // move the sprite to a new random location + createRandomPath(s); + } + } + + // documentation inherited + public void pathCancelled (Sprite sprite, Path path) + { + // nothing doing + } + + /** The number of decoy characters milling about. */ + protected static final int NUM_DECOYS = 5; + + /** The character descriptor for the user character. */ + protected CharacterDescriptor _descUser; + + /** The character descriptor for the decoy characters. */ + protected CharacterDescriptor _descDecoy; + + /** The sprite we're manipulating within the view. */ + protected CharacterSprite _sprite; + + /** The test sprites that meander about aimlessly. */ + protected CharacterSprite _decoys[]; +} diff --git a/tests/src/java/com/threerings/openal/TestSoundManager.java b/tests/src/java/com/threerings/openal/TestSoundManager.java new file mode 100644 index 00000000..caf3fdc6 --- /dev/null +++ b/tests/src/java/com/threerings/openal/TestSoundManager.java @@ -0,0 +1,55 @@ +// +// $Id$ + +package com.threerings.openal; + +import java.io.IOException; + +import com.samskivert.util.Interval; +import com.samskivert.util.Queue; +import com.samskivert.util.RunQueue; + +/** + * Tests the OpenAL sound system. + */ +public class TestSoundManager +{ + public static void main (String[] args) + { + if (args.length == 0) { + System.err.println("Usage: TestSoundManager sound.wav"); + System.exit(-1); + } + + final Thread current = Thread.currentThread(); + RunQueue rqueue = new RunQueue() { + public void postRunnable (Runnable r) { + _queue.append(r); + } + public boolean isDispatchThread () { + return (current == Thread.currentThread()); + } + }; + + SoundManager smgr = SoundManager.createSoundManager(rqueue); + ClipProvider provider = new WaveDataClipProvider(); + final SoundGroup group = smgr.createGroup(provider, 5); + final String path = args[0]; + + // queue up an interval to play a sound over and over + Interval i = new Interval(rqueue) { + public void expired () { + Sound sound = group.getSound(path); + sound.play(true); + } + }; + i.schedule(100L, true); + + while (true) { + Runnable r = (Runnable)_queue.get(); + r.run(); + } + } + + protected static Queue _queue = new Queue(); +} diff --git a/tests/src/java/com/threerings/util/DirectionTest.java b/tests/src/java/com/threerings/util/DirectionTest.java new file mode 100644 index 00000000..28501b12 --- /dev/null +++ b/tests/src/java/com/threerings/util/DirectionTest.java @@ -0,0 +1,73 @@ +// +// $Id: DirectionTest.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import junit.framework.Test; +import junit.framework.TestCase; + +/** + * Tests the {@link Direction} class. + */ +public class DirectionTest extends TestCase + implements DirectionCodes +{ + public DirectionTest () + { + super(DirectionTest.class.getName()); + } + + public void runTest () + { + int orient = NORTH; + + for (int i = 0; i < FINE_DIRECTION_COUNT; i++) { +// System.out.print(DirectionUtil.toShortString(orient) + " -> "); + orient = DirectionUtil.rotateCW(orient, 1); + } +// System.out.println(DirectionUtil.toShortString(orient)); + assertTrue("CW rotate", orient == NORTH); + + for (int i = 0; i < FINE_DIRECTION_COUNT; i++) { +// System.out.print(DirectionUtil.toShortString(orient) + " -> "); + orient = DirectionUtil.rotateCCW(orient, 1); + } +// System.out.println(DirectionUtil.toShortString(orient)); + assertTrue("CCW rotate", orient == NORTH); + +// for (double theta = -Math.PI; theta <= Math.PI; theta += Math.PI/1000) { +// orient = DirectionUtil.getFineDirection(theta); +// System.out.println(Math.toDegrees(theta) + " => " + +// DirectionUtil.toShortString(orient)); +// } + } + + public static Test suite () + { + return new DirectionTest(); + } + + public static void main (String[] args) + { + DirectionTest test = new DirectionTest(); + test.runTest(); + } +} diff --git a/tests/src/java/com/threerings/util/DirectionViz.java b/tests/src/java/com/threerings/util/DirectionViz.java new file mode 100644 index 00000000..6d3c2472 --- /dev/null +++ b/tests/src/java/com/threerings/util/DirectionViz.java @@ -0,0 +1,86 @@ +// +// $Id: DirectionViz.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Point; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +/** + * Renders the output of {@link DirectionUtil#getDirection} just for + * kicks. + */ +public class DirectionViz extends JPanel + implements MouseMotionListener +{ + public DirectionViz () + { + addMouseMotionListener(this); + } + + public void doLayout () + { + super.doLayout(); + _center = new Point(getWidth() / 2, getHeight() / 2); + } + + public void paintComponent (Graphics g) + { + super.paintComponent(g); + + g.setColor(Color.blue); + g.drawLine(_center.x, _center.y, _spot.x, _spot.y); + + int orient = DirectionUtil.getFineDirection(_center, _spot); + g.drawString(DirectionUtil.toShortString(orient), _spot.x, _spot.y); + } + + public void mouseDragged (MouseEvent event) + { + } + + public void mouseMoved (MouseEvent event) + { + _spot.x = event.getX(); + _spot.y = event.getY(); + repaint(); + } + + public static void main (String[] args) + { + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.getContentPane().add(new DirectionViz(), BorderLayout.CENTER); + frame.setSize(300, 300); + frame.show(); + } + + protected Point _center; + protected Point _spot = new Point(); +} diff --git a/tests/src/java/com/threerings/util/KeyTimerApp.java b/tests/src/java/com/threerings/util/KeyTimerApp.java new file mode 100644 index 00000000..bc4c4cc9 --- /dev/null +++ b/tests/src/java/com/threerings/util/KeyTimerApp.java @@ -0,0 +1,97 @@ +// +// $Id: KeyTimerApp.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import java.awt.Frame; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +import com.threerings.util.Log; + +public class KeyTimerApp +{ + public KeyTimerApp () + { + _frame = new TestFrame(); + _frame.setSize(400, 300); + } + + public void run () + { + _frame.show(); + } + + public static void main (String[] args) + { + KeyTimerApp app = new KeyTimerApp(); + app.run(); + } + + protected class TestFrame extends Frame implements KeyListener + { + public TestFrame () + { + addKeyListener(this); + _prStart = _rpStart = -1; + } + + public void keyPressed (KeyEvent e) + { + long now = System.currentTimeMillis(); + _prStart = now; + + if (_rpStart != -1) { + Log.info("RP\t" + (now - _rpStart)); + } + + logKey("keyPressed", e); + } + + public void keyReleased (KeyEvent e) + { + long now = System.currentTimeMillis(); + _rpStart = now; + + Log.info("PR\t" + (now - _prStart)); + + logKey("keyReleased", e); + } + + public void keyTyped (KeyEvent e) + { + logKey("keyTyped", e); + } + + /** + * Logs the given message and key. + */ + protected void logKey (String msg, KeyEvent e) + { + int keyCode = e.getKeyCode(); + Log.info(msg + " [key=" + KeyEvent.getKeyText(keyCode) + "]."); + } + + protected long _prStart, _rpStart; + } + + protected Frame _frame; +} diff --git a/tests/src/java/com/threerings/util/KeyboardManagerApp.java b/tests/src/java/com/threerings/util/KeyboardManagerApp.java new file mode 100644 index 00000000..822fd650 --- /dev/null +++ b/tests/src/java/com/threerings/util/KeyboardManagerApp.java @@ -0,0 +1,108 @@ +// +// $Id: KeyboardManagerApp.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import com.samskivert.swing.Controller; +import com.samskivert.swing.ControllerProvider; + +import com.threerings.util.Log; + +public class KeyboardManagerApp +{ + public KeyboardManagerApp () + { + _frame = new TestFrame(); + _frame.setSize(400, 300); + } + + public void run () + { + _frame.show(); + } + + public static void main (String[] args) + { + KeyboardManagerApp app = new KeyboardManagerApp(); + app.run(); + } + + protected static class TestFrame extends JFrame + implements ControllerProvider + { + public TestFrame () + { + // create the test controller + _ctrl = new TestController(); + + setDefaultCloseOperation(EXIT_ON_CLOSE); + + JPanel top = new JPanel(); + + // add some sample key mappings + KeyTranslatorImpl xlate = new KeyTranslatorImpl(); + xlate.addPressCommand(KeyEvent.VK_LEFT, TestController.MOVE_LEFT); + xlate.addPressCommand( + KeyEvent.VK_RIGHT, TestController.MOVE_RIGHT); + xlate.addPressCommand(KeyEvent.VK_SPACE, TestController.DROP); + + // create the keyboard manager + KeyboardManager keymgr = new KeyboardManager(); + keymgr.setTarget(top, xlate); + keymgr.setEnabled(true); + + getContentPane().add(top); + } + + // documentation inherited + public Controller getController () + { + return _ctrl; + } + + /** The test controller. */ + protected Controller _ctrl; + } + + protected static class TestController extends Controller + { + public static final String MOVE_LEFT = "move_left"; + public static final String MOVE_RIGHT = "move_right"; + public static final String DROP = "drop"; + + // documentation inherited + public boolean handleAction (ActionEvent action) + { + String cmd = action.getActionCommand(); + Log.info("handleAction [cmd=" + cmd + "]."); + return true; + } + } + + /** The test frame. */ + protected JFrame _frame; +} diff --git a/tests/src/java/com/threerings/util/MessageBundleTest.java b/tests/src/java/com/threerings/util/MessageBundleTest.java new file mode 100644 index 00000000..25f74049 --- /dev/null +++ b/tests/src/java/com/threerings/util/MessageBundleTest.java @@ -0,0 +1,79 @@ +// +// $Id: MessageBundleTest.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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 Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// 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 A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.util; + +import java.util.ResourceBundle; + +import junit.framework.Test; +import junit.framework.TestCase; + +/** + * Tests the {@link MessageBundle} class. + */ +public class MessageBundleTest extends TestCase +{ + public MessageBundleTest () + { + super(MessageBundleTest.class.getName()); + } + + public void runTest () + { + try { + String path = "rsrc.i18n.messages"; + ResourceBundle rbundle = ResourceBundle.getBundle(path); + MessageBundle bundle = new MessageBundle(); + bundle.init(null, "test", rbundle, null); + + String key1 = MessageBundle.compose("m.foo", + MessageBundle.taint("bar"), + MessageBundle.taint("baz")); + String key2 = MessageBundle.compose("m.biff", + MessageBundle.taint("beep"), + MessageBundle.taint("boop")); + String key = MessageBundle.compose("m.meta", key1, key2); + + String output = bundle.xlate(key); + if (!OUTPUT.equals(output)) { + fail("xlate failed: " + output); + } + + } catch (Exception e) { + fail("Test failed: " + e); + } + } + + public static Test suite () + { + return new MessageBundleTest(); + } + + public static void main (String[] args) + { + MessageBundleTest test = new MessageBundleTest(); + test.runTest(); + } + + protected static final String OUTPUT = + "Meta arg one is 'Foo arg one is 'bar' and two is 'baz'.' and " + + "two is 'Biff arg one is 'beep' and two is 'boop'.'."; +} diff --git a/tests/src/java/com/threerings/util/UIDTest.java b/tests/src/java/com/threerings/util/UIDTest.java new file mode 100644 index 00000000..cabc45b6 --- /dev/null +++ b/tests/src/java/com/threerings/util/UIDTest.java @@ -0,0 +1,30 @@ +// +// $Id$ + +package com.threerings.util; + +import com.threerings.util.unsafe.Unsafe; + +/** + * Does something extraordinary. + */ +public class UIDTest +{ + public static void main (String[] args) + { + if (Unsafe.setuid(1000)) { + System.err.println("Yay! My uid is changed."); + } else { + System.err.println("Boo hoo! I couldn't change my uid."); + } + if (Unsafe.setgid(60)) { + System.err.println("Yay! My gid is changed."); + } else { + System.err.println("Boo hoo! I couldn't change my gid."); + } + try { + Thread.sleep(60*1000L); + } catch (Exception e) { + } + } +}